---
name: typographic-clipping-mask
description: Use when you want "Editorial, artistic, premium" - Uses large text as a mask for images, video, or gradients.
---

# Typographic Clipping Mask

> **Category:** Text  -  **Personality:** Editorial, artistic, premium
>
> **Best use:** Magazines, portfolios, campaigns
>
> **Ratings:** Professional 4/5 - Casual 3/5 - Exotic 5/5 - Visual impact 5/5 - Difficulty 4/5 - Performance cost 3/5

## What it is

A giant headline becomes a **window**: the letterforms are clipped from a photo, gradient, or video sitting behind them, so the picture only shows *inside* the type. You get the punch of a poster headline plus the richness of an image in one element. Use it for magazine covers, editorial heroes, portfolio splash sections and campaign titles — anywhere a single bold word should carry the art direction. The whole effect is two CSS lines (`background-clip: text` + a transparent fill); everything else is styling, an optional slow pan, and a fallback.

## Dependencies / CDN

**None - pure CSS** for the effect itself; the pan + media-swap are **vanilla JS** (no library). The demo loads three Google Fonts (optional — swap for your own):

```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=Big+Shoulders+Display:wght@700;900&family=Bodoni+Moda:ital@1&family=Familjen+Grotesk:wght@400;600&display=swap" rel="stylesheet">
```

## HTML

A wrapper holds the clipped headline plus an optional outlined "ghost" copy behind it (keeps the word legible where the image goes dark). Put both lines inside **one** element so a single continuous image spans them. A segmented control swaps what shows through.

```html
<div class="tcm-wordwrap">
  <span class="tcm-ghost" aria-hidden="true">Wild<br>North</span>
  <h2 class="tcm-word" id="tcmWord">Wild<br>North</h2>
</div>

<div class="tcm-seg" role="group" aria-label="Choose the media clipped inside the headline">
  <button type="button" data-fill="mountain" aria-pressed="true">Mountain</button>
  <button type="button" data-fill="coast"    aria-pressed="false">Coast</button>
  <button type="button" data-fill="gradient" aria-pressed="false">Gradient</button>
</div>
```

## CSS

```css
.tcm-wordwrap{position:relative;display:inline-block;max-width:100%}

/* shared metrics so the ghost lines up exactly under the clipped word */
.tcm-word,.tcm-ghost{
  font-family:'Big Shoulders Display',sans-serif;font-weight:900;line-height:.82;
  text-transform:uppercase;letter-spacing:.004em;margin:0;
  font-size:clamp(64px,18.5vw,228px);
}

/* outlined ghost copy sits behind the clipped word */
.tcm-ghost{
  position:absolute;inset:0;z-index:0;display:block;color:transparent;
  pointer-events:none;user-select:none;
  -webkit-text-stroke:1px rgba(236,230,218,.14);text-stroke:1px rgba(236,230,218,.14);
}

/* ---- THE EFFECT ---- */
.tcm-word{
  position:relative;z-index:1;
  color:#d8c7a4;                              /* FALLBACK: visible solid colour if clip:text is unsupported */
  background-image:url("/path/to/landscape-mountain.jpg");
  background-repeat:no-repeat;
  background-size:auto 200%;                  /* >100% so there's room to pan */
  background-position:50% 45%;
}
/* only flip the text transparent where clip-to-text actually works */
@supports ((-webkit-background-clip:text) or (background-clip:text)){
  .tcm-word{
    color:transparent;
    -webkit-text-fill-color:transparent;      /* needed because -webkit-text-fill-color wins over color */
    -webkit-background-clip:text;
            background-clip:text;
  }
}

/* swap the masked media by changing only background-image */
.tcm-word.is-coast{background-image:url("/path/to/landscape-coast.jpg")}
.tcm-word.is-gradient{
  background-image:linear-gradient(118deg,#e0623f 4%,#f2b14b 44%,#6f9fb5 100%);
  background-size:220% 220%;
}
```

## JavaScript

Optional. Swaps the clipped media and drifts the crop ("Ken Burns") inside the letters with a touch of pointer parallax — all gated by `prefers-reduced-motion`.

