Pure CSS Text effects #loop #text

Letter wave in pure CSS

Every letter does the same flip; the stagger turns it into a wave.

  • 24 lines of CSS
  • 11 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. Wrap every letter in a <span> with display: inline-block — transforms are ignored on plain inline boxes.
  2. All letters share one keyframe animation: lift toward the camera while flipping 360°.
  3. animation-delay: calc(var(--i) * 0.12s) offsets each letter, and the offsets read as a travelling wave.
  4. Put the real word in aria-label and hide the spans from screen readers, or it is read letter by letter.

Key techniques

  • inline-block letters
  • animation-delay from --i
  • rotateY flip

Copy-paste code

HTML 11 lines

<div class="scene">
  <h1 class="wave" aria-label="WAVE 3D">
    <span style="--i:0" aria-hidden="true">W</span>
    <span style="--i:1" aria-hidden="true">A</span>
    <span style="--i:2" aria-hidden="true">V</span>
    <span style="--i:3" aria-hidden="true">E</span>
    <span style="--i:4" aria-hidden="true">·</span>
    <span style="--i:5" aria-hidden="true">3</span>
    <span style="--i:6" aria-hidden="true">D</span>
  </h1>
</div>

CSS 24 lines

.scene {
  perspective: 600px;
}

.wave {
  display: flex;
  gap: 2px;
  margin: 0;
  font: 900 4rem system-ui;
  transform-style: preserve-3d;
}

.wave span {
  display: inline-block;
  color: hsl(calc(255 + var(--i) * 18) 90% 70%);
  animation: wave 2.8s ease-in-out infinite;
  animation-delay: calc(var(--i) * 0.12s);
}

@keyframes wave {
  0%        { transform: translateZ(0)    rotateY(0deg); }
  22%       { transform: translateZ(60px) rotateY(180deg); }
  45%, 100% { transform: translateZ(0)    rotateY(360deg); }
}

Sass source 30 lines

.d-waveletters {
  display: flex;
  gap: 2px;
  font-size: 46px;
  font-weight: 900;
  transform-style: preserve-3d;

  span {
    display: inline-block; // transforms are ignored on plain inline boxes
    color: hsl(calc(255 + var(--i) * 18) 90% 70%);
    text-shadow: 0 6px 14px rgb(0 0 0 / 0.5);
    animation: d-waveletters 2.8s ease-in-out infinite;
    animation-delay: calc(var(--i) * 0.12s);
  }
}

@keyframes d-waveletters {
  0% {
    transform: translateZ(0) rotateY(0deg);
  }

  22% {
    transform: translateZ(60px) rotateY(180deg);
  }

  45%,
  100% {
    transform: translateZ(0) rotateY(360deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.