Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

To make layered text interactive, keep the title as real HTML, place decorative copies behind it, and map a user input—usually pointer position—to a restrained CSS transform. CSS creates an impression of depth, not extruded letter geometry: the effect comes from flat text layers, perspective and motion working together. A good implementation stays readable without interaction and has a reduced-motion fallback.

What “dynamic” layered text means

Layered text begins as a stack of repeated copies of the same words, offset slightly to suggest an extruded edge. Add motion and the stack tilts or shifts; add interactivity and its appearance responds to the user. Dynamicism can go further: the effect can adapt to viewport size, changing content, scroll position and motion preferences.

CSS 3D transforms position two-dimensional elements in a three-dimensional coordinate system; they do not turn glyphs into solid geometry. Perspective, overlap, shadows and occlusion create the illusion. See the CSS Transforms specification for the 3D rendering model.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Start with semantic text and decorative layers

Keep one real, readable title in the document. Mark its visual duplicates as hidden from assistive technology so a screen reader does not announce the same words repeatedly.

<div class="scene">
  <h1 class="hero-title">
    <span class="hero-title__front">DEPTH</span>
    <span class="hero-title__layers" aria-hidden="true">
      <span class="hero-title__layer" style="--i: 1">DEPTH</span>
      <span class="hero-title__layer" style="--i: 2">DEPTH</span>
      <span class="hero-title__layer" style="--i: 3">DEPTH</span>
    </span>
  </h1>
</div>

For a short fixed heading, hand-written layers are simple. For reusable headings or adjustable depth, generate decorative copies from the front text. Use text content, not copied arbitrary HTML, and keep the original heading element and its semantic role intact.

Build the static depth effect

Put perspective on the scene that acts as the viewer, preserve the shared 3D context on the layer container, and move each copy by its index. The following values are starting points, not standards: tune them for the typeface, title size and background.

.scene {
  --layer-x: 0.8px;
  --layer-y: 0.8px;
  --layer-z: 1px;
  position: relative;
  perspective: 600px;
  isolation: isolate;
}

.hero-title {
  position: relative;
  display: inline-block;
  margin: 0;
  font-size: clamp(3rem, 12vw, 10rem);
  line-height: 0.9;
  letter-spacing: -0.06em;
}

.hero-title__front,
.hero-title__layers,
.hero-title__layer {
  width: max-content;
}

.hero-title__front,
.hero-title__layer {
  display: block;
  white-space: nowrap;
}

.hero-title__layers {
  position: absolute;
  inset: 0;
  pointer-events: none;
  transform-style: preserve-3d;
}

.hero-title__layer {
  position: absolute;
  inset: 0;
  color: rgb(255 255 255 / 55%);
  transform: translate3d(
    calc(var(--i) * var(--layer-x)),
    calc(var(--i) * var(--layer-y)),
    calc(var(--i) * var(--layer-z))
  );
}

.hero-title__front {
  position: relative;
  z-index: 1;
  color: white;
  text-shadow: 0.02em 0.02em 0.04em rgb(0 0 0 / 35%);
}

perspective controls foreshortening: smaller values exaggerate depth, while larger values flatten it. translateZ() changes a layer’s position, not its shape. A parent scene with roughly 400–1,000px perspective and 8–24 copies is a reasonable visual starting range for a display title, but test the result rather than treating those numbers as rules. Keep every duplicate’s font, line-height and letter spacing aligned or the edges will drift.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Generate layers when text or depth changes

Dynamic generation is useful when titles change, multiple headings share one effect, or the layer count is configurable. This function reads plain text and adds hidden decoration without replacing the semantic heading:

function createLayers(element, count = 12) {
  const text = element.textContent.trim();
  const front = document.createElement("span");
  const layers = document.createElement("span");

  front.className = "hero-title__front";
  front.textContent = text;
  layers.className = "hero-title__layers";
  layers.setAttribute("aria-hidden", "true");

  for (let index = 1; index <= count; index++) {
    const layer = document.createElement("span");
    layer.className = "hero-title__layer";
    layer.style.setProperty("--i", index);
    layer.textContent = text;
    layers.append(layer);
  }

  element.replaceChildren(front, layers);
}

If application state changes the title later, update the front and regenerate or update every decorative layer as well. Do not leave stale copies behind. If you measure text for positioning, allow fonts to finish loading first—document.fonts.ready can help—or use a close fallback and recheck after layout changes.

Add pointer-following motion

A group tilt is the simplest interaction: normalize pointer position within the scene to a range from −1 to 1, then rotate the decorative stack. Interpolation smooths small pointer movements. Use Pointer Events so the same event model works across input devices, but limit hover-driven behavior to fine pointers.