```js
var stage=document.getElementById('tcmStage'),
    word =document.getElementById('tcmWord');

// 1) segmented control -> choose what shows through the type
stage.querySelector('.tcm-seg').addEventListener('click',function(e){
  var btn=e.target.closest('button[data-fill]'); if(!btn) return;
  word.classList.toggle('is-coast',    btn.dataset.fill==='coast');
  word.classList.toggle('is-gradient', btn.dataset.fill==='gradient');
  stage.querySelectorAll('.tcm-seg button').forEach(function(b){
    b.setAttribute('aria-pressed', b===btn ? 'true' : 'false');
  });
});

// 2) slow pan + pointer parallax (skipped for reduced motion)
if(!matchMedia('(prefers-reduced-motion: reduce)').matches){
  var offX=0,offY=0,tgX=0,tgY=0;
  stage.addEventListener('pointermove',function(e){
    var r=stage.getBoundingClientRect();
    tgX=((e.clientX-r.left)/r.width -.5)*-8;
    tgY=((e.clientY-r.top )/r.height-.5)*-6;
  });
  stage.addEventListener('pointerleave',function(){tgX=tgY=0;});
  (function frame(now){
    var t=now/1000, px=50+Math.sin(t*0.17)*15, py=46+Math.cos(t*0.13)*6;
    offX+=(tgX-offX)*0.06; offY+=(tgY-offY)*0.06;
    word.style.backgroundPosition=(px+offX).toFixed(2)+'% '+(py+offY).toFixed(2)+'%';
    requestAnimationFrame(frame);
  })();
}
```

## How it works

- **`background-clip: text`** tells the browser to paint the element's background only where the glyphs are; with the text colour set to `transparent`, the letters become holes that reveal the background image/gradient.
- Both lines live in **one** element, so the *same* background spans them — the photo reads as one continuous image flowing across "WILD" and "NORTH" rather than restarting per line.
- **`background-size` is intentionally larger than 100%** (`auto 200%`) so there is spare image around the type; the JS then nudges `background-position` to slowly pan that crop inside the letters.
- The **ghost** is the same word with a faint `-webkit-text-stroke` and no fill, layered behind. It keeps the headline shape readable over dark patches of the photo and adds editorial depth.

## Customization

- **Bolder = more image.** Heavier, more condensed display faces (here Big Shoulders Display 900) expose far more of the picture than a thin serif. Crank `font-weight` and shrink `letter-spacing` for maximum reveal.
- **Mask anything:** point `background-image` at a photo, a `linear/conic-gradient`, or even an animated `<video>` placed behind a `background-clip:text` overlay.
- **Crop:** tune `background-size` / `background-position` to frame the best part of the image inside the word.
- **Pan feel:** change the `Math.sin/​cos` frequencies and amplitudes (`*15`, `*6`) for faster/slower or wider drift; set amplitude to `0` for a static crop.
- **Edge treatment:** raise the ghost stroke alpha for a crisper outline, or drop the ghost entirely for a cleaner cut.

## Accessibility & performance

- Keep the text as **real, selectable text** in the DOM (an `<h2>` here) so screen readers and SEO get the words; the image is decorative. Mark the ghost `aria-hidden="true"`.
- **Contrast:** a photo can leave letters illegible over busy/low-contrast areas — the ghost outline, a darker `background-blend`/overlay, or a `text-shadow` keeps the word readable.
- The pan animates only `background-position` (cheap, compositor-friendly) on a single element; honour `prefers-reduced-motion` by skipping the rAF loop (the CSS leaves a static crop).
- Large `background-size` on a giant clipped element costs some GPU memory — use a reasonably sized image, not an 8K original.

## Gotchas

- **Always ship the `-webkit-` prefix** (`-webkit-background-clip:text`). Chrome, Safari and friends still require it; without it the property is dropped and your text turns fully transparent.
- **`-webkit-text-fill-color` beats `color`.** If you only set `color:transparent` but a parent sets `-webkit-text-fill-color`, the fill wins and nothing clips — set the fill to `transparent` too.
- **Fallback is mandatory.** Declare a real `color` first, then flip to transparent *inside* `@supports (... background-clip:text)`. Otherwise a non-supporting engine renders invisible text.
- **One image across lines** needs both lines in the *same* clipped element; separate elements each restart the background and the photo won't line up.
- **`background-size` must exceed 100%** (or the image must be larger than the text box) or there's no room to pan — the crop will just sit still.
- Setting `background-position` inline from JS overrides any CSS keyframe pan, so drive the motion from *one* place (here, JS) to avoid the two fighting.
