Type something to search...
CSS Masks: Advanced Image and Text Masking Techniques

CSS Masks: Advanced Image and Text Masking Techniques

clip-path cuts an element with a hard edge: a pixel is either visible or it isn't. CSS masks are the more flexible sibling. A mask is an image, and the browser uses that image's alpha (or luminance) to decide how visible each pixel of the element should be. That means soft fades, partial transparency, textured edges, and cutouts in any shape an image can describe.

Masks power a lot of polished effects you've probably seen: images that fade into the page, scroll containers with soft edges, icon systems that recolor with a single CSS variable, and grunge-textured headlines. In this guide we'll go through the mask properties one at a time and then combine them into real techniques.

The Core Idea

A mask image is laid over the element. Where the mask is opaque, the element shows. Where it's transparent, the element is hidden. Anything in between is partially visible.

.fade-out {
  mask-image: linear-gradient(to bottom, black, transparent);
}

The element fades from fully visible at the top to invisible at the bottom. The actual color of the mask doesn't matter in the default mode, only its alpha. black, red, and #fff are all fully opaque and all mean "show".

Just like backgrounds, a mask can be:

  • A gradient (linear, radial, conic, or repeating)
  • A raster image such as a PNG with transparency
  • An SVG file
  • A reference to an SVG <mask> element

The Mask Properties

Masks mirror the background properties almost one to one, which makes them easy to learn if you know backgrounds well.

PropertyWhat it controlsBackground equivalent
mask-imageThe image(s) used as the maskbackground-image
mask-sizeSize of each mask layerbackground-size
mask-positionPosition of each mask layerbackground-position
mask-repeatWhether the mask tilesbackground-repeat
mask-originThe box the mask is positioned relative tobackground-origin
mask-clipThe area the mask affectsbackground-clip
mask-modeWhether to use alpha or luminancenone
mask-compositeHow multiple mask layers combinenone
maskShorthand for all of the abovebackground

One important difference: mask-repeat defaults to repeat, just like backgrounds. If your SVG mask shows up tiled across a large element, set mask-repeat: no-repeat.


Technique 1: Fading Image Edges

The simplest and most common use: blending an image softly into the page.

<section class="hero">
  <img class="hero__image" src="/images/forest.jpg" alt="Misty pine forest" />
  <div class="hero__text">
    <h1>Into the woods</h1>
    <p>Guided hikes through the old-growth forests of the north.</p>
  </div>
</section>
.hero {
  display: grid;
  background: #0f172a;
  color: #f8fafc;
}

.hero__image,
.hero__text {
  grid-area: 1 / 1;
}

.hero__image {
  width: 100%;
  height: 70vh;
  object-fit: cover;
  mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
}

.hero__text {
  align-self: end;
  padding: 2rem;
}

Unlike a dark gradient overlay, the mask makes the image itself transparent, so whatever is behind it shows through. If your page background is a pattern or another image, it'll blend perfectly, which an overlay can't do.

Fading on Two Edges

For a vignette on the left and right, use a gradient with two transparent ends:

.carousel-image {
  mask-image: linear-gradient(
    to right,
    transparent,
    black 15%,
    black 85%,
    transparent
  );
}

And for a soft oval spotlight, use a radial gradient:

.portrait {
  mask-image: radial-gradient(ellipse at center, black 50%, transparent 72%);
}

Technique 2: Scroll Containers with Soft Edges

Horizontally scrolling rows of cards often cut off abruptly at the edges. A mask signals "there's more" by fading the content at the edges:

<div class="scroller">
  <ul class="scroller__track">
    <li class="chip">Design systems</li>
    <li class="chip">Accessibility</li>
    <li class="chip">Performance</li>
    <li class="chip">Animation</li>
    <li class="chip">Typography</li>
    <li class="chip">Layout</li>
  </ul>
</div>
.scroller {
  overflow-x: auto;
  mask-image: linear-gradient(
    to right,
    transparent,
    black 2rem,
    black calc(100% - 2rem),
    transparent
  );
}

.scroller__track {
  display: flex;
  gap: 0.75rem;
  width: max-content;
  margin: 0;
  padding: 0.5rem 2rem;
  list-style: none;
}

.chip {
  padding: 0.5rem 1rem;
  border-radius: 999px;
  background: #1f2937;
  color: #e2e8f0;
  white-space: nowrap;
}

Because the mask is applied to the scroll container, it stays fixed at the edges while the content scrolls beneath it. The padding on the track ensures the first and last items can scroll fully into the unfaded zone.


Technique 3: Recolorable Icons

This is one of the most practical uses of masks. Take a single-color SVG icon, use it as a mask, and set the element's background-color. The icon takes on whatever color you choose, including currentColor:

<button class="icon-button" type="button">
  <span class="icon icon--search" aria-hidden="true"></span>
  Search
</button>
.icon {
  display: inline-block;
  width: 1.25em;
  height: 1.25em;
  background-color: currentColor;
  mask-image: var(--icon);
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
  vertical-align: -0.2em;
}

.icon--search {
  --icon: url("/icons/search.svg");
}

.icon-button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: #38bdf8;
}

.icon-button:hover {
  color: #f472b6;
}

The icon inherits the button's text color and changes on hover automatically. This works with icons you can't inline, like files from an external set, and avoids the fragile filter chains people use to recolor images.


Technique 4: Textured and Grunge Text

A mask with a texture gives text a worn, printed, or stamped look:

.stamp-heading {
  font-size: clamp(3rem, 10vw, 7rem);
  font-weight: 900;
  text-transform: uppercase;
  color: #f472b6;
  mask-image: url("/textures/grunge.png");
  mask-size: 400px;
  mask-repeat: repeat;
}

