lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
Let’s say we have an SVG path
definition that we want to use to
shape a container element’s outline
via CSS
clip-path
.
<path d="M1,1 l0,1 l40,1 a1,1 0 0,0,18 0 l40,-1 l0,-1 z" />
For simplicity, we might approximate this clipping shape with
clip-path: polygon(…)
. However, unlike vector paths, polygons can’t reasonably
be used for properly curved shapes; it’s all straight lines and edges.
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
clip-path: polygon(45% 0, 50% 1rem, 55% 0, 100% 0, 100% 100%, 0 100%, 0 0);
Gratifyingly, CSS also supports SVG-style paths via clip-path: path(…)
. Yet if
we insert the aforementioned path definition there, scaling is way off: The
shape’s size appears to be absolute (100×100 pixels, as per its viewBox
)
rather than relative to our container element.
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
clip-path: path("M1,1 l0,1 l40,1 a1,1 0 0,0,18 0 l40,-1 l0,-1 z");
As I understand it, that’s because the path’s user units are not tied to our container element’s size.
We could remedy that by
moving our path definition into a separate SVG
instead of embedding it directly within CSS, thus allowing for
<clipPath clipPathUnits="objectBoundingBox">
and scaling the shape to match
the container’s size. Unfortunately, not only is that
cumbersome in various ways1, it also doesn’t quite
result in the desired effect: While our shape now stretches horizontally as
expected, vertical scaling seems exaggerated. In other words, our shape does
not maintain its original aspect ratio with this technique. We can confirm
this by changing the container element’s height.
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
<svg width="0" height="0" viewBox="0 0 100 100">
<defs>
<clipPath id="clip-shape-arc" clipPathUnits="objectBoundingBox">
<path d="M0.01,0.09 l0,0.09 l0.4,0.09 a0.01,0.09 0 0,0,0.18 0 l0.4,-0.09 l0,-0.09 z" />
</clipPath>
</defs>
</svg>
clip-path: url(#clip-shape-arc);
At this point, it seems prudent to further simplify our example: Let’s replace that fairly complex path definition with another polygonal approximation. As long as we’re still employing vector paths for that, the same principles apply and we can disregard headache-inducing curved shapes for the moment.
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
<svg width="0" height="0" viewBox="0 0 100 100" class="nonvisual">
<defs>
<clipPath id="clip-shape-poly" clipPathUnits="objectBoundingBox">
<path d="M0,0 L0.4,0.6 L0.6,0.6 L1,0 z" />
</clipPath>
</defs>
</svg>
clip-path: url(#clip-shape-poly);
That new path definition is comparatively straightforward: A sloped shape, up to 60 % tall and spanning the entire width. Unsurprisingly, the aspect-ratio issue can be observed here just the same: The slope’s angle changes along with the container element’s height. That’s exactly what we don’t want to happen though!
Temani Afif graciously took the time to
suggest that
masking might be more suitable
than clipping here.2 And indeed, switching from clip-path
to mask
gives us a
new set of affordances – notably the option to maintain aspect ratio, but also
control both size and position more directly (though not necessarily
intuitively; YMMV).
So let’s go back to our original path definition and plug that in – et voilà: This is pretty much the result we were hoping for in the first place!
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
mask: no-repeat;
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M1,1 l0,1 l40,1 a1,1 0 0,0,18 0 l40,-1 l0,-1 z" /></svg>');
mask-size: 100% auto;
mask-position: top left;
All that’s left now is inverting; remember we really wanted to clip outside rather than inside. CSS masking can do that by combining multiple layers, so we don’t even have to change our path definition!
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
lorem ipsum dolor sit amet
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M1,1 l0,1 l40,1 a1,1 0 0,0,18 0 l40,-1 l0,-1 z" /></svg>'),
linear-gradient(red, red);
mask-composite: exclude;
So yay, we’ve achieved the desired effect, eventually, with just a few lines of
code. Nevertheless, it feels like I haven’t fully grokked how all the various
pieces interact. This might be exacerbated by the fact that the respective
affordances of clip-path
and mask
seem simultaneously very similar yet oddly
incongruent – with any luck, I’ll develop a more solid mental model over time.