const scene = document.querySelector(".scene");
const layers = scene.querySelector(".hero-title__layers");
const finePointer = matchMedia("(hover: hover) and (pointer: fine)");
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");

let targetX = 0;
let targetY = 0;
let currentX = 0;
let currentY = 0;
let frameRequested = false;

function render() {
  frameRequested = false;
  currentX += (targetX - currentX) * 0.12;
  currentY += (targetY - currentY) * 0.12;

  layers.style.setProperty("--pointer-x", currentX);
  layers.style.setProperty("--pointer-y", currentY);

  if (Math.abs(targetX - currentX) > 0.001 ||
      Math.abs(targetY - currentY) > 0.001) {
    frameRequested = true;
    requestAnimationFrame(render);
  }
}

function queueRender() {
  if (!frameRequested) {
    frameRequested = true;
    requestAnimationFrame(render);
  }
}

function onPointerMove(event) {
  if (!finePointer.matches || reducedMotion.matches) return;
  const bounds = scene.getBoundingClientRect();
  if (!bounds.width || !bounds.height) return;

  targetX = Math.max(-1, Math.min(1,
    ((event.clientX - bounds.left) / bounds.width) * 2 - 1));
  targetY = Math.max(-1, Math.min(1,
    ((event.clientY - bounds.top) / bounds.height) * 2 - 1));
  queueRender();
}

function resetPointer() {
  targetX = 0;
  targetY = 0;
  queueRender();
}

scene.addEventListener("pointermove", onPointerMove);
scene.addEventListener("pointerleave", resetPointer);

reducedMotion.addEventListener("change", () => {
  if (reducedMotion.matches) resetPointer();
});
.hero-title__layers {
  transform: rotateX(calc(var(--pointer-y, 0) * -6deg))
             rotateY(calc(var(--pointer-x, 0) * 8deg));
  transform-origin: center;
  transition: transform 100ms ease-out;
}

@media (hover: hover) and (pointer: fine) {
  .scene { cursor: default; }
}

The handler reads the scene bounds to normalize coordinates. If profiling shows that measurement is costly, cache dimensions and refresh them with a resize observer. The animation loop is requested only when needed, rather than continuously running. Avoid letting several handlers write competing values to the same transform.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the kind of response deliberately

  • Rotate the group: Stable, restrained and usually the least complicated option.
  • Displace each layer: Can create a stronger swell or “bulge,” but risks making the copies visibly separate.
  • Move only the extrusion or shadow: Often adds enough response without disturbing the letter faces.
  • Parallax the front and back: Adds depth cues, but too much separation harms legibility.

For a bulge, add a small pointer-dependent term to each layer’s offset. Keep its scale low: the index multiplies the movement, so far-back copies otherwise travel much farther.

.hero-title__layer {
  transform: translate3d(
    calc(var(--i) * var(--layer-x) + var(--pointer-x, 0) * var(--i) * 0.12px),
    calc(var(--i) * var(--layer-y) + var(--pointer-y, 0) * var(--i) * 0.12px),
    calc(var(--i) * var(--layer-z))
  );
}

CSS-only motion, scroll and other inputs

CSS is enough for a simple hover or focus response, a transition, or a controlled keyframe animation. For example:

.scene:hover .hero-title__layers,
.scene:focus-within .hero-title__layers {
  transform: rotateX(-5deg) rotateY(8deg) translateZ(10px);
  transition: transform 500ms cubic-bezier(.2, .8, .2, 1);
}

.scene:focus-visible .hero-title__front {
  outline: 2px solid currentColor;
  outline-offset: 0.12em;
}

Use JavaScript when the title must continuously follow pointer position, generated content changes, or multiple inputs need to be combined. Scroll progress, pointer movement, focus and device orientation are not interchangeable inputs. If combining them, keep them in one state model and derive one final transform; separate event handlers that overwrite transform will conflict. Scroll-linked animation can also be implemented in CSS where support in the target browsers is sufficient.

Touchscreens do not have hover. A static depth treatment is a reliable mobile fallback; alternatively, a tap can trigger a short response and settle back. Do not leave a permanent hover state after a tap. Device orientation should not be activated automatically: if it is used, make it an optional control and account for browser permission behavior.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Accessibility: make motion optional and text usable

Decorative copies should be inside an aria-hidden="true" container, while the meaningful title remains ordinary text in its proper heading level. Hiding duplicate layers is necessary, but it does not by itself make the experience accessible. Preserve contrast, keyboard focus visibility and a readable static state. W3C guidance favors real text over text embedded in images because users and assistive technologies can resize, recolor and otherwise adapt it (W3C technique C22).

