Design reference

Masks — every way to hide part of a thing, in one place.

A mask is how a design shows only part of a thing. A fade instead of a hard edge. A photo poured into a shape. Color pouring through letters. There are two kinds — masks (mask-*, per-pixel alpha, so they can fade) and clips (clip-path, binary, so edges stay crisp) — plus SVG's own primitives underneath both. Every specimen below is the real CSS, rendered live from the site's own tokens, so it re-resolves when you flip the theme toggle. Three are already shipping on this site and are tagged In use. It's the sibling of backgrounds, the section gallery, hero sections, animations, icons, and the image gallery.

  1. 01 Gradient masks (soft fades) 4 specimens
  2. 02 Image & shape masks 4 specimens
  3. 03 Compositing several masks 3 specimens
  4. 04 Clip paths (hard edges) 5 specimens
  5. 05 SVG-native masking 3 specimens
  6. 06 Text, blend & utility masks 4 specimens

Mask or clip?

mask-image

Reads the alpha channel of a blend, picture, or SVG and multiplies it into the thing. Solid shows, see-through hides, and everything in between is a part fade. Use it whenever the edge should be soft.

clip-path

Cuts along a drawn outline. Each dot is either all in or all out. No soft edge, but crisp, it can move, and the cut-away part stops taking clicks too. Use it for shapes and reveals.

The prefix rule

Always write -webkit-mask-* and the standard one, in that order. Chrome, Edge, and Safari still take the older form for several mask settings. Firefox reads only the standard one. background-clip: text needs the same pair — the site writes it that way in styles.css. clip-path does not: the one place the site ships it (.carousel-status) is unprefixed, and so is the “In use” specimen below. The clip-path technique swatches further down still write the -webkit- pair as belt-and-braces for very old Safari; that is a demo choice, not a rule this site follows.

The token rule

This site's mask stops use --lead-site-mask-opaque, not black. In the default mask-mode: alpha the hue is irrelevant — only opacity is read — so the token documents intent ("this stop is fully opaque") instead of implying a colour.

01

Gradient masks (soft fades)

mask-image: linear-gradient() · radial-gradient() · multiple mask layers

The one we lean on most. A color blend costs nothing to draw, needs no file, and is the only way to end a thing in a fade rather than a hard line. This is the kind the live site already uses.

Linear edge fade mask-image: linear-gradient(to right, …) transparent → opaque 18% → opaque 82% → transparent The simplest mask there is. A band that melts into the page instead of stopping dead. Turn that same one line around and you get the "there is more below" fade.
Radial spotlight In use .hero-band::before · styles.css radial-gradient(circle at top right, --lead-site-mask-opaque 0%, transparent 62%) · home + hero templates The site's main mask. A dot grid is painted over the whole top block, then a round mask keeps it only near the top right corner. That way it reads as texture, not wallpaper. .hero-canvas reuses the same shape at 70% so the 3D backdrop fades on exactly the same curve.

Scroll this box.

The top and bottom edges stay soft as content passes under them, because a two-stop vertical mask sits on the scroll container itself.

No overlay gradient div, no matching the background colour, and it works over any backdrop — including the theme you are not currently looking at.

That last point is the whole argument: an overlay fade is a lie painted in the page colour, and it breaks the moment the surface behind it changes.

A mask removes pixels instead of covering them.

Scroll-container fade mask-image: linear-gradient(to bottom, …) on the scroller transparent → opaque 12% → opaque 88% → transparent · overflow-y: auto This takes the place of the old fade-box trick for long lists and scrolling panels. It takes away see-through-ness instead of painting a color, so it still works when you flip the theme or change what is behind it.

A fade mask is also the honest way to do a "read more" cut. The words are not chopped off, just faded. So you can still select them, screen readers still find them, and the browser's own find-in-page.

Compare that with clipping the block to a fixed height, which deletes the overflow outright and hands the user nothing to reveal.

Text truncation fade mask-image: linear-gradient(to bottom, opaque 55%, transparent) Pairs with a max-height and a "read more" control A shut block that fades instead of cropping. One catch. Faded words are still read out by screen readers. So the open button must be a real button, and the shut state must not be the only place the content exists.
02

Image & shape masks

mask-image: url() · mask-size · mask-repeat · mask-mode

For when the shape is not a color blend. Any picture with see-through parts can be the stencil. An SVG, a built-in image, or a PNG. The mask settings size it, tile it, and place it just like a background image.

