Animating the Depth: Making Layered Text Move

The static 3D layered text effect is visually solid, but real engagement comes from motion. By animating the existing CSS custom properties and layer structure, we can create everything from subtle wobbles to fully dynamic patterns without touching the core technique.

Motion warning: The animated examples referenced here involve moving visuals. Proceed with care if you are sensitive to motion.

Wobble and Float

A common desire is to rotate an element while keeping the text itself readable. The solution is a composite rotation: rotate on the z-axis, add a tilt on the x-axis, and then reverse that initial z-axis rotation.

@keyframes wobble {
  0%, 100% { transform: rotateZ(-12deg) rotateX(0deg) rotateZ(12deg); }
  50%      { transform: rotateZ(-12deg) rotateX(15deg) rotateZ(12deg); }
}

Because the first and last z-rotations cancel each other out but happen around a moving x-axis, the result is a wobble that demonstrates the depth at every angle. Pair this with a floating motion on the .layers element along the z-axis.

.layers {
  animation: float 6s ease-in-out infinite;
}

@keyframes float {
  0%, 100% { transform: translateZ(-100px); }
  50%      { transform: translateZ(0px); }
}

To sell the illusion, leave the original span in place as a shadowed anchor. Make its text color transparent and animate the blur in its text-shadow.

span {
  color: transparent;
  text-shadow: 0 0 20px rgba(0,0,0,0.5);
  animation: shadowPulse 6s ease-in-out infinite;
}

@keyframes shadowPulse {
  0%, 100% { text-shadow: 0 0 20px rgba(0,0,0,0.5); }
  50%      { text-shadow: 0 0 10px rgba(0,0,0,0.8); }
}

Letter-by-Letter Motion

Animating an entire word as a single unit is straightforward, but splitting it into individual letters opens new possibilities. It does come with performance costs—more moving elements means more browser work. If you take this route, use fewer layers and animate fewer letters.

By giving the .scene a display: flex to align letters side-by-side and adding per-letter delays with :nth-child, each character can animate independently, producing a wave-like cascade across the word.

Beyond the Z-Axis

Movement is not restricted to depth. The layer transformation can use any axis. By basing these transforms on the --n custom property, a few interesting variations emerge:

  • Shifting: Animating translateX moves the text horizontally across layers.
  • Sloping: Rotating on the y-axis creates an angled, cascading effect.
  • Tilting: Rotation on the x-axis tips the word forward or backward.
  • Rotating: Applying rotation on the z-axis spins the layered stack.

Staggered Layer Delays

Animating an entire block with scale produces a simple pulse:

.layeredText {
  animation: pulse 2s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.03); }
}

But a richer effect comes from applying that same animation to each individual layer with a slight delay. The :is selector is useful here for including both the underlying span and the styled .layer elements in the same animation rule.

.layers span,
.layers :is(.layer) {
  animation: pulse 2s ease-in-out infinite;
}

The result is a sequential pulsing, where each level of the stack moves slightly after the one below it.

Pseudo-Element Decoration

Pseudo elements are well-suited for adding decorative flair—outlines, arrows, or other shapes—on top of the existing layer stack. Since each layer can have ::before and ::after, you get extra visual interest that automatically inherits the 3D layout.

When you only want to target a portion of the stack, combine the class with :nth-child logic. To select only the bottom twelve layers out of a total of twenty-four, use a selector like:

.layer:nth-child(-n + 12)::after {
  content: "→";
  /* decoration styles */
}

This keeps decorations from overlapping the readable portion of the text. Different ranges are possible with more complex selectors. For a 3D "loading" display, for instance, you might isolate layers six through eighteen:

.layer:nth-child(n + 6):nth-child(-n + 18)::before {
  content: "";
  /* spinner styles */
}

When faking shadows with blurred pseudo elements, be aware that filter: blur() can be a performance drain and should be used sparingly.

Patterns on the Top Face

Instead of pseudo elements, you can style the text itself with any pattern. Target the top layer with :last-child. Set the text to transparent, and use background-clip: text so a background-image is visible through the glyphs.

.layer:last-child {
  color: transparent;
  background-image: repeating-linear-gradient(45deg, #333 0 10px, #ccc 10px 20px);
  background-clip: text;
  -webkit-background-clip: text;
}

Works with any background image—striped gradients, repeating radial rings, or even photographic imagery.

Full 3D Patterns

The pattern trick works on more than just the top layer. Apply it to every layer with a repeating pattern for a full 3D-printed look. Store the primary text color in a --color custom property, then make all layer text transparent.

.layer, span {
  color: transparent;
  --color: #1a1a1a;
}

A moving checkerboard is easy with a repeating-conic-gradient, using the --color variable and a semi-transparent black for contrast:

.layer {
  background-image: repeating-conic-gradient(
    from 0deg,
    var(--color) 0deg 90deg,
    rgba(0,0,0,0.2) 90deg 180deg
  );
  background-size: 20px 20px;
  background-clip: text;
  -webkit-background-clip: text;
  animation: patternMove 20s linear infinite;
}

@keyframes patternMove {
  from { background-position: 0 0; }
  to   { background-position: 100px 0; }
}

Because the --n variable correlates with depth, the pattern layers shift in sync, and the simple animation of background-position creates a cohesive moving texture.

Variable Font Axes as Depth

Variable fonts provide unique opportunities when their axes change across layers. The Climate Crisis font includes a YEAR axis (1979 to 2025). When set based on the layer index, the text appears to melt and shrink progressively, creating an ecological statement in 3D.

Other examples include the classic weight axis of Bitcount (100 to 900). Setting the weight per layer generates a ridgeline effect that looks like peaks rising from the text. The Kablammo font has a MORF axis that changes letter shapes entirely, and font-variation-settings is animatable. Combining that with a per-layer delay produces a lively distortion wave—though it can be demanding on the browser.

Animated Layer Positioning

The static transform for instance: transform: translateZ(calc(var(--n) * 10px)). The translateZ value can also be animated. Starting all layers at the same point (zero depth) and animating each one's z-position with an equal delay between them produces the 3D compound look and its motion. This technique leans toward advanced applications and isn't required for every project, but is valuable for certain sophisticated scenes.

Moving Forward

Layered text is far from static. Whether it's the subtle wobble or a fully textured, moving pattern, the depth—built on reusable CSS custom properties—amplifies what motion does. Everything covered here remains self-contained and predictable, a stepping stone toward the interactivity that follows: :hover states and JavaScript-driven input that will push the effect further.