Pure CSS Text effects #loop #text

Layered text in pure CSS

Real depth this time: ten copies of GO DEEP stacked along Z, so perspective is correct.

  • 39 lines of CSS
  • 15 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The shadow-stack text is a 2D trick: its "depth" always points the same way. This version has real depth.
  2. Ten copies of the words are stacked with position: absolute and pushed back 3 units × i along Z.
  3. The words are two lines, GO over DEEP, and every copy carries the same <br>, so the copies line up and the wall runs down both lines. DEEP alone is nearly three times wider than it is tall, and at the width limit it stood under the 40vmin floor.
  4. Each copy is darker than the one in front of it, which shades the side wall.
  5. Rotate the parent and the side wall appears on the correct side, with true perspective. Cost: more DOM, and the copies need aria-hidden.
  6. The unit is --u, one number on the root that every length here is a multiple of, so the wall is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • stacked copies with translateZ
  • aria-hidden duplicates
  • compare with the shadow version

Copy-paste code

HTML 15 lines

<div class="scene">
  <h1 class="deep">
    <b>GO<br>DEEP</b>
    <span style="--i:1" aria-hidden="true">GO<br>DEEP</span>
    <span style="--i:2" aria-hidden="true">GO<br>DEEP</span>
    <span style="--i:3" aria-hidden="true">GO<br>DEEP</span>
    <span style="--i:4" aria-hidden="true">GO<br>DEEP</span>
    <span style="--i:5" aria-hidden="true">GO<br>DEEP</span>
    <span style="--i:6" aria-hidden="true">GO<br>DEEP</span>
    <span style="--i:7" aria-hidden="true">GO<br>DEEP</span>
    <span style="--i:8" aria-hidden="true">GO<br>DEEP</span>
    <span style="--i:9" aria-hidden="true">GO<br>DEEP</span>
    <span style="--i:10" aria-hidden="true">GO<br>DEEP</span>
  </h1>
</div>

CSS 39 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the words are the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.37vmin;
  perspective: calc(800 * var(--u));
}

/* Two lines, because DEEP on its own is nearly three times wider than it is tall: sized to the
   92vmin width limit it stood 28vmin high, under the 40vmin floor. A kicker over the word is what
   a headline does anyway, and every copy carries the same two lines, so the wall runs down both. */
.deep {
  position: relative;
  margin: 0;
  font: 900 calc(90 * var(--u))/0.92 Inter, system-ui;
  text-align: center;
  transform-style: preserve-3d;
  animation: deep-rock 5s ease-in-out -1.75s infinite alternate;
}

.deep b {
  position: relative;
  color: #fff;
}

.deep span {
  position: absolute;
  inset: 0;
  color: color-mix(in srgb, #ff4d9d calc(100% - var(--i) * 7%), #000);
  transform: translateZ(calc(var(--i) * -3 * var(--u)));
}

/* The wall hangs off one side, so the words sit off centre at either end of the rock, further at the
   38deg end than at the -38deg one: the rock is nudged 2 units left to balance the two ends, and the
   -1.75s delay starts it a third of the way through a swing, turned about 20deg, so a paused card
   shows the wall and still sits in the middle. */
@keyframes deep-rock {
  from { transform: translateX(calc(-2 * var(--u))) rotateY(-38deg) rotateX(10deg); }
  to   { transform: translateX(calc(-2 * var(--u))) rotateY(38deg)  rotateX(-8deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.