- var n = 0;

#wrapper
  #paper
    #cursor
    #dot1.dot
    #dot2.dot
    #dot3.dot
    #vertical-margin
    #horizontal-margins
      while n < 31
        .horizontal-margin= n++
    #letters
View Compiled
$gray250: rgb(250,250,250);
$gray245: rgb(245,245,245);
$gray240: rgb(240,240,240);
$gray230: rgb(230,230,230);
$gray220: rgb(220,220,220);
$gray200: rgb(200,200,200);
$gray180: rgb(180,180,180);
$gray150: rgb(150,150,150);
$gray120: rgb(120,120,120);
$gray90: rgb(90,90,90);
$gray60: rgb(60,60,60);
$gray30: rgb(30,30,30);

$blue: rgb(3,169,244);
$darkBlue: rgb(21,101,192);
$red: rgb(211,47,47);

$dark: rgb(21, 15, 30);

$shadow: rgba(0, 0, 0, 0.25) 0px 14px 45px, rgba(0, 0, 0, 0.22) 0px 10px 18px;
$dotShadow: rgba(0, 0, 0, 0.16) 0px 3px 10px inset, rgba(0, 0, 0, 0.23) 0px 3px 10px inset;

$horizontalMargins: 31;

@mixin center{
  left: 50%;
  position: absolute;
  top: 50%;
  transform: translateX(-50%) translateY(-50%);
}

html, body, #wrapper{
  font-family: 'Roboto Mono', sans-serif;
  height: 100%;
  margin: 0px;
  padding: 0px;
  width: 100%;
}

#wrapper{
  &::-webkit-scrollbar {
    width: 4px;
  }

  &::-webkit-scrollbar-thumb {
    background-color: white;
  }
}

#wrapper{
  background: linear-gradient(to right, $blue, $darkBlue);
  overflow-x: hidden;
  overflow-y: auto;
  position: relative;
  
  #paper{
    background-color: white;
    border-radius: 2px;
    box-shadow: $shadow;
    height: 700px;
    left: 50%;
    margin: 50px 0px;
    overflow: hidden;
    position: absolute;
    top: 0px;
    transform: translateX(-50%);
    width: 500px;
    
    &.typing{
      animation: rumble 0.82s cubic-bezier(.36,.07,.19,.97) infinite;
    }
    
    #cursor{
      animation: blink 1.2s linear infinite;
      background-color: $blue;
      height: 20px;
      left: 90px;
      position: absolute;
      top: 80px;
      width: 2px;
    }
    
    .dot{
      background-color: $blue;
      border-radius: 1000px;
      box-shadow: $dotShadow;
      height: 25px;
      left: 25px;
      position: absolute;
      width: 25px;
      z-index: 2;
      
      &#dot1{
        top: 70px;
      }
      
      &#dot2{
        top: 340px;
      }
      
      &#dot3{
        top: 600px;
      }
    }
    
    #vertical-margin{
      background-color: rgba($red, 0.5);
      height: 100%;
      left: 80px;
      position: absolute;
      top: 0px;
      width: 2px;
    }
    
    #horizontal-margins{
      .horizontal-margin{
        background-color: rgba($blue, 0.5);
        color: transparent;
        font-size: 0px;
        height: 2px;
        left: 0px;
        position: absolute;
        top: 80px;
        width: 100%;

        @for $i from 0 through $horizontalMargins{
          &:nth-child(#{$i}){
            top: 60px + (20px * $i);
          }
        }
      }
    }
    
    #letters{
      backface-visibility: hidden;
      min-height: 100%;
      overflow-x: hidden;
      overflow-y: auto;
      width: 100%;
    }
  }
}

.letter{
  display: inline-block;
  font-weight: 300;
  height: 20px;
  margin-left: 2px;
  position: absolute;
  text-align: center;
  transition: all 0.5s, color 0.1s;
  width: 10px;
  
  &.off-screen{
    font-weight: 700;
    transform: scale3d(10, 10, 10) rotate(1080deg);
  }
}

@keyframes blink{
  0%, 49%{
    opacity: 1;
  }
  50%, 99%{
    opacity: 0;
  }
}

