Pure CSS Text effects #text #loop #layers

Isometric block text in pure CSS

One word stacked twenty times along the Z axis. Laid on a tilted floor, the copies merge into a solid extrusion under a gradient front.

  • 51 lines of CSS
  • 25 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The same word is stacked 21 times, each copy one step further down the Z axis via translateZ(calc(var(--i) * -1.2 * var(--u))), where --u is the one base unit every length here is a multiple of. Seen at an angle the copies merge into a solid extrusion.
  2. rotateX(54deg) rotateZ(-32deg) lays the whole stack on an isometric-looking floor: tip it back, then turn it on the floor.
  3. Each copy gets slightly darker as --i grows (hsl(... calc(46% - var(--i) * 1.3%))) — a cheap ambient shadow down the "sides" with no lighting math.
  4. The front copy carries a gradient clipped to the text with background-clip: text; the last, deepest copy is blurred into a floor shadow instead.
  5. alternate + ease-in-out makes the sway loop seamless: the way back is the mirror image of the way there.
  6. The floor shadow hangs below the word, so the whole stack is lifted by half of it. What is drawn is then centred on the model box, which the word on its own would not be.

Key techniques

  • translateZ(calc(var(--i) * -1.2 * var(--u)))
  • rotateX + rotateZ floor
  • lightness from --i
  • background-clip: text

Copy-paste code

HTML 25 lines

<div class="scene">
  <div class="depth">
    <span style="--i:0">Depth</span>
    <span aria-hidden="true" style="--i:1">Depth</span>
    <span aria-hidden="true" style="--i:2">Depth</span>
    <span aria-hidden="true" style="--i:3">Depth</span>
    <span aria-hidden="true" style="--i:4">Depth</span>
    <span aria-hidden="true" style="--i:5">Depth</span>
    <span aria-hidden="true" style="--i:6">Depth</span>
    <span aria-hidden="true" style="--i:7">Depth</span>
    <span aria-hidden="true" style="--i:8">Depth</span>
    <span aria-hidden="true" style="--i:9">Depth</span>
    <span aria-hidden="true" style="--i:10">Depth</span>
    <span aria-hidden="true" style="--i:11">Depth</span>
    <span aria-hidden="true" style="--i:12">Depth</span>
    <span aria-hidden="true" style="--i:13">Depth</span>
    <span aria-hidden="true" style="--i:14">Depth</span>
    <span aria-hidden="true" style="--i:15">Depth</span>
    <span aria-hidden="true" style="--i:16">Depth</span>
    <span aria-hidden="true" style="--i:17">Depth</span>
    <span aria-hidden="true" style="--i:18">Depth</span>
    <span aria-hidden="true" style="--i:19">Depth</span>
    <span aria-hidden="true" style="--i:20">Depth</span>
  </div>
</div>

CSS 51 lines

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

.depth {
  position: relative;
  transform-style: preserve-3d;
  transform: translateY(calc(-17 * var(--u))) rotateX(54deg) rotateZ(-32deg);
  animation: sway 7s ease-in-out infinite alternate;
  font-size: calc(50 * var(--u));
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  user-select: none;
}

.depth span {
  position: absolute;
  inset: 0;
  /* 1.2 units per step: at this tilt that is about one screen pixel on a card, so no gaps show */
  transform: translateZ(calc(var(--i) * -1.2 * var(--u)));
  color: hsl(252 62% calc(46% - var(--i) * 1.3%));
}

/* the front copy gives the element its size and carries the gradient */
.depth span:first-child {
  position: relative;
  display: block;
  background: linear-gradient(100deg, #2ee6d6, #8b6cff 45%, #ff4d9d);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* the last copy is the soft shadow on the floor: static, so the blur costs nothing per frame */
.depth span:last-child {
  color: rgb(0 0 0 / 0.55);
  text-shadow: 0 0 calc(10 * var(--u)) rgb(0 0 0 / 0.6);
  transform: translateZ(calc(-25 * var(--u))) translate(calc(-5 * var(--u)), calc(7 * var(--u)));
}

/* the 17-unit lift answers the floor shadow and the extrusion, which both hang below the word:
   the word plus what it casts is what has to sit in the middle of the box */
@keyframes sway {
  from { transform: translateY(calc(-17 * var(--u))) rotateX(56deg) rotateZ(-40deg); }
  to   { transform: translateY(calc(-17 * var(--u))) rotateX(48deg) rotateZ(-22deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.