CSS + JS Text effects #pointer #text #faux-3d #sass-loop

Pointer-lit text in CSS + JavaScript

The extrusion is a Sass-generated shadow stack whose direction follows your pointer.

  • 26 lines of CSS
  • 3 lines of HTML
  • 11 lines of JS
  • No dependencies
  • MIT licensed
GitHub

How it works

  1. Same shadow-stack as the extruded text — but every offset is calc(var(--dx) * Npx).
  2. So the entire extrusion direction is controlled by just two numbers.
  3. JS treats the pointer as a light: it writes the opposite direction into --dx / --dy, and the shadow swings away from it.
  4. Without JS the defaults still give a perfectly good static extrusion — a nice progressive enhancement.

Key techniques

  • text-shadow with calc(var() × n)
  • Sass @function
  • pointer → custom properties

Copy-paste code

HTML 3 lines

<div class="scene">
  <h1 class="lit">SHADOW</h1>
</div>

CSS 26 lines

.scene {
  display: grid;
  place-items: center;
  width: 100vw;
  height: 100vh;
}

.lit {
  --dx: 0.7;   /* direction — overwritten from JS */
  --dy: 0.7;
  margin: 0;
  font: 900 4.5rem system-ui;
  color: #fff;
  text-shadow:
    calc(var(--dx) * 1px)  calc(var(--dy) * 1px)  0 #e6a340,
    calc(var(--dx) * 2px)  calc(var(--dy) * 2px)  0 #d99a3c,
    calc(var(--dx) * 3px)  calc(var(--dy) * 3px)  0 #cc9139,
    calc(var(--dx) * 4px)  calc(var(--dy) * 4px)  0 #bf8835,
    calc(var(--dx) * 5px)  calc(var(--dy) * 5px)  0 #b37f32,
    calc(var(--dx) * 6px)  calc(var(--dy) * 6px)  0 #a6762e,
    calc(var(--dx) * 7px)  calc(var(--dy) * 7px)  0 #996d2b,
    calc(var(--dx) * 8px)  calc(var(--dy) * 8px)  0 #8c6327,
    calc(var(--dx) * 9px)  calc(var(--dy) * 9px)  0 #805a24,
    calc(var(--dx) * 10px) calc(var(--dy) * 10px) 0 #735120,
    calc(var(--dx) * 22px) calc(var(--dy) * 22px) 20px rgb(0 0 0 / 0.5);
}

JS 11 lines

const scene = document.querySelector('.scene');
const text = document.querySelector('.lit');

scene.addEventListener('pointermove', (e) => {
  const r = scene.getBoundingClientRect();
  const x = (e.clientX - r.left) / r.width - 0.5;
  const y = (e.clientY - r.top) / r.height - 0.5;
  // the pointer is the light, so the shadow falls the other way
  text.style.setProperty('--dx', -x * 2);
  text.style.setProperty('--dy', -y * 2);
});

Sass source 31 lines

@use 'sass:list';

// Same shadow-stack idea as the extruded text, but every offset is multiplied by
// --dx / --dy at runtime, so JS can swing the whole extrusion with two numbers.
@function lit-extrude($depth) {
  $shadows: ();

  @for $i from 1 through $depth {
    $shade: color-mix(in srgb, var(--warm) #{(95 - $i * 5) * 1%}, #000);
    $shadows: list.append($shadows, calc(var(--dx) * #{$i}px) calc(var(--dy) * #{$i}px) 0 $shade, comma);
  }

  @return list.append($shadows, calc(var(--dx) * #{$depth * 2}px) calc(var(--dy) * #{$depth * 2}px) 20px rgb(0 0 0 / 0.5), comma);
}

.d-lit {
  --dx: 0.7;
  --dy: 0.7;
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;

  span {
    color: #fff;
    font-size: 50px;
    font-weight: 900;
    letter-spacing: 0.03em;
    text-shadow: lit-extrude(14);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.