@keyframes rumble {
  10%, 90% {
    transform: translateX(-50%) translate3d(-3px, 0, 0);
  }
  
  20%, 80% {
    transform: translateX(-50%) translate3d(4px, 0, 0);
  }

  30%, 50%, 70% {
    transform: translateX(-50%) translate3d(-2px, 0, 0);
  }

  40%, 60% {
    transform: translateX(-50%) translate3d(4px, 0, 0);
  }
}
View Compiled
const getEl = el => document.getElementById(el)
const setStyle = (el, prop, val) => el.style[prop] = val
const setAttr = (el, attr, val) => el.setAttribute(attr, val)
const addClass = (el, className) => el.classList.add(className)
const removeClass = (el, className) => el.classList.remove(className)
const resetStyles = el => el.removeAttribute('style')
const removeAllChildren = el => {
  while (el.hasChildNodes()) el.removeChild(el.lastChild)
}

const WRAPPER = getEl('wrapper'),
      PAPER = getEl('paper'),
      LETTERS = getEl('letters'),
      CURSOR = getEl('cursor')

let HAS_STARTED_TYPING = false,
    LAST_TYPE_TIMESTAMP = 0

const MIN_COL = 9,
      MAX_COL = (LETTERS.clientWidth / 10) - 2,
      MIN_ROW = 4,
      MAX_ROW = 34,
      LETTER_WIDTH = 10,
      LETTER_HEIGHT = 20,
      COLORS = {
        BLUE: 'rgb(3,169,244)',
        RED: 'rgb(239,83,80)',
        PURPLE: 'rgb(171,71,188)',
        GREEN: 'rgb(67,160,71)',
        YELLOW:' rgb(253,216,53)'
      }

const STATE = {
  range: 0.1,
  pos: {
    row: MIN_ROW,
    col: MIN_COL
  }
}

const getRandColor = () => {
  const rand = Math.floor((Math.random() * 5) + 1)
  switch(rand){
    case 1:
      return COLORS.BLUE
    case 2:
      return COLORS.RED
    case 3:
      return COLORS.PURPLE
    case 4:
      return COLORS.GREEN
    case 5:
      return COLORS.YELLOW
  }
}

const getRandPosOffScreen = () => {
  const lowX1 = 0 - (window.innerWidth * 0.3),
        highX1 = 0 - (window.innerWidth * 0.2),
        lowY1 = 0,
        highY1 = window.innerHeight,
        
        lowX2 = window.innerWidth * 1.2,
        highX2 = window.innerWidth * 1.3,
        lowY2 = 0,
        highY2 = window.innerHeight,
        
        lowX3 = 0,
        highX3 = window.innerWidth,
        lowY3 = 0 - (window.innerHeight * 0.3),
        highY3 = 0 - (window.innerHeight * 0.2),
        
        lowX4 = 0,
        highX4 = window.innerWidth,
        lowY4 = window.innerHeight * 1.2,
        highY4 = window.innerHeight * 1.3
  
  const rand = Math.floor((Math.random() * 4) + 1)
  
  let x = 0,
      y = 0
  
  switch(rand){
    case 1:
      x = Math.floor(Math.random() * (highX1 - lowX1 + 1)) + lowX1
      y = Math.floor(Math.random() * (highY1 - lowY1)) + lowY1
      break
    case 2:
      x = Math.floor(Math.random() * (highX2 - lowX2 + 1)) + lowX2
      y = Math.floor(Math.random() * (highY2 - lowY2)) + lowY2
      break
    case 3:
      x = Math.floor(Math.random() * (highX3 - lowX3 + 1)) + lowX3
      y = Math.floor(Math.random() * (highY3 - lowY3)) + lowY3
      break
    case 4:
      x = Math.floor(Math.random() * (highX4 - lowX4 + 1)) + lowX4
      y = Math.floor(Math.random() * (highY4 - lowY4)) + lowY4
      break
  }
  
  return { x, y }
}

const setLetterPos = (letter, x, y) => {
  setStyle(letter, 'left', x + 'px')
  setStyle(letter, 'top', y + 'px')
}

const setLetterColor = letter => {
  const color = getRandColor()
  setStyle(letter, 'color', color)
}

const createLetter = key => {
  const letter = document.createElement('div')
  letter.innerHTML = key
  setLetterColor(letter)
  addClass(letter, 'off-screen')
  addClass(letter, 'letter')
  return letter
}

const setInitialLetterPos = letter => {
  const pos = getRandPosOffScreen()
  setLetterPos(letter, pos.x, pos.y)
}

const isValidLetter = e => {
  return !e.ctrlKey 
    && e.key !== 'Enter'
    && !(e.key === ' ' && STATE.pos.col === MIN_COL)
}

const isEndOfPage = () => {
  return STATE.pos.row === MAX_ROW && STATE.pos.col === MAX_COL
}