Rank #4
Blank Flip Book Paper with Holes - 240 Sheets (480 Pages) Flipbook Animation Paper : Works with Flip Book Kit Light Pads: for Drawing, Sketching Supplies/Comic Book Kit - Drawing Paper Animation Kit
  • Complete animation paper set … A great animation starter kit! Our 240 sheet (480 pages) flipbook paper with holes is quality 4.5inch x 2.5inch 120 gsm flippable paper and binding screws!
  • Perfect starter kits ... No more making your own flipbooks with scraps and staples. Our kits come with 2 sizes of binding screws, allowing you to trace and make flipbooks of many different sizes!
  • Individual pages ... Creating your own movies and animation has never been easier. No more limits on your animations that sewn binding books give you - With individual pages YOU get to decide!
  • Tracing made easy ... With our beautiful thick individual pages it is much easier to use with a light source, such as flip book light pads (not included) to trace your animations
  • Easy drawing ... No more spiral binding or pesky sewn book spines getting in your way. Our sketch pad paper is individual and free, just like your stop motion animations

Respect a reduced-motion preference and provide a minimal depth treatment rather than a moving tilt:

@media (prefers-reduced-motion: reduce) {
  .hero-title__layers {
    transform: none !important;
    transition: none !important;
    animation: none !important;
  }

  .hero-title__layer {
    transform: translate3d(
      calc(var(--i) * 0.35px),
      calc(var(--i) * 0.35px),
      0
    ) !important;
  }
}

The reduce preference is documented by MDN’s guide to accessibility media queries. If a visible motion toggle is appropriate, make it a real button and ensure it also stops JavaScript updates; CSS alone cannot prevent a script from continually writing transforms. Avoid making hover the only way to reach a readable or meaningful state.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Keep the effect resilient and performant

  • Layers and cost: Start with a modest number of copies and add more only if the visible payoff justifies it. Avoid animating text shadows across many layers.
  • Animate transforms and opacity: They are generally preferable to layout-changing properties such as width, margin or position. This does not guarantee smooth performance; device, text rasterization, layer count and shadows all matter. See MDN’s CSS performance guidance.
  • Coalesce pointer updates: Use requestAnimationFrame(), avoid layout writes in pointer handlers, and consider pausing or simplifying the effect when the scene is offscreen.
  • Do not overuse will-change: Promoting every copy can consume memory without making the effect better.
  • Check responsive layout: Keep duplicate line-height, letter spacing, width and font declarations consistent. Long or localized words may wrap differently; short display titles are better candidates than paragraph text or navigation labels.
  • Resolve transform conflicts: Compose rotation and translation in one transform declaration or use nested wrappers for scene perspective, group tilt and per-layer depth.
  • Test on actual mobile hardware: A desktop preview cannot establish how a dense stack or animated shadows will perform on a phone.

Troubleshooting

  • The stack looks flat: Confirm the scene has perspective, the relevant parent has transform-style: preserve-3d, and no later rule replaces the transform.
  • Copies drift: Check identical typography and dimensions, ensure the title’s containing block is positioned, and account for late font loading or wrapping.
  • Motion jitters: Interpolate instead of applying raw coordinates, coalesce writes through animation frames, and reduce layers or shadows.
  • The title is hard to read: Reduce tilt, extrusion offset and shadow opacity; increase the front face’s contrast.
  • Screen readers repeat the title: Put decorative copies under aria-hidden="true" and expose only the semantic text.
  • Reduced motion has no effect: Check the media query, stop JavaScript updates as well as CSS animation, and make sure a manual toggle changes both.

When CSS layering is the right tool

For a hero heading, logo or short label that needs an approximate extrusion, HTML and CSS with a small JavaScript controller are usually enough. If the motion needs coordinated timelines or scroll sequences, an animation library such as GSAP may help; its pricing page says the library is free for all users, but it is unnecessary for a single pointer-to-variable effect. A broader authored 3D scene may call for a tool such as Spline, while reusable stateful designer-authored graphics may suit Rive. Those tools solve wider problems and may move text out of the ordinary DOM model.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A focused helper such as DepthText can be considered if avoiding custom layer and input code is worth adding a dependency; evaluate its maintenance, license, accessibility and runtime behavior yourself. Its published size and performance statements are vendor claims, not independent benchmarks. For genuine extruded geometry, lighting and camera control, use a 3D or WebGL approach and supply an accessible text alternative. For body copy and dense interfaces, ordinary semantic text with restrained styling is the better choice.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.