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.
- 01 Gradient masks (soft fades)
4 specimens - 02 Image & shape masks
4 specimens - 03 Compositing several masks
3 specimens - 04 Clip paths (hard edges)
5 specimens - 05 SVG-native masking
3 specimens - 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.
Gradient masks (soft fades)
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.
mask-image: linear-gradient(to right, …) 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. .hero-band::before · styles.css 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.
mask-image: linear-gradient(to bottom, …) on the scroller 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.
mask-image: linear-gradient(to bottom, opaque 55%, transparent) 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. Image & shape masks
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.
mask-image: url("data:image/svg+xml,…") 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. mask-image: url(…) + background: currentColor 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. mask-image: url("/assets/hero-pattern.svg") 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 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. Compositing several masks
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.
exclude) mask-composite: exclude 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. intersect) mask-composite: intersect 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. subtract) mask-composite: subtract 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.
Clip paths (hard edges)
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) 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%) 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%) 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…") 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) 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. SVG-native masking
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 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 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. <text fill="var(--lead-mask-reveal)"> inside a <mask> 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. Text, blend & utility masks
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
.hero-highlight · styles.css 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
mix-blend-mode: screen | multiply + isolation: isolate 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 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.
.carousel-status · styles.css 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.
- 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. - 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 anaria-labeloraria-hidden. - 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. - Watch the cost. Masks and clips create a new rendering surface. Animating
mask-positionorclip-pathis fine and GPU-friendly; animatingmask-imageitself is not — and a mask on a large, frequently-repainting element (a scrolling list, a video) is worth profiling before it ships. - Respect reduced motion. Any animated reveal — a
clip-pathwipe, 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.