const initializeLetter = key => {
  const letter = createLetter(key)
  setInitialLetterPos(letter)
  LETTERS.appendChild(letter)
  return letter
}

const bumpLetterPos = isUp => {
  if(isUp){
    if(STATE.pos.col < MAX_COL){
      STATE.pos.col = Math.min(STATE.pos.col + 1, MAX_COL)
    }
    else{
      STATE.pos.col = MIN_COL
      STATE.pos.row = Math.min(STATE.pos.row + 1, MAX_ROW)
    }
  }
  else{
    if(STATE.pos.col > MIN_COL){
      STATE.pos.col = Math.max(STATE.pos.col - 1, MIN_COL)
    }
    else{
      STATE.pos.col = MAX_COL
      STATE.pos.row = Math.max(STATE.pos.row - 1, MIN_ROW)
    }
  }
}

const bumpCursorPos = () => {
  const x = STATE.pos.col * LETTER_WIDTH + CURSOR.clientWidth,
        y = STATE.pos.row * LETTER_HEIGHT
  setLetterPos(CURSOR, x, y)
}

const determineFinalLetterPos = () => {
  let x = 0,
      y = 0
  if(STATE.pos.col <= MAX_COL){
    x = STATE.pos.col * LETTER_WIDTH
    y = STATE.pos.row * LETTER_HEIGHT
  }
  else{
    x = STATE.pos.col * LETTER_WIDTH
    y = (STATE.pos.row + 1) * LETTER_HEIGHT
  }
  
  bumpLetterPos(true)
  bumpCursorPos()
  
  return {x, y}
}

const setFinalLetterPos = letter => {
  const pos = determineFinalLetterPos()
  setLetterPos(letter, pos.x, pos.y)
}

const getLastLetter = () => {
  const letters = LETTERS.childNodes
  let letter = null
  for(let i = letters.length - 1; i >= 0; i--){
    if(!letters[i].dataset.removed){
      letter = letters[i]
      break
    }
  }
  return letter
}

const setLeavingLetterPos = letter => {
  const pos = getRandPosOffScreen()
  setLetterPos(letter, pos.x, pos.y)
  addClass(letter, 'off-screen')
}

const removeLetter = () => {
  const letter = getLastLetter(),
        color = getRandColor()
  if(letter === null) return 0
  LAST_TYPE_TIMESTAMP = moment()
  setStyle(letter, 'color', color)
  setLeavingLetterPos(letter)
  setAttr(letter, 'data-removed', true)
  bumpLetterPos(false)
  bumpCursorPos()
  setTimeout(() => {
    LETTERS.removeChild(letter)
  }, 500)
}

const handleAlternateKeys = e => {
  switch(e.keyCode){
    case 8: // Backspace
      removeLetter()
      break
    case 9: // Tab
      e.preventDefault()
      break
    case 13: // Enter
      break
  }
}

const typeLetter = key => {
  LAST_TYPE_TIMESTAMP = moment()
  const letter = initializeLetter(key)
  setFinalLetterPos(letter)
  setTimeout(() => {
    removeClass(letter, 'off-screen')
    setTimeout(() => setStyle(letter, 'color', 'black'), 500)
  }, 13)
}

let typeInterval = null
const typeSentence = sentence => {
  let i = 0
  typeInterval = setInterval(() => {
    typeLetter(sentence[i])
    if(i === sentence.length - 1) clearInterval(typeInterval)
    i++
  }, 200)
}

const onInitialType = () => {
  STATE.pos.row = MIN_ROW
  STATE.pos.col = MIN_COL
  removeAllChildren(LETTERS)
  clearInterval(typeInterval)
  HAS_STARTED_TYPING = true
}

const checkIfTyping = () => {
  const timeToLastType = moment() - LAST_TYPE_TIMESTAMP
  if(!PAPER.classList.contains('typing') && timeToLastType <= 300){
    addClass(PAPER, 'typing')
  }
  else if(PAPER.classList.contains('typing') && timeToLastType > 300){
    removeClass(PAPER, 'typing')
  }
}

window.onkeypress = e => {
  if(!HAS_STARTED_TYPING) onInitialType()
  if(!isEndOfPage() && isValidLetter(e)) typeLetter(e.key)
}

window.onkeydown = e => {
  if(!HAS_STARTED_TYPING) onInitialType()
  handleAlternateKeys(e)
}

window.onload = () => {
  typeSentence('Start typing something!')
  setInterval(() => checkIfTyping(), 300)
}

External CSS

  1. https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.0/moment.min.js