Inline SVG shape mask mask-image: url("data:image/svg+xml,…") mask-size: contain · mask-repeat: no-repeat · mask-position: center Any drawn outline as the stencil, written right into the style sheet so nothing extra has to load. This is how a photo or a color blend gets poured into a brand shape. And unlike clip-path: polygon(), curves are cheap.
Icon-as-stencil (recolouring) mask-image: url(…) + background: currentColor The standard technique for tinting a monochrome SVG from CSS Mask an empty element with an icon and fill it with background: currentColor and the icon inherits text colour — no fill attribute, no inlining, no per-theme asset. The trade-off: it is a shape, not a document, so it can carry no <title> and must be decorative.
Tiled mask (halftone) mask-image: url("/assets/hero-pattern.svg") mask-size: 1.75rem · mask-repeat: repeat · the site's real dot-grid asset The same asset the hero paints as a background, used here as the mask: the fill shows only through the dots. Note the dots are 45% opaque in the source file, so the tile renders at 45% — mask alpha passes straight through.
alpha
luminance
mask-mode: alpha vs luminance mask-mode: alpha | luminance | match-source Same white→black gradient, two readings CSS defaults to alpha (opacity is the mask); SVG's <mask> defaults to luminance (here brightness is the mask, so white shows and black hides). Both boxes use the very same blend. Only the mode differs. Getting this backwards is the most common "my mask does nothing" bug.
03

Compositing several masks

mask-composite: add | subtract | intersect | exclude

mask-image takes a comma-separated list, and mask-composite decides how the layers mix. It is the same as add, subtract, and intersect in a drawing tool. This is where masks stop being just a fade and start being a build kit.

Knockout hole (exclude) mask-composite: exclude full-cover layer XOR a radial circle Two layers, keeping only what does not overlap. That punches a clean hole through the thing. It is the base for spotlight overlays, ring edges, and cut-out cards. And the hole is really see-through, not just page-coloured patch on top.
Vignette (intersect) mask-composite: intersect radial vignette ∩ horizontal fade Only where both masks are opaque survives. Stacking a soft shape against a directional fade gives a photographic vignette that no single gradient can express.
Notched shape (subtract) mask-composite: subtract base layer minus a positioned circle Order matters here: subtract removes the layers beneath from the top one. The classic use is a ticket stub, a notch where an avatar overlaps a card, or a badge cut-out on a thumbnail.

Support note. mask-composite is the standard property and is supported across current Chrome, Edge, Safari, and Firefox. Older WebKit shipped a different -webkit-mask-composite with different keywords (source-out, xor), so pair the two only deliberately — and treat any composite effect as an enhancement that degrades to the un-composited first layer.

04

Clip paths (hard edges)

clip-path: inset() · circle() · ellipse() · polygon() · path() · shape() · url()

Binary geometry: in or out, nothing in between. What you give up in softness you get back in animatability — clip-path interpolates between shapes with the same number of points, which makes it the cleanest way to build a reveal.

