---
name: 3d-text-extrusion
description: Use when you want "Bold, dramatic, retro/futuristic" - Creates depth by extending text into 3D space.
---

# 3D Text Extrusion

> **Category:** Text  -  **Personality:** Bold, dramatic, retro/futuristic
>
> **Best use:** Hero headlines, event pages
>
> **Ratings:** Professional 3/5 - Casual 4/5 - Exotic 5/5 - Visual impact 5/5 - Difficulty 4/5 - Performance cost 4/5

## What it is

3D text extrusion fakes real, solid depth on a flat headline by stacking many slightly-offset copies of the glyphs behind the face — each step a little darker — so the letters read as chunky blocks pushed into the screen. The whole extrusion is one layered `text-shadow`, which means a tiny script can **rewrite the shadow on every pointer move** to swing the depth direction with the cursor while the headline plane tilts in perspective. It is loud, retro-futuristic and instantly says "event poster / hero headline". With no JavaScript — or when the visitor asks for reduced motion — it falls back to a fixed straight-down extrusion that still looks fully 3D.

## Dependencies / CDN

None for the effect — pure CSS for the depth, vanilla JS for the pointer tilt (no library, no SRI needed). The demo only pulls two Google Fonts (a heavy poster display + a retro mono):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
```

## HTML

The backdrop (`.tx-sky` / `.tx-sun` / `.tx-grid`) is decorative dressing; only the `#txTitle` headline carries the effect.

```html
<div class="tx-stage" id="txStage">
  <!-- decorative synthwave backdrop (optional) -->
  <div class="tx-sky"></div><div class="tx-sun"></div><div class="tx-grid"></div>

  <div class="tx-poster">
    <span class="tx-kick">&#9670; Synthwave Festival &middot; Vol. 07 &#9670;</span>
    <h2 class="tx-title" id="txTitle">
      <span class="tx-line">Neon</span><span class="tx-line">Horizon</span>
    </h2>
    <p class="tx-meta">SAT 15 AUG 2026 &mdash; DOCKYARD 9, TEL AVIV PORT</p>
  </div>
</div>
```

## CSS

The headline rule is the whole recipe — a chrome gradient face plus a stacked, progressively-darker `text-shadow` for the extruded sides:

```css
.tx-title{
  margin:0;
  font-family:'Anton',sans-serif;          /* heavy + condensed extrudes cleanly */
  text-transform:uppercase; line-height:.86;
  font-size:clamp(58px,14vw,150px);

  /* optional "chrome" face: a vertical gradient clipped to the glyphs */
  color:#ffd9ef;                            /* fallback if background-clip unsupported */
  background:linear-gradient(180deg,#fff 0%,#d7faff 16%,#36f9f6 32%,
                             #b06bff 54%,#ff4d8d 74%,#ff9b45 100%);
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;

  /* THE EFFECT: each offset copy is one slab of depth; colours darken toward the back */
  text-shadow:
    0 1px 0 #7a1450, 0 3px 0 #6a1149, 0 5px 0 #5a0e42, 0 7px 0 #4b0b3a,
    0 9px 0 #3d0833, 0 11px 0 #2f0629, 0 13px 0 #220420,
    0 18px 26px rgba(0,0,0,.55),            /* contact shadow under the block */
    0 0 36px rgba(255,45,149,.45);          /* neon glow */

  transform:perspective(900px) rotateX(0deg) rotateY(0deg);
  transition:transform .25s ease-out;
  will-change:transform,text-shadow;
}
.tx-line{display:block}

/* reduced motion = static: no tilt, keep the straight-down extrusion above */
@media (prefers-reduced-motion:reduce){
  .tx-title{transition:none; transform:none}
}
```

## JavaScript

Optional. It regenerates the same `text-shadow` list each pointer move so the **direction** of the extrusion sweeps with the cursor, and tilts the plane in perspective. It bails out entirely under `prefers-reduced-motion`, leaving the CSS static block.

```js
(function(){
  var stage=document.getElementById('txStage'), title=document.getElementById('txTitle');
  if(!stage||!title) return;
  if(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;

  var STEPS=18, nx=0, ny=0, raf=0;
  var c0=[122,20,80], c1=[28,6,40];                 // shade: just under the face -> deep back
  function lerp(a,b,t){return Math.round(a+(b-a)*t);}
  function shade(t){return 'rgb('+lerp(c0[0],c1[0],t)+','+lerp(c0[1],c1[1],t)+','+lerp(c0[2],c1[2],t)+')';}

  function render(){
    raf=0;
    var fs=parseFloat(getComputedStyle(title).fontSize)||120;
    var deeper=1+(-ny)*0.28, depth=fs*0.13*deeper, unit=depth/STEPS;
    var ang=(90-nx*52)*Math.PI/180, cos=Math.cos(ang), sin=Math.sin(ang), s=[];   // 90deg = straight down
    for(var i=1;i<=STEPS;i++){
      var off=i*unit;
      s.push((cos*off).toFixed(2)+'px '+(sin*off).toFixed(2)+'px 0 '+shade((i-1)/(STEPS-1)));
    }
    s.push((cos*depth).toFixed(1)+'px '+(sin*depth+fs*0.08).toFixed(1)+'px '+Math.round(fs*0.22)+'px rgba(0,0,0,.55)');
    s.push('0 0 '+Math.round(fs*0.3)+'px rgba(255,45,149,.45)');
    title.style.textShadow=s.join(',');
    title.style.transform='perspective(900px) rotateX('+(ny*6)+'deg) rotateY('+(nx*13)+'deg)';
  }
  stage.addEventListener('pointermove',function(e){
    var r=stage.getBoundingClientRect();
    nx=Math.max(-1,Math.min(1,((e.clientX-r.left)/r.width -0.5)*2));
    ny=Math.max(-1,Math.min(1,((e.clientY-r.top )/r.height-0.5)*2));
    if(!raf) raf=requestAnimationFrame(render);                 // coalesce to one paint/frame
  });
  stage.addEventListener('pointerleave',function(){nx=0;ny=0;if(!raf)raf=requestAnimationFrame(render);});
  render();
})();
```

## How it works

- **`text-shadow` is the extrusion.** A shadow is the glyph silhouette filled with the shadow colour, so a list of shadows stepped `0 1px`, `0 3px`, `0 5px`… paints a solid wall of glyphs receding behind the face. Keep the steps ≈1–2px apart or you see gaps between slabs.
- **Darkening per step** (`shade()` lerps a mid-magenta toward a deep plum) gives the side wall a lit-to-shadow gradient, which is what makes it read as a real block instead of a flat drop shadow.
- **Direction = a vector.** Each step is placed along `(cos θ, sin θ)`. The script maps cursor-X to the angle (`90° − nx·52°`, i.e. straight-down at centre, sweeping to down-left / down-right), so the extrusion swings as you move — the headline feature.
- **`perspective() rotateX/rotateY`** tilts the whole plane a few degrees toward the pointer, reinforcing the directional depth.
- **The last two shadows** are a blurred contact shadow (lifts the block off the page) and a neon glow (the synthwave halo).
- **Graceful by default:** the CSS ships a complete straight-down extrusion, so it looks finished with JS disabled or motion reduced.

## Customization

- **Depth:** raise `STEPS` and/or the `fs*0.13` factor for a deeper block; lower for a subtle emboss. Depth scales with font-size automatically.
- **Direction range:** the `nx*52` term is how far the extrusion swings; drop to `*25` for a restrained tilt, raise toward `*70` for drama. Set `ang` to a constant for a fixed, JS-free look.
- **Colour:** change `c0`/`c1` for the side-wall gradient, and the face `background` gradient for the front. A single solid `text-fill-color` instead of the clip gives a flat retro face.
- **Tilt:** tune `rotateX(ny*6)` / `rotateY(nx*13)`; set both to 0 to keep depth-swing without plane rotation.
- **Font:** any heavy face works — Anton, Archivo Black, Unbounded 800. Avoid thin/light weights.

## Accessibility & performance

- The headline is real `<h2>` text — fully selectable, readable by screen readers, and indexable; the 3D is purely visual. The backdrop layers are `aria-hidden`.
- The pointer tilt is a progressive enhancement (mouse/pen/touch via Pointer Events); keyboard and no-pointer users get the polished static block.
- `prefers-reduced-motion: reduce` is honoured in both CSS (no transition/tilt) and JS (the script returns before binding any listener).
- Pointer updates are coalesced through `requestAnimationFrame`, so we rebuild the shadow at most once per frame. Animate `transform`/`text-shadow` strings — never animate `filter: blur()` per step.
- Ensure the bright face keeps contrast over the dark stage; the dark extrusion + glow already separate the glyphs from the backdrop.

## Gotchas

- **`text-shadow` still renders when the fill is transparent.** That is *why* the chrome combo (`background-clip:text` + `-webkit-text-fill-color:transparent`) works — the shadows draw from the glyph shape, not the fill. Always include a solid `color` fallback for browsers without `background-clip:text`.
- **Too few steps = a stripey, hollow block.** The offset between consecutive shadows must be ≤ ~2px or daylight shows between the slabs; that is the trade-off behind the "performance cost" rating (many shadow layers per glyph).
- **One blur, not many.** A blur radius on every step is expensive; keep blur only on the final contact-shadow and glow layers — the extrusion slabs use `0` blur.
- **Heavy, condensed fonts only.** Light or wide faces extrude into illegible mush; a poster face like Anton has the thick uniform strokes the technique needs.
- **`background-clip:text` needs the `-webkit-` prefix** and a transparent fill, and it can be flattened by some text-effects/filters on the same element — apply the clip and shadows directly on the text node.
