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

Pointer-lit text in CSS + JavaScript

The extrusion is a stack of text-shadows whose direction follows your pointer.

  • 41 lines of CSS
  • 3 lines of HTML
  • 11 lines of JS
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Same shadow-stack as the extruded text — but every offset is calc(var(--dx) * N * var(--u)).
  2. So the entire extrusion direction is controlled by just two numbers, and its size by a third: --u, the one base unit every length here is a multiple of.
  3. JS treats the pointer as a light: it writes the opposite direction into --dx / --dy, and the shadow swings away from it. It writes plain numbers, never lengths, so the CSS stays in charge of the scale.
  4. Without JS the defaults still give a perfectly good static extrusion — a nice progressive enhancement.
  5. The headline stands on one line. It is many times wider than it is tall, so it is sized by its width, like the other wide models in the gallery, not by its height: it is marked wide in its gallery entry.

Key techniques

  • text-shadow with calc(var() × n)
  • works without JS: default direction
  • pointer → custom properties

Copy-paste code

HTML 3 lines

<div class="scene" tabindex="0" role="img" aria-label="LIGHT SHADOW, a headline whose long shadow falls away from the pointer">
  <h1 class="lit">LIGHT SHADOW</h1>
</div>

CSS 41 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the headline is the same share of
     a card, the editor, a full screen and a recording canvas */
  --u: 0.145vmin;
  display: grid;
  place-items: center;
  width: 100%;   /* the whole canvas is the light's field, so the pointer is read across it */
  height: 100%;
}

/* One line, as a headline is: LIGHT SHADOW draws many times wider than it is tall, so it is sized
   by its width (it is marked 'wide'): the unit puts it near the width the gallery's other wide
   models have */
.lit {
  --dx: 0.7;   /* direction — overwritten from JS */
  --dy: 0.7;
  margin: 0;
  font: 900 calc(72 * var(--u))/1.2 Inter, system-ui;
  text-align: center;
  white-space: nowrap;
  transform: translateY(calc(-4 * var(--u)));
  color: #fff;
  /* the long shadow in a deep amber, so the white face stands 3:1 or more off it (large text) where
     the shadow shows at its edges; from a bright #e6a340 it was 2.2:1 */
  text-shadow:
    calc(var(--dx) * 1 * var(--u))  calc(var(--dy) * 1 * var(--u))  0 #b07424,
    calc(var(--dx) * 2 * var(--u))  calc(var(--dy) * 2 * var(--u))  0 #a56d22,
    calc(var(--dx) * 3 * var(--u))  calc(var(--dy) * 3 * var(--u))  0 #9a6620,
    calc(var(--dx) * 4 * var(--u))  calc(var(--dy) * 4 * var(--u))  0 #905f1d,
    calc(var(--dx) * 5 * var(--u))  calc(var(--dy) * 5 * var(--u))  0 #85581b,
    calc(var(--dx) * 6 * var(--u))  calc(var(--dy) * 6 * var(--u))  0 #7a5019,
    calc(var(--dx) * 7 * var(--u))  calc(var(--dy) * 7 * var(--u))  0 #6f4917,
    calc(var(--dx) * 8 * var(--u))  calc(var(--dy) * 8 * var(--u))  0 #654214,
    calc(var(--dx) * 9 * var(--u))  calc(var(--dy) * 9 * var(--u))  0 #5a3b12,
    calc(var(--dx) * 10 * var(--u)) calc(var(--dy) * 10 * var(--u)) 0 #4f3410,
    calc(var(--dx) * 22 * var(--u)) calc(var(--dy) * 22 * var(--u)) calc(20 * var(--u)) rgb(0 0 0 / 0.5);
}

/* the keyboard: Tab to it and the shadow falls as it would for a pointer near the top right, even
   with the pointer resting on the canvas (!important beats the direction the script writes inline) */
.scene:focus-visible .lit { --dx: -0.8 !important; --dy: 0.4 !important; }

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);
});
Standalone snippet · plain CSS, no build step, no dependencies.