inset() clip-path: inset(12% 8% round 1rem) Rectangular crop, optional corner radius A rectangle pulled in from each edge, with an optional round radius. The most useful member of the family precisely because it is the least exciting — and the one that animates most predictably for wipe reveals.
circle()
ellipse()
circle() & ellipse() clip-path: circle(46% at 50% 50%) Radius keywords closest-side / farthest-side supported Round crops that beat border-radius: 50% in one way that matters: the clipped area stops receiving pointer events, so the corners of a circular avatar are genuinely not clickable.
polygon() clip-path: polygon(0 0, 100% 0, 92% 100%, 8% 100%) Any number of straight-line vertices, percentages or lengths Slanted dividers, arrows, ribbons, and torn-edge bands. Two shapes with the same number of points can slide into each other. That is how shifting reveals get built with no extra code.
path() — and the newer shape() clip-path: path("M0,40 C…") SVG path syntax · absolute pixel coordinates only Curves, at last — but path() takes raw SVG path data in fixed pixels, so it does not respond to element size. The newer shape() function fixes exactly that with percentage and calc() coordinates; it is well supported in current browsers but not yet universal, so ship it behind @supports (clip-path: shape(from 0% 0%, line to 100% 0%)) with path() or polygon() underneath.
url(#id) — referenced SVG clip path clip-path: url(#msk-arrow-clip) clipPathUnits="objectBoundingBox" · 0–1 coordinate space The escape hatch for shapes CSS cannot spell. Set clipPathUnits="objectBoundingBox" and author the path in a 0–1 space and the clip scales with the element — the responsive answer path() never had. Costs an inline <svg> in the document.
05

SVG-native masking

<mask> · <clipPath> · mask-type · text as a stencil

Everything above is CSS reaching into SVG's model. Working inside SVG directly gets you one thing CSS still cannot do: text as a mask shape, with real font rendering.

<mask> (luminance) mask="url(#id)" on the shape White reveals, black hides · override with mask-type="alpha" SVG's own mask element, and the reason mask-mode exists in CSS: here brightness is the mask, so the white end shows and the black end vanishes. Anything paintable can go inside a <mask> — gradients, shapes, even other images.
<clipPath> (multi-shape) clip-path="url(#id)" on the shape Several children union together into one clip region A clip path can hold more than one shape, and they add up — a union with no compositing property required. The same <clipPath> can be pointed at from CSS on an HTML element, so one definition serves both worlds.
CARAVAN
Text as a stencil <text fill="var(--lead-mask-reveal)"> inside a <mask> Real glyphs, real kerning — not an outlined path Letters as the mask shape, with the color, a photo, or a video showing through them. There is a real cost here. Screen readers do not read SVG text inside a mask, so the whole picture needs role="img" and an aria-label carrying the words — as this one does.
06

Text, blend & utility masks

background-clip: text · mix-blend-mode · overflow: hidden · clip-path: inset(50%)

Four techniques that are masking in effect if not in name — including the two the site already relies on, and the one clip that exists purely for screen readers.

A better funnel

Gradient text In use .hero-highlight · styles.css background-clip: text + color: transparent · slow pan under prefers-reduced-motion: no-preference The letters mask a color blend. Keep two rules. The text must stay real text you can select, never a picture. And the blend must stand out from what is behind it. A pale blend on a pale background is a WCAG failure that no automated check will flag for you. The site restricts this to one key phrase per headline.

KNOCKOUT

Blend-mode knockout mix-blend-mode: screen | multiply + isolation: isolate Text punched through a filled panel by blending, not masking Blend modes let a layer subtract itself from what is behind it — knockout headlines over imagery, without a second asset. isolation: isolate on the parent is mandatory, or the blend escapes into the whole page. It is compositing, not masking, so it cannot fade partially the way mask-image can.
overflow: hidden + border-radius The everyday clip — used all over this site Cheapest possible crop · clips descendants, not the element itself Before you reach for a mask, try the plain answer first. A round box that crops what is inside it. Every card, picture frame, and swatch on these pages uses it. Where it falls short is why the rest of this page exists — rectangles and radii only, and it creates a scroll container and a new stacking context.

Visible label …plus this sentence, which is clipped to nothing but still announced.

The accessibility clip In use .carousel-status · styles.css clip-path: inset(50%) + 1px box + absolute positioning The one clip on this page that is not decorative. inset(50%) collapses the box to nothing visually while leaving it in the accessibility tree — unlike display: none or visibility: hidden, which stop it being announced at all. The site uses it for the carousel's live "Slide n of m" reading.

Not masking, despite the family resemblance. shape-outside changes how inline text wraps around a float — it never hides a pixel. -webkit-box-reflect draws a faded reflection (and is WebKit-only). Canvas globalCompositeOperation and WebGL stencil buffers do real masking, but inside a bitmap the DOM cannot see. Keep all four out of the mask/clip conversation.

·

Before you ship a mask

The five checks that separate a mask that survives review from one that gets reverted.

  1. Write both properties. -webkit-mask-* first, standard second. Firefox reads only the standard property; several WebKit sub-properties still want the prefix. Same for -webkit-background-clip: text.
  2. Never mask meaning. A mask hides dots, not content. Masked words are still read out and still found by find-on-page. If a thing must really be hidden, hide it properly. If it is just for looks, mark it role="img" with an aria-label or aria-hidden.
  3. Check both themes. Build the mask from tokens (--lead-site-mask-opaque, the accent tokens) so it re-resolves on the theme toggle. A mask hard-coded against one background is a bug waiting for the other one.
  4. Watch the cost. Masks and clips create a new rendering surface. Animating mask-position or clip-path is fine and GPU-friendly; animating mask-image itself is not — and a mask on a large, frequently-repainting element (a scrolling list, a video) is worth profiling before it ships.
  5. Respect reduced motion. Any animated reveal — a clip-path wipe, a panning gradient — belongs inside @media (prefers-reduced-motion: no-preference), with the finished state as the default. That is the rule the whole animations page is built on.