---
name: text-reveal-mask
description: Use when you want "Elegant, editorial, premium" - Reveals text through clipping masks, often from bottom to top.
---

# Text Reveal Mask

> **Category:** Text  -  **Personality:** Elegant, editorial, premium
>
> **Best use:** Headlines, case studies
>
> **Ratings:** Professional 5/5 - Casual 3/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 3/5 - Performance cost 2/5

## What it is

A headline that **rises into view from behind a mask**, one line at a time. Each line of type sits inside its own `overflow:hidden` clip; the text itself starts pushed down (`translateY(100%)`, hidden below the clip) and slides up to `translateY(0)`, so it appears to emerge from a hard bottom edge. Staggering the lines a few hundredths of a second apart makes the headline assemble itself in reading order. It is the quiet, expensive-looking move you see on fashion, architecture and case-study covers — reach for it on a hero headline you want people to *watch* arrive.

It works on whole **lines** (each line moves as one solid block). That is what keeps it distinct from "Split Text", which animates every letter independently.

## Dependencies / CDN

None - pure CSS for the effect; a few lines of vanilla JS only to trigger it on scroll-in and to power "Replay". The demo loads display + label fonts (optional):

```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=Bodoni+Moda:ital,opsz,wght@0,6..96,400..700;1,6..96,500..600&family=Space+Mono:ital,wght@0,400;0,700" rel="stylesheet">
```

## HTML

Wrap **each line** in a clip (`.line`) with an inner element that actually moves. Give every inner element a per-line `--d` (its stagger delay):

```html
<h2 class="reveal-head">
  <span class="line"><span style="--d:.18s">A house that</span></span>
  <span class="line"><span style="--d:.29s">follows</span></span>
  <span class="line"><span style="--d:.40s">the <em>afternoon</em></span></span>
  <span class="line"><span style="--d:.51s">sun.</span></span>
</h2>

<!-- no-JS / safety fallback: show the text if the script never runs -->
<noscript><style>.line>span{transform:none!important}</style></noscript>
```

## CSS

```css
/* the mask: clip the line, then push its text below the clip */
.reveal-head{ font:600 clamp(32px,8.2vw,92px)/1.03 "Bodoni Moda",Georgia,serif; margin:0; }
.line{ display:block; overflow:hidden; padding-bottom:.1em; margin-bottom:-.02em; }
.line > span{ display:block; white-space:nowrap; transform:translateY(122%); will-change:transform; }
.reveal-head em{ font-style:italic; color:#8a2b3a; }   /* an accent word, still part of its line */

/* the reveal: text slides up; --d staggers each line (set inline, reading-order) */
.is-revealed .line > span{
  transform:translateY(0);
  transition:transform 1s cubic-bezier(.19,1,.22,1);
  transition-delay:var(--d, 0s);
}

/* instant for users who ask for less motion */
@media (prefers-reduced-motion:reduce){
  .line > span{ transform:none !important; }
}
```

> Note: `translateY(122%)` (not 100%) hides the line fully — the extra `padding-bottom` that protects descenders also enlarges the clip, so the text must travel a little further to stay tucked away.

## JavaScript

Not required for the animation itself, but recommended to (a) trigger it when the headline scrolls into view, and (b) drive a "Replay" button. The replay trick: freeze transitions, snap back to the hidden state, then re-enable and re-add the class so it animates forward again (never backwards).

```js
var head = document.querySelector('.reveal-head');
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;

function play(){
  head.classList.add('no-trans');        // .no-trans *{transition:none!important}
  head.classList.remove('is-revealed');  // snap to hidden
  void head.offsetWidth;                 // force reflow
  head.classList.remove('no-trans');     // re-enable transitions
  void head.offsetWidth;                 // force reflow
  head.classList.add('is-revealed');     // animate in
}

if (reduce) { head.classList.add('is-revealed'); }      // instant, no motion
else {
  new IntersectionObserver(function(entries, obs){
    entries.forEach(function(e){ if (e.isIntersecting){ play(); obs.disconnect(); } });
  }, { threshold:.35 }).observe(head);
}
document.querySelector('.replay').addEventListener('click', play);
```

For the cleanest result, gate the first `play()` behind `document.fonts.ready` (with a short timeout fallback) so the letters rise in their real display font rather than the fallback.

## How it works

- **Clip + push-down.** Each `.line` is `overflow:hidden`; its inner `<span>` is shoved down with `translateY(122%)`, so it sits *below* the clip and is invisible.
- **Slide to zero.** Adding `.is-revealed` transitions the inner span back to `translateY(0)`. Because the parent clips, the type looks like it is rising out from behind a solid bottom edge — a moving mask.
- **Stagger = rhythm.** A per-line `transition-delay` (`--d`) releases the lines in sequence so the headline builds top-to-bottom while each line rises bottom-to-top.
- **`transform` only.** Animating `transform` (not `top`/`margin`) keeps the whole thing on the GPU compositor — cheap and butter-smooth.

## Customization

- **Direction:** flip to `translateY(-122%)` for a drop-down-from-above reveal; reverse the `--d` order (bottom line smallest delay) to assemble the headline upward.
- **Pace & feel:** longer `transition` (1.1–1.4s) + a softer ease reads more luxurious; tighten the `--d` step (~.06s) for a snappier cascade.
- **Mask edge:** add a 1px rule or a thin gradient under each `.line` to make the "edge" the text rises from explicit.
- **Accent:** italicise one word inside a line (as the demo does with *afternoon*) for editorial emphasis — it still travels as part of that line, so the line-mask reading is preserved.
- **Same trick, other media:** the demo reuses it on the feature image, sliding a paper-coloured `.veil` upward to uncover the photo.

## Accessibility & performance

- **Honour `prefers-reduced-motion`** — show the final text instantly (the demo also benefits from the catalog's global reduced-motion rule that collapses transition durations).
- **No-JS fallback:** the markup is real, selectable text; a `<noscript>` rule resets the transform so the headline is fully visible if the script never runs.
- **Cheap:** transforms + opacity only, no layout thrash, no library. A handful of lines costs essentially nothing; trigger via `IntersectionObserver` so off-screen headlines don't animate until seen.
- Screen readers read the headline normally — `transform` does not remove it from the accessibility tree, and the lines are in DOM order.

## Gotchas

- **Descenders get clipped.** `overflow:hidden` cuts letters like *g, y, p*. Add `padding-bottom` (e.g. `.1em`) to the clip and a matching negative `margin-bottom` to keep leading tight — and push the text a bit further than 100% (the demo uses `122%`) so it still hides.
- **Lines must fit on one line.** `white-space:nowrap` keeps each line solid; if a line is wider than its container it gets clipped horizontally and silently vanishes. Size the type responsively (the demo uses `clamp()`) or shorten the copy.
- **Replaying naively runs the animation backwards.** Removing `.is-revealed` while transitions are live makes the text slide back down. Disable transitions for one reflow before re-arming (the `no-trans` step above).
- **FOUC of the wrong font.** If the reveal fires before the display font loads, the letters rise in the fallback face then snap — wait on `document.fonts.ready` (with a timeout) before the first play.
- **It's per-line, not per-letter.** If you want individual characters, that's the "Split Text" effect; mixing the two on one headline usually looks busy.
</content>
</invoke>
