CSS
    layoutfrontendanimation

    clip-path and mask: Advanced Shape Techniques for the Modern Web

    Both properties hide part of an element without any extra markup, but clip-path is strictly binary while mask is a full grayscale operation. Here's which one fits which visual effect.

    Editor: Paul RadfordJun 10, 20267 min read

    Paul Radford — Full-Stack Developer & Editor. Paul is a full-stack developer and editor of template.tips. Articles here are AI-drafted and reviewed by Paul, who knows the code well enough to catch what's wrong and cut the hype.

    clip-path and mask: Advanced Shape Techniques for the Modern Web

    clip-path cuts an element's rendered content to a geometric shape or SVG path, hiding everything outside it, while mask uses the luminance or alpha channel of an image, gradient, or SVG to control opacity pixel by pixel. Use clip-path for hard-edged shapes and mask when you need soft transitions, gradients, or photographic cutouts.

    Why these two properties get confused

    Both properties hide part of an element without touching the DOM or requiring extra markup. That's where the similarity ends. clip-path is binary: a pixel is either inside the shape (visible) or outside it (gone). There's no partial visibility, no feathering, no anti-aliased soft edge beyond whatever the browser does for the boundary itself.

    mask is a grayscale (or alpha) operation. Every pixel gets an opacity value derived from the mask source. White areas of a luminance mask show the element at full opacity, black areas hide it completely, and gray values produce partial transparency. This is why you reach for mask when you want a photo to fade into transparency at its edges, or a gradient-based reveal effect, and why clip-path alone can't do that no matter how you shape the path.

    If you only remember one distinction: clip-path decides what's there or not there. mask decides how visible something is.

    Basic shapes with clip-path

    The simplest use case is decorative cropping without image editing software.

    css
    1/* Diamond-shaped avatar using polygon() */
    2.avatar-diamond {
    3 width: 200px;
    4 height: 200px;
    5 object-fit: cover;
    6 /* Four points defining a rotated square */
    7 clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
    8}
    9
    10/* Rounded hexagon badge */
    11.badge-hex {
    12 width: 160px;
    13 height: 160px;
    14 clip-path: polygon(
    15 25% 5%, 75% 5%, 100% 50%,
    16 75% 95%, 25% 95%, 0% 50%
    17 );
    18 transition: clip-path 0.3s ease;
    19}
    20
    21.badge-hex: hover {
    22 /* Slightly expand the shape on hover for a subtle pop effect */
    23 clip-path: polygon(
    24 20% 0%, 80% 0%, 100% 50%,
    25 80% 100%, 20% 100%, 0% 50%
    26 );
    27}

    polygon() coordinates are percentages of the element's own box by default, which makes shapes responsive without recalculation. The circle(), ellipse(), and inset() functions cover the common cases; reach for polygon() only when the shape genuinely needs more than four control points, because hand-tuning polygon coordinates is tedious and fragile against content changes.

    How do you animate a clip-path shape smoothly?

    Animating between two polygon() values only works cleanly if both shapes have the same number of points listed in a comparable order. Browsers interpolate point-by-point, not by shape recognition, so going from a 4-point square to a 6-point hexagon produces a jarring snap rather than a smooth morph unless you pad the simpler shape with duplicate points at the same position.

    css
    1.shape-morph {
    2 width: 300px;
    3 height: 300px;
    4 background: linear-gradient(135deg, #6366f1, #ec4899);
    5 /* Six points, even though it renders as a rectangle (two points overlap) */
    6 clip-path: polygon(
    7 0% 0%, 100% 0%, 100% 0%,
    8 100% 100%, 0% 100%, 0% 100%
    9 );
    10 transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1);
    11}
    12
    13.shape-morph: hover {
    14 /* Same six-point structure, now forming a proper hexagon */
    15 clip-path: polygon(
    16 50% 0%, 100% 25%, 100% 75%,
    17 50% 100%, 0% 75%, 0% 25%
    18 );
    19}

    Also note that clip-path transitions are not interruptible in older engines the way transform is; if a user hovers rapidly the animation can look stuttery on lower-powered devices. Test on actual mid-range Android hardware, not just your dev machine, before shipping a shape-morph as a primary interaction.

    Using SVG paths for shapes CSS can't express

    polygon() handles straight edges. For curves (blob shapes, organic cutouts, brand logos) you need an SVG <clipPath> referenced from CSS, or the path() function with an SVG path string directly in CSS.

    css
    1.blob-clip {
    2 width: 400px;
    3 height: 400px;
    4 object-fit: cover;
    5 /* path() takes a standard SVG path data string */
    6 clip-path: path(
    7 "M200,20 C300,20 380,100 380,200 C380,300 300,380 200,380 " +
    8 "C100,380 20,300 20,200 C20,100 100,20 200,20 Z"
    9 );
    10}

    In practice, hand-writing that path data is unpleasant. The realistic workflow is: design the shape in Figma or Illustrator, export as SVG, and either paste the d attribute into path() or reference the SVG's <clipPath id="..."> element via clip-path: url(#my-shape). The url() approach is more maintainable for complex shapes because you edit the SVG file directly rather than a minified path string embedded in CSS.

    When mask outperforms clip-path

    Fading a hero image into the background color, without a visible hard edge, is a mask job.

    css
    1.hero-image {
    2 width: 100%;
    3 height: 60vh;
    4 object-fit: cover;
    5 /* Gradient mask: opaque in the center, transparent at top and bottom */
    6 mask-image: linear-gradient(
    7 to bottom,
    8 transparent 0%,
    9 black 15%,
    10 black 85%,
    11 transparent 100%
    12 );
    13 /* WebKit still requires the prefixed property as of early 2026 */
    14 -webkit-mask-image: linear-gradient(
    15 to bottom,
    16 transparent 0%,
    17 black 15%,
    18 black 85%,
    19 transparent 100%
    20 );
    21}

    You can also mask with an actual image file (a torn-paper texture, a splatter PNG, a noise pattern) using mask-image: url("texture.png"), controlling how the alpha or luminance channel of that PNG determines opacity with mask-mode. This is the technique behind a lot of "grunge" or "torn edge" section dividers you see on agency and portfolio sites, and it beats trying to build the same effect with a dozen clipped polygon layers.

    A real caveat worth knowing before you ship

    Safari's mask support has a long and inconsistent history. Even in versions released as late as Safari 15 (2021 to 2022), mask-composite and multi-layer mask-image values behaved differently from Chromium, and the unprefixed mask shorthand only became reliable in Safari without the -webkit- prefix starting around Safari 15.4. If you support Safari versions older than that, or you're not sure what your analytics say about your user base, ship both the prefixed and unprefixed declarations, in that order, so the prefixed one is overridden when the browser understands the standard property. Check current support on caniuse before assuming parity, because mask feature support is more fragmented across browsers than clip-path support, which has been solid in all major engines since around 2017.

    Another gotcha: clip-path and mask both create a new stacking context and, critically, both clip the element to their shape for hit-testing purposes in most browsers, meaning clicks outside the visible shape but inside the original box won't register on the element. That's usually what you want for a clipped button, but it will break naive event-delegation code that assumes the full bounding box is interactive.

    Performance: what actually costs you frames

    Static shapes cost effectively nothing, they're computed once at layout and composited normally. The expensive case is animating clip-path with polygon() values that have many points, or animating mask-image gradients, on large elements (full-viewport hero sections, for instance) on mid-range mobile hardware. Both properties are GPU-accelerated in Chromium and Firefox for the common cases, but complex SVG-path clipping or filter-mask combinations can fall back to software rendering in some browser and OS combinations, which you will only notice as dropped frames during scroll or animation.

    If you're animating a shape on scroll (a common pattern for "reveal" sections), profile with the browser's performance panel and specifically watch for "Paint" and "Composite Layers" entries ballooning during the scroll. If you see that, simplify the polygon point count, reduce the masked element's size, or switch the animation to clip-path: inset() (which is cheap to compute) instead of polygon() or path().

    Choosing between the two for your actual use case

    Ask what the edge needs to look like. Hard, geometric, decorative crop: clip-path. Soft fade, gradient reveal, or texture-based transparency: mask. Need to animate a shape morph as a primary UI interaction: clip-path with matched point counts, tested on real devices. Need a photographic vignette or an image that dissolves into the page background: mask-image with a gradient, full stop.

    Before committing to either in production, pull up the MDN clip-path reference and the MDN mask reference side by side, check the current browser compatibility tables (they're updated more often than most blog posts claiming to cover this), and prototype the specific shape or fade you need in an isolated CodePen against your actual target browser list rather than assuming either property behaves identically everywhere. The CSS Masking spec is also worth skimming if you're building something that composites multiple mask layers, since the interaction between mask-composite values is one of the areas where spec text and implementation still diverge slightly across engines.

    Related Articles