The texture's transparent specks punch holes in the letters. Because it's a mask, the heading remains real, selectable, accessible text.

Gradient-Masked Text

You can also combine text with a gradient mask to fade a long heading or a truncated paragraph:

.excerpt {
  max-height: 9em;
  overflow: hidden;
  mask-image: linear-gradient(to bottom, black 60%, transparent);
}

That's a clean "read more" teaser that works on any background, unlike the old approach of overlaying a white gradient, which breaks on non-white backgrounds and in dark mode.


Technique 5: Luminance Masks

By default, masks from images use the alpha channel. With mask-mode: luminance, the browser uses brightness instead: white means fully visible, black means hidden, and grays are partially visible.

.photo-luminance {
  mask-image: url("/masks/brush-stroke.jpg");
  mask-mode: luminance;
  mask-size: cover;
}

This is handy when you have a black-and-white mask image (like a JPEG brush stroke) without an alpha channel. SVG <mask> elements use luminance by default, which is why a white shape inside an SVG mask means "show".


Technique 6: Combining Layers with mask-composite

Like backgrounds, masks can have multiple layers. mask-composite controls how each layer combines with the layers below it:

  • add (default): layers stack, visible where any layer is visible.
  • subtract: the current layer cuts away from the layers beneath it.
  • intersect: visible only where both overlap.
  • exclude: visible where exactly one layer is visible.

Punching a Hole

.card--notched {
  mask-image:
    linear-gradient(black, black),
    radial-gradient(circle at 100% 0, black 24px, transparent 25px);
  mask-composite: subtract;
}

Layer order matters here. With subtract, the top layer (listed first) is kept only where it falls outside the layers beneath it. The first layer covers the whole card, and the second is a small circle in the top-right corner, so the result is the full card minus a rounded notch. Swap the order and you'd get the opposite: nothing visible at all, because the circle never falls outside the rectangle.

Gradient Borders

A popular pattern combines two layers to show only a ring around the element:

.gradient-border {
  position: relative;
  padding: 1.5rem;
  border-radius: 16px;
}

.gradient-border::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  border-radius: inherit;
  background: linear-gradient(135deg, #38bdf8, #818cf8, #f472b6);
  mask:
    linear-gradient(black 0 0) content-box,
    linear-gradient(black 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

The first mask layer covers only the content box, and the second covers the whole element. exclude shows only where they don't overlap: the 2px padding ring. The result is a gradient border that respects border-radius.

Older WebKit used a different, non-standard syntax for compositing (-webkit-mask-composite: xor instead of exclude). If you support older Safari, include both.


Animating Masks

Mask positions and sizes animate just like background positions. That makes for elegant reveals:

.reveal-mask {
  mask-image: linear-gradient(to right, black 40%, transparent 60%);
  mask-size: 250% 100%;
  mask-position: 100% 0;
  mask-repeat: no-repeat;
  transition: mask-position 0.8s ease;
}

.reveal-mask.is-visible {
  mask-position: 0 0;
}

The mask is wider than the element and starts shifted so the transparent part covers everything. Sliding it reveals the element with a soft, feathered edge, which clip-path can't do.

Gradient stops themselves can't be transitioned directly, but registered custom properties with @property can:

@property --reveal {
  syntax: "<percentage>";
  initial-value: 0%;
  inherits: false;
}

.spot-reveal {
  mask-image: radial-gradient(
    circle at 50% 50%,
    black var(--reveal),
    transparent calc(var(--reveal) + 10%)
  );
  transition: --reveal 0.6s ease;
}

.spot-reveal:hover {
  --reveal: 80%;
}

Masks vs. clip-path

Both hide parts of an element, but they suit different jobs:

  • Use clip-path for hard-edged geometric shapes, pointer-event shaping, and interpolated shape morphs.
  • Use masks for soft edges, partial transparency, textures, image-based shapes, and icons.

They can also be combined. A clip path can cut the silhouette, and a mask can soften it further.

Pitfalls and Best Practices

  • Remember mask-repeat. It defaults to repeat, which surprises people using single SVG masks.
  • Cross-origin images. Mask images loaded from another domain must be served with appropriate CORS headers, or they won't apply.
  • Masks hide shadows and outlines too. Like clip-path, a mask affects the whole rendered element, including box-shadow and focus outlines. Apply them to a wrapper or a pseudo-element if you need visible focus rings.
  • Hidden isn't gone. Masked content is still in the DOM and still reachable by screen readers and keyboard focus. Don't use masks to hide interactive elements.
  • Performance. Static masks are cheap, but animating large masks every frame can cost compositing time. Test on real devices.
  • Forced colors. In high contrast modes, masked icons that rely on background-color may disappear. Consider forced-color-adjust or fallback text.

Browser Support

The unprefixed mask properties are supported in all current major browsers. For many years, Chromium and Safari only supported the -webkit- prefixed versions, so older codebases often write both:

.fade {
  -webkit-mask-image: linear-gradient(black, transparent);
  mask-image: linear-gradient(black, transparent);
}

If your audience includes older browsers, keep the prefixed line. For newer features like mask-composite keywords or mask-mode, confirm support on caniuse and test in Safari in particular.

Conclusion

CSS masks let you control visibility pixel by pixel, with gradients, images, and SVG. Use them to fade images into the page, soften scroll containers, recolor icons with currentColor, texture your headlines, and build gradient borders with mask-composite. Because masks mirror the background properties, you already know most of the syntax.

Start with a single mask-image: linear-gradient(...) on your next hero image, and you'll quickly see why masks have become one of the most useful visual tools in modern CSS.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading