GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

CSS Text Shadow Effects — 9 Patterns

css text shadow effects: A shadow is where light goes missing, yet these nine letters shine by shadow alone — item 1 stacks depth by a layer value, item 5

Auto-plays · click a tile to jump to its section · all nine in one zip

Nine CSS Text Shadow Effects — Where One Value Changes the Shadow Each Time

Every pattern below reads one JavaScript number and writes it straight into a text-shadow string, the same four-part syntax (MDN's reference covers all four) of offset-x offset-y blur-radius color. The order moves from a shadow that only ever looks static (item 1's stacked layers) to one that only reacts to a real cursor (item 9's hover lift): items 1 through 4 build a fixed 3D read from layered offsets, item 5 breaks that stability on purpose, items 6 and 7 add color and count as the moving variable, and items 8 and 9 hand the driving value to the reader's own scroll or pointer.

01Extruded Letters Whose Layer Value Stacks Into Depth

Six shadow layers each shift the same one-pixel step further along the same diagonal, and extrudeColor() darkens each layer's lightness as it goes so the deepest layer reads darkest — like a css text shadow 3d effect built from stacking, not from perspective. buildExtrudeShadow() reads a single step value on a 1s sine loop between 1px and 15px, which is exactly what a landing-page hero title would want: thin at rest, thick as it settles.

Extruded Letters Whose Layer Value Stacks Into Depth
Open in a new tab

01 · Extruded Letters Whose Layer Value Stacks Into Depth — Measured area 10.384% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

SHADOW_LAYER_COUNTbuildExtrudeShadowrenderExtrude
  // build a step value into SHADOW_LAYER_COUNT stacked shadow layers as one string
  function buildExtrudeShadow(step) {
    var parts = [];
    for (var i = 1; i <= SHADOW_LAYER_COUNT; i++) {
      var d = (i * step).toFixed(2);
      parts.push(d + 'px ' + d + 'px 0 ' + extrudeColor(i, SHADOW_LAYER_COUNT));
    }
    return parts.join(', ');
  }

Measured: 10.4% cumulative area, 26.0 average intensity, 22/23 frames moved.

02Letters Raised by an Opposed Light and Dark Shadow Value

One angleDeg value feeds buildEmbossShadow(), which points a bright shadow one way and a dark shadow the exact opposite way — same value, opposite sign, so as the angle turns a card badge label reads as raised metal at one moment and stamped-in at the next. There's no separate "pressed" state to toggle; the same formula produces both looks depending only on where the angle currently sits.

Letters Raised by an Opposed Light and Dark Shadow Value
Open in a new tab

02 · Letters Raised by an Opposed Light and Dark Shadow Value — Measured area 10.903% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

EMBOSS_DEPTH_PXbuildEmbossShadowrenderEmboss
  // place a light shadow and a dark shadow on opposite sides from one angleDeg value
  function buildEmbossShadow(angleDeg) {
    var rad = (angleDeg * Math.PI) / 180;
    var lx = (Math.cos(rad) * EMBOSS_DEPTH_PX).toFixed(2);
    var ly = (Math.sin(rad) * EMBOSS_DEPTH_PX).toFixed(2);
    var dx = (-Math.cos(rad) * EMBOSS_DEPTH_PX).toFixed(2);
    var dy = (-Math.sin(rad) * EMBOSS_DEPTH_PX).toFixed(2);
    return [
      lx + 'px ' + ly + 'px 0 rgba(255,255,255,.85)',
      dx + 'px ' + dy + 'px 1px rgba(10,10,14,.75)'
    ].join(', ');
  }

Measured: 10.9% cumulative area, 129.2 average intensity, 23/23 frames moved.

03Letters Whose Eight-direction Values Merge Into an Outline

DIRS loops over eight directions — the four axis directions plus the four diagonals — so buildOutlineShadow() spends one distance value eight times to ring a glyph with color and leave the fill untouched, useful for poster numerals that need to read against any background behind them. Shrinking that one value below its 0.4px floor doesn't drop any copies; the loop always runs all eight regardless of how thin the outline gets.

Letters Whose Eight-direction Values Merge Into an Outline
Open in a new tab

03 · Letters Whose Eight-direction Values Merge Into an Outline — Measured area 10.6% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

OUTLINE_MAX_PXbuildOutlineShadowrenderOutline
  var DIRS = [
    [1, 0], [-1, 0], [0, 1], [0, -1],
    [0.7071, 0.7071], [-0.7071, 0.7071], [0.7071, -0.7071], [-0.7071, -0.7071]
  ];

  var word = document.getElementById('olWord');
  var value = document.getElementById('olValue');
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // overlay one distance value d across eight directions so only the outline remains, no fill
  function buildOutlineShadow(d) {
    var parts = [];
    for (var i = 0; i < DIRS.length; i++) {
      var x = (DIRS[i][0] * d).toFixed(2);
      var y = (DIRS[i][1] * d).toFixed(2);
      parts.push(x + 'px ' + y + 'px 0 rgba(23,20,26,.92)');
    }
    return parts.join(', ');
  }

Measured: 10.6% cumulative area, 140.2 average intensity, 21/23 frames moved.

04Retro Letters Whose Shadow Stretches by a Diagonal Length Value

buildRetroShadow() doesn't offset one shadow further — it adds a new 1px-diagonal layer for every unit of len, so a vintage campaign headline gets as many stacked steps as the value calls for, each one slightly more transparent than the last. At the loop's low end only four layers exist; at its high end all 34 do, which is the difference between a short nudge and a long dragging trail.

Retro Letters Whose Shadow Stretches by a Diagonal Length Value
Open in a new tab

04 · Retro Letters Whose Shadow Stretches by a Diagonal Length Value — Measured area 10.081% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

RETRO_MAX_LENbuildRetroShadowrenderRetro
  // repeat a len value one 1px step at a time along the diagonal until it stretches that far
  function buildRetroShadow(len) {
    var parts = [];
    var n = Math.round(len);
    for (var i = 1; i <= n; i++) {
      parts.push(i + 'px ' + i + 'px 0 rgba(10,79,232,' + (0.9 - (i / n) * 0.35).toFixed(2) + ')');
    }
    return parts.join(', ');
  }

Measured: 10.1% cumulative area, 140.3 average intensity, 22/23 frames moved.

05Glitch Letters That Fracture by a Color-split Value

seedRand() is a fixed-seed generator, not Math.random() — the same 11-seed LCG produces the same sequence every reload, so buildGlitchShadow() looks random but isn't. Clicking the word reseeds it with a new number and snaps mag to a 14x burst for 220ms, which is the one item in this set with a real click listener rather than a scripted loop alone.

Glitch Letters That Fracture by a Color-split Value
Open in a new tab

05 · Glitch Letters That Fracture by a Color-split Value — Measured area 10.158% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GLITCH_SPLIT_PXseedRandbuildGlitchShadow
  // shake one mag value with seeded noise to split a red shadow and a blue shadow apart
  function buildGlitchShadow(mag) {
    var rx = (mag * (rnd() * 2 - 1)).toFixed(2);
    var ry = (mag * 0.28 * (rnd() * 2 - 1)).toFixed(2);
    var bx = (-mag * (rnd() * 2 - 1)).toFixed(2);
    var by = (-mag * 0.28 * (rnd() * 2 - 1)).toFixed(2);
    return [
      rx + 'px ' + ry + 'px 0 rgba(255,54,96,.88)',
      bx + 'px ' + by + 'px 0 rgba(58,164,255,.88)'
    ].join(', ');
  }

Measured: 10.2% cumulative area, 144.0 average intensity, 23/23 frames moved.

06Metallic Letters Whose Brightness Value Shifts by Layer

buildSheenShadow() runs a sine curve through each of seven layers at a slightly different phase, so turning the angle value doesn't just move a highlight — it moves a bright band down through the stack, the way light travels across brushed metal. The word "metal" here means the read a premium brand logotype would want, not a specific finish — no brand name backs the sheen itself.

Metallic Letters Whose Brightness Value Shifts by Layer
Open in a new tab

06 · Metallic Letters Whose Brightness Value Shifts by Layer — Measured area 10.386% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SHEEN_LAYER_COUNTbuildSheenShadowrenderSheen
  // one angle value layers a sine-curve brightness per layer so top and bottom shine
  // differently, like metal sheen -- as phase drifts, the bright band travels down the stack
  function buildSheenShadow(angleDeg) {
    var rad = (angleDeg * Math.PI) / 180;
    var parts = [];
    for (var i = 1; i <= SHEEN_LAYER_COUNT; i++) {
      var phase = rad + (i / SHEEN_LAYER_COUNT) * Math.PI * 1.4;
      var bright = (Math.sin(phase) + 1) / 2;
      var l = Math.round(22 + bright * 58);
      var y = (i * 6.8).toFixed(2);
      parts.push('0px ' + y + 'px 0 hsl(220 10% ' + l + '%)');
    }
    return parts.join(', ');
  }

Measured: 10.4% cumulative area, 43.9 average intensity, 23/23 frames moved.

07Letters That Read as Stacked Paper by a Layer Value

Each pass through buildPaperShadow()'s loop pushes two shadows per layer — a flat sheet color and a soft dark edge beneath it — and alternates a warm and cool sheet tone by layer parity, so a card-news title graphic reads as up to six loose pages rather than one flat stack. The layer count itself is the animated value: at 1 it's a single sheet, at PAPER_LAYER_COUNT (6) it's the full pile.

Letters That Read as Stacked Paper by a Layer Value
Open in a new tab

07 · Letters That Read as Stacked Paper by a Layer Value — Measured area 11.642% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

PAPER_LAYER_COUNTbuildPaperShadowrenderPaper
  // stack n paper layers from one layer value n, each with its own color and distance --
  // more layers read as more sheets stacked on top of each other
  function buildPaperShadow(n) {
    var parts = [];
    for (var i = 1; i <= n; i++) {
      var d = (i * OFFSET_STEP_PX).toFixed(2);
      var warm = i % 2 === 0;
      var sheet = warm ? 'rgba(255,247,230,.92)' : 'rgba(246,238,222,.92)';
      parts.push(d + 'px ' + d + 'px 0 ' + sheet);
      parts.push(d + 'px ' + (Number(d) + 1.4).toFixed(2) + 'px 2px rgba(23,20,26,' + (0.22 - i * 0.02).toFixed(2) + ')');
    }
    return parts.join(', ');
  }

Measured: 11.6% cumulative area, 145.0 average intensity, 18/23 frames moved.

08Letters Whose Shadow Grows by a Scroll-progress Value

buildScrollShadow() never runs on a timer — ratioFromScroll() reads the viewport's own scrollTop against its scrollHeight on a native scroll event, so a long about-page section heading only grows its shadow as far as the reader has actually scrolled. Until a real wheel, touchstart, or keydown fires, a scripted loop fakes the same ratio so the gif isn't a flat 0%; the first real scroll event replaces it for good.

Letters Whose Shadow Grows by a Scroll-progress Value
Open in a new tab

08 · Letters Whose Shadow Grows by a Scroll-progress Value — Measured area 9.894% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SCROLL_MAX_PXbuildScrollShadowrenderScroll
  // feed one scroll-progress value (ratio 0-1) straight into the shadow distance, so it grows on the way down
  function buildScrollShadow(ratio) {
    var d = (ratio * SCROLL_MAX_PX).toFixed(2);
    return [
      (d * 0.4).toFixed(2) + 'px ' + d + 'px 0 rgba(10,79,232,.5)',
      (d * 0.4).toFixed(2) + 'px ' + (d * 2).toFixed(2) + 'px ' + (4 + ratio * 10).toFixed(1) + 'px rgba(10,79,232,' + (0.28 + ratio * 0.3).toFixed(2) + ')'
    ].join(', ');
  }

  function renderScroll(ratio) {
    word.style.textShadow = buildScrollShadow(ratio);
    value.textContent = Math.round(ratio * 100) + '%';
  }

  function ratioFromScroll() {
    var max = viewport.scrollHeight - viewport.clientHeight;
    return max > 0 ? Math.max(0, Math.min(1, viewport.scrollTop / max)) : 0;
  }

  viewport.addEventListener('scroll', function () {
    renderScroll(ratioFromScroll());
  });
  ['wheel', 'touchstart', 'keydown'].forEach(function (ev) {
    viewport.addEventListener(ev, function () { hasUser = true; }, { passive: true });
  });

Measured: 9.9% cumulative area, 66.5 average intensity, 23/23 frames moved.

09Letters Whose Depth Value Lifts When the Cursor Arrives

buildLiftShadow() takes a 0-1 proximity value and pushes two things at once from it — the shadow's offset and its blur radius — while a separate curP += (targetP - curP) * LERP line eases the value itself, so a portfolio project card title doesn't snap to full depth the instant the pointer lands. Focus and hover trigger the same targetP, so a keyboard tab into the word lifts it exactly as a mouse would.

Letters Whose Depth Value Lifts When the Cursor Arrives
Open in a new tab

09 · Letters Whose Depth Value Lifts When the Cursor Arrives — Measured area 11.546% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

HOVER_LIFT_PXbuildLiftShadowrenderLift
  // one proximity value (p 0-1) pushes the shadow offset and blur together so the letters
  // read as lifted off the background; HOVER_LIFT_PX sets the base depth unit
  function buildLiftShadow(p) {
    var y = (p * HOVER_LIFT_PX * 1.6).toFixed(2);
    var y2 = (p * HOVER_LIFT_PX * LIFT_MULTIPLIER).toFixed(2);
    var blur = (4 + p * HOVER_LIFT_PX * 6.5).toFixed(1);
    return [
      '0px ' + y + 'px 0 rgba(10,79,232,' + (0.55 + p * 0.3).toFixed(2) + ')',
      '0px ' + y2 + 'px ' + blur + 'px rgba(10,79,232,' + (0.22 + p * 0.36).toFixed(2) + ')'
    ].join(', ');
  }

Measured: 11.5% cumulative area, 48.3 average intensity, 23/23 frames moved.

Where the Value Drives the Shadow — Layer Count, Offset Distance, Color Split

Item Cumulative area Avg. intensity Moved frames
01 Extrude layer stack 10.4% 26.0 22/23
02 Emboss light/dark 10.9% 129.2 23/23
03 Eight-direction outline 10.6% 140.2 21/23
04 Diagonal retro trail 10.1% 140.3 22/23
05 RGB-split glitch 10.2% 144.0 23/23
06 Layered metal sheen 10.4% 43.9 23/23
07 Paper stack 11.6% 145.0 18/23
08 Scroll-linked depth 9.9% 66.5 23/23
09 Hover depth lift 11.5% 48.3 23/23

Item 7's average intensity (145.0) is the highest of the nine and its moved-frame count (18/23) the lowest — PAPER_LAYER_COUNT only steps between six integer states across the loop, so each step is a bigger pixel jump than a continuously-eased value like item 9's curP produces, even though item 9 covers more frames. Three constants carry most of that difference: SHADOW_LAYER_COUNT (item 1) controls how many offset copies exist at all, GLITCH_SPLIT_PX (item 5) controls how far apart two colors sit, and HOVER_LIFT_PX (item 9) controls how far one interaction pushes an offset and a blur together — a css font shadow effect built from any of the nine functions above is really just one of these three kinds of number, read at a different rate.

Where It Breaks — Reduced Motion, Low-end Devices, and Long Text Wrapping

Every one of the nine .ex__word-style rules sets white-space: nowrap, so a headline longer than its container doesn't wrap — it overflows past the edge or gets clipped by whatever sits around it, and none of the nine functions recompute their offsets for a second line, because every offset here is one flat pixel number, not a value relative to line height or column width. That's fine for the one- and two-character demo words above; it stops being fine the moment a real headline runs three or four words long on a narrow phone screen, where the clamp() font size shrinks but the layout rule underneath it does not change.

Layer count is also a frame-time cost, not just a look: item 1's variant can push SHADOW_LAYER_COUNT to 14, item 3 always draws eight, and item 7 draws two shadows per paper layer — each one a full repaint of the glyph outline at whatever size clamp() currently renders, every frame, for as long as the loop runs. A low-end phone rendering a 140px text-shadow with a dozen-plus layers on a continuous requestAnimationFrame loop is doing meaningfully more paint work per frame than the same phone rendering item 8's two-part shadow, which only recomputes on an actual scroll event.

prefers-reduced-motion: reduce stops every rAF loop and locks each pattern to one fixed state instead — mostly its deepest or most extreme value (items 1, 3, 4, 6, and 7 jump straight to their maximum), except item 8, which was never on a timer to begin with, and item 9, which still answers a real hover or focus event but snaps to full lift instead of easing through LERP. Item 5 is the one exception worth being honest about: even under reduced motion, clicking the word still fires a one-time burst to 14x the base split for 220ms — a discrete state change on click, not a continuous animation, which is why it's left running rather than disabled outright. The kdbb8nch that opens all nine index.html/style.scss pairs sits in this paragraph on purpose — anyone who reads this far already knows which of the nine constants they'd change first.

Where a text-shadow Effect Like This Actually Earns Its Place

Item 1's extrude and item 6's sheen both read best on a hero title that already has room to breathe — a landing page or a premium brand logotype, somewhere a shadow can run 100-140px tall without competing with body text underneath it. Item 2's emboss and item 3's outline work at the opposite scale: a card badge label or a poster's event numerals, small enough that the raised-or-pressed read or the eight-direction ring has to land in one glance, not over a few seconds of looking.

Item 5's glitch and item 4's retro trail are the two that ask for a specific mood before a specific size — an error screen headline for the glitch, a vintage campaign headline for the long diagonal trail — and neither reads right dropped into a neutral UI that isn't already leaning that direction. Item 8 and item 9 are the two actually driven by what the reader does: a long about-page section heading growing its own shadow as the page scrolls, or a portfolio project card title lifting only when a cursor or a keyboard tab actually reaches it.

Three Variants and What prefers-reduced-motion Leaves Standing

Variant Value change What shifts
Adjust the extrude layer-count value SHADOW_LAYER_COUNT 6 → 14 Shortening the value makes the letters look thin and nearly flat; lengthening it stacks far more layers for a much thicker extrude
Adjust the glitch color-split distance value GLITCH_SPLIT_PX 2 → 6 Shortening the value offsets the color channels only slightly for a subtle wobble; lengthening it pulls them far apart for a much harsher broken look
Adjust the hover depth-lift value HOVER_LIFT_PX 4 → 12 Shortening the value barely stretches the shadow on hover, making the change hard to notice; lengthening it stretches the shadow far enough that the letters look lifted off the screen

None of the three variants touch what reduced motion removes above — SHADOW_LAYER_COUNT at 6 or at 14 still jumps straight to its maximum under prefers-reduced-motion: reduce, and HOVER_LIFT_PX at 4 or at 12 still answers pointerenter and focus without lerping either way. What accessibility still needs from all nine, regardless of which constant moves: none of the nine words carry aria-hidden, so a screen reader reads the letters themselves and never touches the shadow layers stacked behind them — the shadow is decoration a reader can skip entirely, which is exactly what it should be.

How Are These Nine css text shadow examples Different From the Default text-shadow?

Written by hand, text-shadow takes one to a handful of comma-separated layers and stops — a designer picks the offsets once and the CSS never changes them again. Every pattern above instead recomputes the same property from one JavaScript number on every frame or every event, which is the actual difference: not the CSS property (all nine still just set .style.textShadow), but whether a human or a loop is choosing the numbers. MDN's filter reference documents drop-shadow(), the nearest CSS-only alternative — it follows a glyph's actual alpha shape instead of its box, but it can't be layered the way text-shadow can, so none of the nine functions above use it.

Questions Worth Answering Before You Layer a text-shadow Effect

Can I Use a css text shadow generator Instead of Copying These Nine Functions?

A generator that outputs a single fixed text-shadow value works fine for items that don't move — copy its output straight into item 2's or item 3's CSS and skip the JavaScript entirely. It can't replace the loop-driven items (1, 4, 5, 6, 7, 8, 9), though, since those read a changing value every frame; a generator only ever describes one frame at a time.

Why Does Item 5's Split Snap Instead of Just Looping Smoothly?

Because seedRand() is deterministic, not random in the true sense — the same LCG sequence plays out every time the page loads, and a click reseeds it with a new starting number rather than picking a genuinely unpredictable one. The 220ms burst after a click is a separate branch in the code (burstUntil), not a continuation of the looping sine-based wobble the rest of the cycle runs on.

Does Stacking Many text-shadow Layers Actually Cost Real Render Time?

Yes, proportionally to layer count and glyph size — each layer is a separate paint pass behind the same text, so item 3's fixed eight layers cost more per frame than item 2's two, and item 1's variant pushing SHADOW_LAYER_COUNT to 14 costs roughly twice what its default of 6 does. None of the nine trigger layout (text-shadow never changes an element's box), so the cost stays in paint, not reflow — real on a slow device, but bounded.

For the same idea of one value driving a whole visual system, 9 CSS Glow Effect Animations — Neon, Copy-Paste applies a comparable layered-shadow approach to color and blur instead of depth; 9 CSS Text Highlight Animations, Copy-Paste is worth comparing item 2's opposed-shadow emboss against, since both change how flat text reads against its background without touching the glyphs themselves; and 9 CSS Text Animation Effects for Reveals is the closest sibling in purpose — both sets exist to make static text feel considered, but reveals move the letters themselves while every pattern above only moves the shadow behind them.

Enter the archive password

The password is inside this article. You will find it as you read.