Mert Özen Further With Every Line
Frontend CSS Properties: A Hands-On Guide — #08

clip-path, mask and shape-outside: Clipping, Masking and Flowing Text Around Shapes

Mert Özen Nov 15, 2026 13 min 14 views
clip-path, mask and shape-outside: Clipping, Masking and Flowing Text Around Shapes

clip-path paints only the inside of a shape and also does hit testing against it, mask-image gives partial visibility through an image's alpha channel, and shape-outside flows the text around a floated element along a shape; none of them changes the box in the layout. The post measures shape() against path(), a masked icon in forced colors, and a round author photo, a cover strip with faded edges and text flowing around a tilted book in Bookstore.

Version note: This post follows the CSS Masking Module Level 1 and CSS Shapes Level 1 and Level 2 specifications and the November 2026 state of Baseline (web-features 3.46.0); the examples were run in Chromium 158 and WebKit 27.2 (Playwright 1.65), and Firefox support comes from web-features and MDN browser-compat-data 8.1.9.

Guess First

For Bookstore, the online bookstore used as the shared example across this site's series, we are adding three things to the book page: a round author photo, a "Recommended" strip that scrolls sideways, and an introduction text that follows the edge of a tilted book picture. All three are about not showing part of an element, or flowing text around a shape. First, a question:

/* 1) basic shapes */
#inset { clip-path: inset(10px round 8px); }
#circle { clip-path: circle(40px at 50% 50%); }
#polygon { clip-path: polygon(50% 0, 100% 100%, 0 100%); }
/* box-shadow is clipped too */
#shadowed { clip-path: inset(0 round 8px); box-shadow: 0 0 0 6px #b91c1c; }

We clipped 200×100 blue boxes with these shapes. If you click the clipped top left corner of a box, who gets the click: the box or the element behind it? And does the box get smaller in the layout?

The Basic Shapes of clip-path

clip-path paints only the part of an element that is inside a shape. There are four basic shapes: inset() (a rectangle pulled in from the edges, optionally with rounded corners), circle(), ellipse(), and polygon() with its points separated by commas. Percentages refer to the element's box. The measurement in the lab:

node scripts/08-clip-mask.mjs
inset    box 200x100  corner pixel rgb(255, 255, 255)  click at corner hits SECTION, at center hits inset
circle   box 200x100  corner pixel rgb(255, 255, 255)  click at corner hits SECTION, at center hits circle
polygon  box 200x100  corner pixel rgb(255, 255, 255)  click at corner hits SECTION, at center hits polygon
shadowed 3px outside the box: rgb(255, 255, 255)

All three boxes stay 200×100: clip-path does not change layout, and the clipped part keeps taking up space. The corner pixels are white, so the clipped part is not painted. The answer to the opening question is here as well: a click on the corner goes to the section behind the box, a click in the middle goes to the box. The browser does hit testing against the clipped shape; clicking the corner of a round button does not trigger the button. Both engines give the same result.

The last line shows a side effect: clip-path also cuts off the element's own box-shadow and outline, because they are part of what the element paints. The red shadow that should appear 3px outside the box is not there. To give a clipped shape a shadow, put a filter: drop-shadow() on a parent element; as we saw in the previous post, drop-shadow follows the pixels of the shape inside.

path() and shape(): Fixed Pixels or Percentages?

For curves that basic shapes cannot express there are two functions. path() takes SVG path syntax and has worked in clip-path for years. shape() became Baseline in 2026 and lets you write the same commands in CSS syntax, with percentages and calc(). I wrote a shape with a wavy bottom edge in both and measured it at two widths:

/* 2) path() uses fixed pixels, shape() can use percentages */
.wave { display: block; inline-size: 200px; block-size: 100px; margin-block-end: 8px; background: #15803d; }
.wave.wide { inline-size: 400px; }
.wave.path { clip-path: path("M 0 0 H 200 V 70 Q 100 100 0 70 Z"); }
.wave.shape { clip-path: shape(from 0 0, hline to 100%, vline to 70%, curve to 0 70% with 50% 100%, close); }
--- chromium
.wave.path       width 200 (150, 40): rgb(21, 128, 61)  (100, 80): rgb(21, 128, 61)
.wave.path.wide  width 400 (300, 40): rgb(255, 255, 255)  (200, 80): rgb(255, 255, 255)
.wave.shape      width 200 (150, 40): rgb(21, 128, 61)  (100, 80): rgb(21, 128, 61)
.wave.shape.wide width 400 (300, 40): rgb(21, 128, 61)  (200, 80): rgb(21, 128, 61)
--- webkit
.wave.path       width 200 (150, 40): rgb(21, 128, 61)  (100, 80): rgb(21, 128, 61)
.wave.path.wide  width 400 (300, 40): rgb(255, 255, 255)  (200, 80): rgb(255, 255, 255)
.wave.shape      width 200 (150, 40): rgb(21, 128, 61)  (100, 80): rgb(21, 128, 61)
.wave.shape.wide width 400 (300, 40): rgb(21, 128, 61)  (200, 80): rgb(21, 128, 61)

On the 200px box both are the same: (150, 40) and (100, 80), near the lowest point of the wave, are painted. On the 400px box the difference shows. The coordinates of path() are fixed pixels; the shape is still 200px wide, so (300, 40) and (200, 80) are empty. shape() is written in percentages, stretches with the box, and both points are painted. If a clip shape must grow with its box in a responsive component, use shape(); writing a polygon() or inset() first and the shape() line after it is a good enough fallback for older browsers.

You will see a detail in the Baseline table: web-features lists path() as "not Baseline". The reason is not clip-path; the same entry also covers the SVG d property, which Safari does not support. According to browser-compat-data, clip-path: path() has worked without a prefix since Safari 13.1 and on its own has been widely available since 2021.

The Author Photo: circle()

In Bookstore we show the author photo in a circle:

<section class="author-card">
    <img class="author-photo" src="img/author-eliot.webp" alt="Portrait of George Eliot" width="240" height="360" loading="lazy">
    <p>George Eliot was the pen name of Mary Ann Evans (1819-1880), one of the leading English novelists of her time.</p>
</section>
.author-card { display: flex; gap: 1rem; align-items: center; margin-block: 1.5rem; }
.author-photo {
    flex: none;
    inline-size: 6rem;
    block-size: 6rem;
    object-fit: cover;
    /* same result as border-radius: 50% here, but it also clips outline and shadows */
    clip-path: circle(50%);
}
node scripts/08-pages.mjs
--- chromium
author photo: 96x96, clip-path circle(50%)
  corner (3, 3): rgb(249, 250, 251)  center: rgb(50, 65, 85)
strip: scrollWidth 1104, clientWidth 720
  mask-image linear-gradient(to right, rgba(0, 0, 0, 0), rgb(0, 0, 0) 32px, rgb(0, 0, 0) calc(100% - 32px), rgba(0, 0, 0, 0))
  pixel 6px from the right edge: rgb(239, 228, 215)  60px from the edge: rgb(193, 120, 40)
edition text line starts: 136, 144, 152, 161, 169, 174
--- webkit
author photo: 96x96, clip-path circle(50%)
  corner (3, 3): rgb(249, 250, 251)  center: rgb(50, 65, 85)
strip: scrollWidth 1104, clientWidth 720
  mask-image linear-gradient(to right, rgba(0, 0, 0, 0), rgb(0, 0, 0) 32px, rgb(0, 0, 0) calc(100% - 32px), rgba(0, 0, 0, 0))
  pixel 6px from the right edge: rgb(239, 228, 216)  60px from the edge: rgb(193, 120, 40)
edition text line starts: 136, 145, 153, 161, 169, 174

The first two lines are about the photo: the corner pixel of the 96×96 box matches the page background (249, 250, 251), and the middle is the photo itself. Here clip-path: circle(50%) and border-radius: 50% give the same picture. The difference is in the details: you can give an image with border-radius an outline and a shadow, and both follow the circle; clip-path cuts both off. On the other hand clip-path can take shapes other than a circle, and it can animate between two shapes of the same type (for example from circle(0) to circle(75%)). The photo is a 240×360 portrait; object-fit: cover fits it into the square box by cropping from the middle (the property itself is in post 08 of the CSS3 series), and the top of the author's head is cut off a little. Choosing the focal point of that crop is the subject of the next post in this series.

Fading Edges with mask-image

clip-path shows the inside of a shape completely and hides the outside completely. mask-image uses the alpha channel of an image instead: where the mask is opaque the element shows, where it is transparent it does not, and every value in between shows partly. That is why a gradient can fade the edge of an element smoothly:

/* 3) a gradient mask fades the edges */
.strip { inline-size: 300px; block-size: 40px; background: #1d4ed8;
    mask-image: linear-gradient(to right, transparent, #000000 40px, #000000 calc(100% - 40px), transparent); }
strip mask  x=1: rgb(246, 248, 253)  x=20: rgb(139, 164, 235)  x=150: rgb(29, 78, 216)

The strip's color is #1d4ed8 (29, 78, 216). At the far left the mask is almost fully transparent and the pixel is very close to white (246, 248, 253); 20px in, the mask is half opaque and the color is halfway between blue and white (139, 164, 235); in the middle the mask is fully opaque and the color is unchanged. The colors of transparent and #000000 in the gradient do not matter; only their alpha values count.

The "Recommended" strip on Bookstore's book page is a list of covers that scrolls sideways. The fading edges tell the user that the row continues:

.book-strip {
    display: flex;
    gap: 1rem;
    margin: 0;
    /* the first and last cover start outside the faded edges */
    padding: 0 2rem;
    list-style: none;
    overflow-x: auto;
    /* faded edges: a hint that the row scrolls sideways */
    mask-image: linear-gradient(to right, transparent, #000000 2rem, #000000 calc(100% - 2rem), transparent);
}
.book-strip img { display: block; inline-size: 10rem; block-size: auto; border-radius: 0.25rem; }

In the page measurement the strip's scrollWidth is 1104 and its visible width is 720, so about 380px of content is hidden on the right. The pixel 6px from the right edge is the orange of the cut-off Dune cover (193, 120, 40), almost blended into the background (239, 228, 215). Thanks to the padding, the first cover starts outside the faded area; at the start of the scroll it is fully visible. Scrolling without JavaScript and landing neatly on the covers (scroll snap) is in this series' post on overflow and scrolling.

Old code often has -webkit-mask-image. The unprefixed mask-image reached Chrome in version 120, in December 2023, and entered Baseline then; it has been widely available since June 2026. Old code written only with the prefix keeps working, but add the unprefixed line too; new masking features (such as the standard values of mask-composite) arrive on the unprefixed properties.

SVG Icon Masks and Forced Colors

A common use of the mask shorthand is to make a single-color SVG icon paintable in any color: you set the element's background to currentColor, the icon file becomes the mask, and the icon takes the color of the text.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21s-7.5-4.6-9.4-9.3C1.2 8.3 3.4 4.5 7 4.5c2 0 3.6 1.1 5 2.8 1.4-1.7 3-2.8 5-2.8 3.6 0 5.8 3.8 4.4 7.2C19.5 16.4 12 21 12 21z"/></svg>
/* an SVG icon as a mask: the color comes from currentColor */
.icon { display: inline-block; inline-size: 48px; block-size: 48px; background-color: currentColor;
    mask: url("../img/heart.svg") center / contain no-repeat; }
#icon-red { color: #b91c1c; }
#icon-fixed { color: #b91c1c; }
@media (forced-colors: active) {
    #icon-fixed { background-color: CanvasText; }
}
--- chromium, forced colors none
icon-red   background-color rgb(185, 28, 28), center pixel rgb(185, 28, 28)
icon-fixed background-color rgb(185, 28, 28), center pixel rgb(185, 28, 28)
--- chromium, forced colors active
icon-red   background-color rgb(255, 255, 255), center pixel rgb(255, 255, 255)
icon-fixed background-color rgb(0, 0, 0), center pixel rgb(0, 0, 0)

In normal mode both hearts are red. In forced colors mode (like Windows' high contrast themes) the browser turns background-color into the page's background color, white here. An icon whose background is white disappears on a white page: the middle of #icon-red is white. #icon-fixed, which switches its background to the CanvasText system color inside @media (forced-colors: active), is black and visible. If the icon is not decoration but the only content of a button, this line is required.

I ran this measurement on a separate, small lab page. When I left the icons on the big lab page, Chromium painted the icon in its old red even though the computed value said white; Playwright's forced colors emulation does not mirror the real mode exactly on every page. That is why trying it once by hand with a real forced colors setting is still necessary.

shape-outside: Flowing Text Around the Book

Text that flows around a floated element normally follows the element's rectangular box. shape-outside turns that boundary into a shape: a basic shape (circle(), polygon()…) or the opaque pixels of an image. I floated a picture of a tilted book on a transparent background and measured where the first lines of the paragraph start:

/* 4) text flows around the opaque pixels of the image */
.flow { inline-size: 420px; }
.flow img { float: left; inline-size: 180px; block-size: 180px; }
#shaped img { shape-outside: url("../img/book-tilted.png"); shape-margin: 8px; }
--- chromium
boxed  line starts: 180, 180, 180, 180, 180, 180, 180, 180
shaped line starts: 131, 139, 147, 155, 162, 170, 169, 109
--- webkit
boxed  line starts: 180, 180, 180, 180, 180, 180, 180, 180
shaped line starts: 131, 140, 147, 155, 163, 170, 169, 109

With a plain float, every line starts at pixel 180, the right side of the image box. With shape-outside: url(...), the lines start at 131 and move right as they go down (139, 147, 155…), following the edge of the tilted book with an 8px shape-margin; past the bottom corner of the book they pull back to 109. shape-image-threshold decides from which alpha value a pixel counts as part of the shape (default 0, meaning every pixel that is not fully transparent). The "About this edition" section in Bookstore uses the same technique:

.edition-cover {
    float: left;
    inline-size: 180px;
    block-size: auto;
    /* the text follows the edge of the book, not the square image box */
    shape-outside: url("../../img/book-tilted.png");
    shape-margin: 0.75rem;
}
.edition { display: flow-root; }

The last line of the page measurement: the six lines of the introduction follow the edge of the book from 136 to 174. display: flow-root makes the section contain the float, so the next section does not slide up next to the book (the block formatting context itself is in post 04 of the CSS3 series).

shape-outside: url() has one condition: the image must be readable under CORS. An image from the same origin as the page is fine; if the image comes from a CDN without a CORS header, the browser ignores the shape and the text follows the rectangle again. There is no error; it just silently does not work.

In Production

Like filter, clip-path and mask make the element a new stacking context (post 04 of the CSS3 series). A dropdown or tooltip that has to overflow a clipped card gets cut; if there is such content, clip only the image.

Because masking and clipping do not change layout, the clipped area still takes up space, and the hidden content can still be focused with the keyboard and read by screen readers. Using clip-path to "hide" content means hiding it visually while leaving it accessible; sometimes that is what you want (text only for screen readers), sometimes it is not.

Mask images are separate requests. For icon masks, a single SVG sprite or inline SVG usually means fewer requests and easier styling; we look at inline SVG icons in the next post of this series.

Common Pitfalls

First, leaving old -webkit-mask and -webkit-mask-image code without its unprefixed counterpart. It works today, but the unprefixed properties are the standard and new features arrive there.

Second, forgetting that the part clipped with clip-path still takes up space. The gap next to a clipped image comes from the box still having its old size.

Third, a masked icon painted with background-color disappearing in forced colors mode. Give it a system color such as CanvasText inside @media (forced-colors: active).

Fourth, giving an element with clip-path a shadow or an outline and being surprised that it does not show. Both are clipped; put the shadow on a parent with drop-shadow(), and use border-radius when you need the outline.

Fifth, using an image from another origin without a CORS header for shape-outside: url(). The shape is silently ignored.

Baseline Status

node scripts/baseline.mjs clip-path shape-function path-shape masks shape-outside
clip-path                  widely 2023-07-21  Chrome 88, Firefox 71, Safari 13.1, iOS 13
shape-function             newly 2026-02-24   Chrome 135, Firefox 148, Safari 18.4, iOS 18.4
path-shape                 not Baseline       Chrome 88, Firefox 97, Safari -, iOS -
masks                      widely 2026-06-07  Chrome 120, Firefox 53, Safari 15.4, iOS 15.4
shape-outside              widely 2022-07-15  Chrome 37, Firefox 62, Safari 10.1, iOS 10.3
FeatureStatusNote
clip-path (basic shapes)Widely availableHit testing uses the clipped shape
shape()Newly available (2026-02)Responsive shapes with percentages and calc()
path()Not Baseline (web-features)clip-path: path() alone is widely available since 2021; Safari lacks the SVG d property
mask, mask-imageWidely availableUnprefixed: newly in 2023-12, widely in 2026-06
shape-outside, shape-marginWidely availableurl() needs CORS

A Small Experiment

★☆☆ Turn #polygon in the lab into an octagon with cut corners and verify with 08-clip-mask.mjs that a click on the top left corner still goes to the section and a click in the middle goes to the box.

★★☆ Write a mask for the "Recommended" strip that fades only the right edge and keeps the left edge sharp; add a line to 08-pages.mjs that measures the pixel 6px in from the strip's left edge.

★★★ Show the author photo on the book page with a clip that grows from circle(45%) to circle(50%) on hover (with a transition that respects prefers-reduced-motion), and add a visible shadow to the photo without breaking the clip-path.

In the next post we style images and SVG icons: the inline behavior of img and its reset, object-position to fix the cut-off head in the author photo, image-rendering, fill, stroke and currentColor in inline SVG, and catalog icons from a single sprite.