Add this component

npx zentauri-ui add masked-heading

Accessibility notes

Keyboard accessible by default with semantic markup, visible focus treatment, and tokenized states. Add descriptive labels for icon-only or decorative usage.

Dependency notes

Check Installation for shared peers. This component keeps styling in Tailwind classes and the --zui-* token contract.

Typography & motion

Letters as windows.

A real heading whose words open onto an image, a looping video, or a drifting gradient mesh. Each word rises out of its own line, then the media drifts slowly and follows the pointer.

Type studies / 002

Designed in the details

Real heading textSVG clip, no canvasMotion-aware

Your type lab

Tune the reveal.

Use the page theme toggle to compare light and dark.


Designed in the details

Choose a gradient mesh. It shows through the letters whenever no image or video is set.

One heading. Many windows.

Rise lifts each word out of its own line. Wipe uncovers the media from the inline start. Fade settles the whole heading into focus. None keeps the heading still.

A gradient mesh, word by word


Designed in the details

A photograph wiped in from the inline start


Beyond the horizon

Outlined letters that fill with light


Light finds the edges

Still, centered, and motion-free


Quiet by design

The words stay words.

The copy is ordinary text in a real heading element, so its accessible name, selection, and translation all work. The media, the SVG mask, and the outline are decorative: they are hidden from assistive tech and never intercept the pointer.

System reduced-motion settings show the final heading at once, keep the media still, and never start video. Touch input does not move the media. With scripts off or forced colors on, the heading renders as plain readable text.

One damped animation loop drives the entrance, drift, and parallax. It writes SVG attributes and styles directly, with no React state per frame. It pauses while the heading is off-screen or the tab is hidden, and stops once the media is still. Video plays only while the heading is visible.

Masked Heading API

Generated from the package prop types and variant definitions.

MaskedHeading

MaskedHeadingProps

Variants

PropTypeDefault
align
centerendstart
start
appearance
auroracandyemberforestmidnightmonooceansunset
aurora
size
lgmdsmxl
lg

Behavior

PropTypeDefault
delayDelay before the entrance starts, in milliseconds. Clamped to 0–10000.number | undefined0
driftAmplitude of the slow idle drift of the media, in px. 0 holds it still. Clamped to 0–120.number | undefined16
durationEntrance duration in milliseconds. Clamped to 0–10000.number | undefined1100
levelHeading level, rendered as `h1`–`h6`.MaskedHeadingLevel | undefined2
mediaTypeKind of media `src` points to. Videos are muted, looped and paused for reduced motion.MaskedHeadingMediaType | undefined"image"
onRevealCompleteCalled once when the entrance finishes, or immediately when it is skipped.(() => void) | undefinednone
outlineStroke every letter so its shape is visible before the media arrives.boolean | undefinedfalse
parallaxHow far the media slides under the letters as a mouse or pen moves, in px. 0 disables it. Clamped to 0–120.number | undefined24
posterPoster frame shown while a video loads.string | undefinednone
revealEntrance style.MaskedHeadingReveal | undefined"rise"
srcImage or video shown through the letters. Without it, the appearance's gradient mesh shows.string | undefinednone
staggerDelay between words in milliseconds for `rise`. Clamped to 0–1000.number | undefined90
triggerWhen the entrance runs.MaskedHeadingTrigger | undefined"view"

Content

PropTypeDefault
children*Heading copy. Split into words for the reveal; stays readable to assistive tech.stringnone
Inherited HTML props
PropTypeDefault
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLHeadingElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

MaskedHeadingVariant

MaskedHeadingVariantProps

Behavior

PropTypeDefault
align'center' | 'end' | 'start' | null | undefinednone
appearance'ocean' | 'forest' | 'sunset' | 'aurora' | 'ember' | 'candy' | 'midnight' | 'mono' | null | undefinednone
size'md' | 'sm' | 'lg' | 'xl' | null | undefinednone
CSS variable overrides

Masked Heading CSS variables

Theme the heading type, the gradient mesh shown through the letters, the outline stroke and the media placeholder. Motion is configured through props; private --_mh-* values are not a public token API.

40 variables

Pattern: --zui-<component>-<slot?>-<variant?>-<property>-<state?>-dark?

