Clip paths

The clipPath element is an interesting one. Mainly because of how ridiculously complicated it is. As of 2023, even browsers struggle with some edge cases. And regular SVG libraries barely support half of its features.

In this chapter, I will try to explain how it's even possible.

Allowed children

For starters, the clipPath name misleads us into thinking that it contains a single path. Instead, it can have any number of paths. Moreover, any basic shapes (circle, rect, etc.), path elements and text elements are allowed. But no groups and images.

Yes, no groups. Unlike a mask or a pattern, a clipPath is a list of elements, not a tree.

Weirdly enough, use is allowed, but only when it directly (no use -> use -> path) references a basic shape, path or text. Which is very strange because use is usually instanced as a group, but not in clipPath.

line, while being a basic shape, isn't allowed either. Since it cannot be filled. Same with paths that do not produce a fill-able shape.

Clip all

By default, a clipPath clips the whole element and each clipPath child element defines a region that should be preserved. Because of that, a clipPath without children or with only malformed/unsupported children would clip the whole element it is applied to.

No styles and no markers

Any style attributes like fill, stroke, opacity, filter, mask, etc. on clipPath children will be ignored. All children must be parsed as fill="black" stroke="none".

The interesting part here is that markers in SVG are also part of the element style. Therefore markers should not be parsed and rendered inside clip paths as well.

On the other hand, clipPath children are affected by display and visibility properties.

clip-rule

To complicate things for no reason, clipPath ignores the fill-rule property and requires us to use clip-rule instead. Why do we need such distinction? I have no idea.

And unlike other reference-able elements, like linearGradient, mask, filter, etc., which you can usually find inside defs, clipPath can search for clip-rule in parent elements as well. Meaning that in the case of:

<g clip-rule="evenodd">
    <clipPath id="clip1">
        <rect/>
    </clipPath>
</g>

clip-rule on g does apply to rect inside clipPath.

Nested clip paths

What if I tell you that you can set a clip path on a clip path which also has a clip path on all of its children?

Yes, this is a valid SVG:

<clipPath id="clip1">
    <rect/>
</clipPath>
<clipPath id="clip2">
    <rect clip-path="url(#clip1)"/>
</clipPath>
<clipPath id="clip3" clip-path="url(#clip2)">
    <rect/>
</clipPath>

In SVG, clipPath is an endlessly recursive tree of paths that eventually produces an alpha mask. Yes, an alpha mask. Not a path. Not a list of paths, but a mask.

The reason I'm trying to highlight this fact is because a lot of 2D graphic libraries do provide a clip(path) function, like CanvasRenderingContext2D.clip() in JavaScript. And while it can be used in some cases it would be hard, if not impossible, to implement a proper SVG clipPath using such function.

How well nested clip paths are supported? Barely... Browsers are mostly fine, except a couple of bugs in Firefox. Inkscape, Batik and librsvg essentially do not support this at all.

Short-hands

While most SVG libraries struggle to implement even the SVG 1.1 spec, the SVG authors decided that it isn't enough and added clip-path short-hands to SVG 2. Now you can write just:

<rect fill="green" x="20" y="20" width="160" height="160" clip-path="circle()"/>

instead of:

<clipPath id="clip1">
    <circle cx="100" cy="100" r="80"/>
</clipPath>
<rect fill="green" x="20" y="20" width="160" height="160" clip-path="url(#clip1)"/>

And as you can imagine, this one is extremely hard to implement. As of December 2023, only browsers even attempt to support it.

clipPath vs mask

After reading all of the above you might be wondering what is the difference between clipPath and mask then? Aren't they doing the same thing? Sure, some simple cases like rectangular clips or single-path clips can be optimized, but a proper clipPath implementation would still have to use to an alpha mask. Right?
And honestly, I have no idea either. Especially since SVG 2 introduced mask-type=alpha which makes mask behave exactly like clipPath.

Essentially, clipPath is just a mask subset instead of being it's own thing.

An 8-bit mask

An interesting tidbit is that the SVG 1.1 spec says that clipPath is "conceptually" a 1-bit mask. Which is technically not true, because you still need 8-bit to implement anti-aliasing. The spec does clarify that, but the wording is still a bit confusing.

The reason I'm mentioning this at all is because Batik does implement clip paths as 1-bit masks. Meaning that clipping is never anti-aliased. It's the only SVG library I know that does this. And yes, at least the SVG 1.1 spec doesn't actually require clip paths to be anti-aliased.

Clip path transform

clipPath has an interesting quirk in a way it handles invalid transforms, like scale(0 0). In general, an invalid transform on an element in SVG makes the whole element invalid and it should not be rendered.
So you might think that clip paths with invalid transforms should simply be ignored. clip-path="url(#invalid-clip)" would become clip-path="none". But instead, the whole element should not be rendered.

No region

Unlike the mask element, clipPath doesn't have a region. Meaning that it's up to the implementation to decide how big the alpha mask bitmap should be.