<div class="container">
  <div class="content">
    <p class="year">0</p>
    <p class="message">
      <span>H</span>
      <span>A</span>
      <span>P</span>
      <span>P</span>
      <span>Y</span>
      <span>&nbsp;</span>
      <span>N</span>
      <span>E</span>
      <span>W</span>    
      <span>&nbsp;</span>
      <span>Y</span>
      <span>E</span>
      <span>A</span>    
      <span>R</span>        
    </p>
  </div>
</div>
:root {
  --ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.container {
  width: 100%;
  min-height: 100vh;
  padding: 30px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #333;
  -webkit-font-smoothing: antialiased;
}
.year {
  font-size: 36px;
  text-align: center;
  font-family: 'Roboto Mono', monospace;
}
.message {
  font-size: 22px;  
  margin-top: 24px;
  position: absolute;
  width: 100%;
  left: 0;
  display: flex;
  justify-content: center;
  font-family: 'Poppins', sans-serif;
  span {
    opacity: 0;
    transform: translateY(18px);
  }
  &.is-active {
    span {
      opacity: 1;
      transform: translateY(0);
      transition: opacity 200ms linear, transform 640ms var(--ease-out-back);
      @for $i from 0 through 15 {
        &:nth-child(#{$i}) {
          transition-delay: #{500 + $i * 40}ms;
        }
      }
    } 
  }
}
View Compiled
type Easing = (t: number) => number

type TweenOptions = {
  from: number
  to: number
  duration: number
  easing: Easing
  onUpdate(value: number): void
}

function clamp(num: number, min: number, max: number) {
  return Math.min(Math.max(num, min), max)
}

function tween({ from, to, duration, easing, onUpdate }: TweenOptions): Promise<void> {
  return new Promise((resolve) => {
    const startTime = performance.now()
    
    const tick = () => {
      const elapsedTime = performance.now() - startTime
      const progress = clamp(elapsedTime / duration, 0, 1)
      const value = from + (to - from) * easing(progress)
      
      onUpdate(value)
      
      if (progress === 1) {
        resolve()
      } else {
        requestAnimationFrame(tick)
      }
    }
    
    onUpdate(from)
    requestAnimationFrame(tick)
  })
}

const easeOutQuart = (t: number) => 1 - Math.pow(1 - t, 4)

async function countUp() {
  const year = document.querySelector<HTMLElement>('.year')
  const message = document.querySelector<HTMLElement>('.message')  

  if (year) {
    await tween({
      from: 0,
      to: 2022,
      duration: 3000,
      easing: easeOutQuart,
      onUpdate: val => {
        year.textContent = Math.floor(val).toString()
      }
    })
    
    message.classList.add('is-active')
  }
}

countUp()
View Compiled

External CSS

  1. https://fonts.googleapis.com/css2?family=Poppins:wght@300&amp;family=Roboto+Mono:wght@200&amp;display=swap

External JavaScript

This Pen doesn't use any external JavaScript resources.