:root {
  --zui-masked-heading-fg: #0f172a;
  --zui-masked-heading-font: initial;
  --zui-masked-heading-weight: 800;
  --zui-masked-heading-leading: 1.05;
  --zui-masked-heading-tracking: -0.03em;
  --zui-masked-heading-sm-size: clamp(2rem,1.5rem + 2.5vw,3rem);
  --zui-masked-heading-md-size: clamp(2.5rem,1.75rem + 4vw,4.5rem);
  --zui-masked-heading-lg-size: clamp(3rem,1.75rem + 6vw,6rem);
  --zui-masked-heading-xl-size: clamp(3.5rem,1.5rem + 9vw,8rem);
  --zui-masked-heading-aurora-mesh: radial-gradient(at 18% 28%,#06b6d4 0,transparent 55%),radial-gradient(at 82% 18%,#8b5cf6 0,transparent 52%),radial-gradient(at 55% 88%,#10b981 0,transparent 58%),linear-gradient(135deg,#0284c7,#4f46e5);
  --zui-masked-heading-sunset-mesh: radial-gradient(at 15% 25%,#f97316 0,transparent 55%),radial-gradient(at 85% 30%,#db2777 0,transparent 55%),radial-gradient(at 50% 90%,#eab308 0,transparent 60%),linear-gradient(135deg,#ea580c,#be185d);
  --zui-masked-heading-ocean-mesh: radial-gradient(at 20% 20%,#0ea5e9 0,transparent 55%),radial-gradient(at 80% 70%,#1d4ed8 0,transparent 55%),radial-gradient(at 45% 95%,#14b8a6 0,transparent 60%),linear-gradient(160deg,#0369a1,#1e3a8a);
  --zui-masked-heading-forest-mesh: radial-gradient(at 25% 30%,#16a34a 0,transparent 55%),radial-gradient(at 78% 25%,#65a30d 0,transparent 52%),radial-gradient(at 55% 90%,#0d9488 0,transparent 58%),linear-gradient(135deg,#15803d,#115e59);
  --zui-masked-heading-ember-mesh: radial-gradient(at 20% 70%,#dc2626 0,transparent 55%),radial-gradient(at 75% 25%,#f59e0b 0,transparent 52%),radial-gradient(at 90% 90%,#9a3412 0,transparent 58%),linear-gradient(135deg,#b91c1c,#c2410c);
  --zui-masked-heading-candy-mesh: radial-gradient(at 15% 30%,#ec4899 0,transparent 55%),radial-gradient(at 85% 25%,#6366f1 0,transparent 52%),radial-gradient(at 50% 90%,#06b6d4 0,transparent 58%),linear-gradient(135deg,#c026d3,#4f46e5);
  --zui-masked-heading-midnight-mesh: radial-gradient(at 20% 25%,#4338ca 0,transparent 55%),radial-gradient(at 80% 80%,#7e22ce 0,transparent 55%),radial-gradient(at 70% 15%,#0f766e 0,transparent 50%),linear-gradient(135deg,#1e1b4b,#312e81);
  --zui-masked-heading-mono-mesh: radial-gradient(at 20% 25%,#64748b 0,transparent 55%),radial-gradient(at 80% 70%,#0f172a 0,transparent 55%),linear-gradient(135deg,#334155,#020617);
  --zui-masked-heading-outline-width: 1px;
  --zui-masked-heading-outline: color-mix(in srgb,var(--_mh-fg,#64748b) 55%,transparent);
  --zui-masked-heading-media-bg: color-mix(in srgb,currentColor 12%,transparent);
}

/* Dark theme variables follow the same names with -dark appended. */
.dark {
  --zui-masked-heading-fg-dark: #f8fafc;
  --zui-masked-heading-font-dark: initial;
  /* ...same variables with -dark at the end */
}

Overview

Import MaskedHeading from the masked-heading entry and pass the copy as a string child. It renders an h2 by default; set level for h1–h6. Without src, the selected appearance shows a tokenized gradient mesh. Pass an image URL, or set mediaType="video" for a muted looping clip with an optional poster.

The defaults are reveal rise, trigger view, duration 1100 ms, stagger 90 ms, delay 0, parallax 24 px, drift 16 px, size lg, align start, and appearance aurora. The view trigger starts once a quarter of the heading is visible, and onRevealComplete fires when the entrance ends.

The letters are measured after layout and re-measured when the heading resizes or web fonts load. Consumers using Tailwind v4 must include the package in their @source scan so both light and dark token classes are generated.

Common use cases

  • Landing page and campaign hero headlines.
  • Section openers in editorial and portfolio layouts.
  • Product launch pages that pair a headline with brand imagery.

FAQ

Is the heading still readable by screen readers and search engines?

Yes. The copy is ordinary text inside a real h1–h6 element. The media and the SVG mask are aria-hidden, and the mask's letters are written in the browser, so server HTML contains the heading text once.

Does it need GSAP, Framer Motion, canvas or WebGL?

No. It uses an SVG clipPath, CSS transforms, and one requestAnimationFrame loop that pauses off-screen and stops once the media is still.

What happens with reduced motion, touch, or no JavaScript?

Reduced motion shows the final heading at once, keeps the media still, and never plays video. Touch input does not move the media. Without scripts, or with forced colors, the heading renders as plain text.