How to Make Any SVG Path a CSS Clip Path

Published 2023-02-17

This article describes a general-purpose method to turn just about any SVG into a <clipPath> that will correctly clip arbitrary HTML elements. I found this technique helpful when building this very site, as setting the clip-path on a display: block or display: inline-block element is an easy way to include icons on a page.[1]

Clipping Paths

The Basics of Clipping Paths

The clip-path property is a relatively new and not fully supported (or old and fully supported, depending when you’re reading this) CSS property.[2] It allows using a shape to specify a clipping region for the given HTML element: a region outside of which the element is not drawn. In other words, the clipping region is like a mask[3] that only lets through portions of the background within the region’s interior (or equivalently, that blocks the portions of the background outside of the region). For instance, here is a 200×200 image, followed by the same image with a circled-star icon as its clipping path.

A square image divided into four regions in the top left A square image divided into four regions in the top left

Regardless of what portion of the image lies inside the clipping region, the image itself still takes up the full amount of space it normally would. Clipping doesn’t affect the size of elements or how they’re laid out, just what portion of them we actually see. The portion outside the clipping region appears transparent, but it’s still there.

Note how the clipping region is centered over the image and fills the entire space; we’ll come back to this later. For now, let’s just confirm this fact by overlaying the clipped image on a dimmed copy of the original.

A square image divided into four regions in the top left A square image divided into four regions in the top left

From now on, we’ll always show our clipped images overlaid on a dimmed copy of the full image.

Valid clip-paths are in approximately one-to-one correspondence with shapes in SVG: you can clip with circles, polygons, paths, and more:

clip-path: inset(100px 50px);
clip-path: circle(50px at 0 100px);
clip-path: ellipse(50px 60px at 0 10% 20%);
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
clip-path: path(
  "M0.5,1 C0.5,1,0,0.7,0,0.3 A0.25,0.25,1,1,1,0.5,0.3 A0.25,0.25,1,1,1,1,0.3 C1,0.7,0.5,1,0.5,1 Z"
);

Thankfully, unlike their SVG counterparts, these clip-path shapes understand dimensions written in CSS units and even some special keywords. So it’s easy to write, say, “clip this element with an ellipse that extends precisely to its horizontal and vertical edges”: it’s just clip-path: ellipse(50% 50% at center). Couldn’t be easier.

That is, unless you’re using clip-path: path(…​), which takes a good old-fashioned SVG path string, which does not support these fancy CSS units. Not only do SVG paths use a somewhat arcane syntax, but they also must be written in terms of unit-less numbers, which specify the location of their path’s elements, such as a line, an arc, or a Bezier curve, in absolute units on the Cartesian plane. This is counter to other numbers in CSS, which almost universally require units — 1px, 2em, 3rem, 4vw, etc.[4]

Therefore, if you want to use a path to clip an element, you must know the dimensions of the element. For instance, to draw a path that references the center of the element, you would need to know the width and height of the element and then divide them by 2 yourself. 50% just isn’t available in path(…​).

It can get confusing going back and forth between SVG and CSS, as SVG elements have many attributes that are also valid CSS properties. For instance, <circle stroke-width="3"></circle> is a valid SVG shape (or it would be if we also included cx, cy, and r), but so is <circle></circle> with accompanying CSS circle { stroke-width: 3; }. Throughout this article, unless otherwise specified, clip-path will always refer to the CSS property applied to an HTML element, not the SVG attribute. For a list of other properties that can be used as both, see here.

Using SVG <clipPath>s

There is one other way to specify a shape to use as clip-path: the clip-path: url(#clippath-id) syntax. This looks up the <clipPath> element with the given ID — which must live in some SVG on the page — and uses the union of the regions clipped by its shapes[5] to construct the clipping region. The <clipPath> itself is not drawn in the SVG; it is merely referenced by other shapes or HTML elements, who use it as their clip-path.

But <clipPath>s are still SVG elements, so they are drawn in absolute coordinates. Here is the source code of the circled-star <clipPath> we’ve been using.

Circled-star SVG[6]
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path d="M50 3.537337A46.461724 46.461724 0 003.5393625 49.997973a46.461724 46.461724 0 0046.4606395 46.46469 46.461724 46.461724 0 0046.460635-46.46469A46.461724 46.461724 0 0050 3.537337Zm0 10.908081 10.450199 21.171887 23.364033 3.395617L66.90509 55.492533l3.991259 23.270846-20.896347-10.985077-20.900406 10.985077 3.991255-23.270846-16.905089-16.479629 23.364035-3.395602Z"/>
</svg>

As we can tell from the viewBox (but more on that later), the dimensions of this icon are 100×100. So, if we tried to tried to naively convert it to a <clipPath>, as shown below…​

Circled-star, as a <clipPath> (but not actually)
<svg xmlns="http://www.w3.org/2000/svg"> (1)
<clipPath id="clippath-circled-star">
<path d="M50 3.537337A46.461724 46.461724 0 003.5393625 49.997973a46.461724 46.461724 0 0046.4606395 46.46469 46.461724 46.461724 0 0046.460635-46.46469A46.461724 46.461724 0 0050 3.537337Zm0 10.908081 10.450199 21.171887 23.364033 3.395617L66.90509 55.492533l3.991259 23.270846-20.896347-10.985077-20.900406 10.985077 3.991255-23.270846-16.905089-16.479629 23.364035-3.395602Z"/>
</path>
</clipPath>
</svg>
1 This SVG’s elements aren’t actually going to be rendered anywhere, so viewBox isn’t needed anymore.

…​and we use it as the clip-path of our image, which has dimensions 200×200, using the following CSS…​

.image {
	clip-path: url(#clippath-circled-star);
}

then we end up with this:

A square image divided into four regions in the top left A square image divided into four regions in the top left

This time, the clipping region does not take up the entire image; it exists entirely within the 100×100 region in the upper left because the width and height of the <clipPath>’s <path> were both 100.[7] So, how can we fix this to achieve what we saw above? How can we get the clipping region to span the entire 200×200 image?

clipPathUnits

Very helpfully, <clipPath> has an attribute, clipPathUnits, that lets us specify exactly what the units of the <clipPath> represent. The default value, userSpaceOnUse, leads to the behavior we just saw: it assumes the <clipPath> and the HTML element it is clipping use the same coordinate system. So, our <clipPath>, which only existed in the square from (0, 0) to (100, 100), created a clipping region that only exposed the part of the image in that same upper-left square.

The other choice for clipPathUnits is objectBoundingBox, which assumes that the <clipPath> occupies a merely 1×1 square, which is then stretched or compressed in both dimensions so that it has the same dimensions as the element it is clipping.

Let’s add clipPathUnits="objectBoundingBox" to our <clipPath> above and use it to clip our image again.

A square image divided into four regions in the top left A square image divided into four regions in the top left

But that’s just the background. Where’d our clipped image go?

As mentioned, objectBoundingBox assumes that the <clipPath> occupies a merely 1×1 square. But our <clipPath> actually still occupies a 100×100 square, and it was stretched so that the top-left unit square fills the 200×200 image — a factor of 200. This means the entire <clipPath> was blown up to 20,000×20,000! It completely missed the image; the region it would expose is way, way down to the bottom right. We have to figure out how to get our <clipPath>’s <path> down into a 1×1 square.

Transforming <clipPath>s

Shrinking the <path>

The hard way to do this would be to simply edit our <path>: take all the numbers and divide them by 100 (except for the boolean flags!). But this would be tedious and would make it hard to just use any old SVG icon as a <clipPath>. Thankfully, there is a very easy way to transform SVG elements, which is…​ the transform attribute. transform applies a linear transformation to an element, and scaling by a factor of 1/100 is a linear transformation, so we’re good. All we have to do is add transform: scale(.01) to the <path> (not to the <clipPath>!), and we get a lovely 1×1 <clipPath> which clips as we expect. This is precisely how the original clipped image in this article was created.

For completeness, here is the final SVG. (In case you’re copy-pasting this somewhere, note that we’ve changed the id of the <clipPath>.)

<svg>
<clipPath id="clippath-circled-star-1x1" clipPathUnits="objectBoundingBox">
	<path transform="scale(.01)" d="same circled-star path" />
</clipPath>
</svg>

If you want to use multiple <clipPath>s in the same document, they don’t all need their own SVG. Putting them all in the same SVG is fine, like so:

<svg>
<clipPath id="clippath-1">...</clipPath>
<clipPath id="clippath-3">...</clipPath>
<clipPath id="clippath-3">...</clipPath>
...
</svg>

For images that aren’t square, the <clipPath> scales separately in each dimension. Here’s just the left half of our image, and next to it is what happens when we use the circled star to clip just that half (which is now twice as tall as it is wide).

A square image divided into four regions in the top left A square image divided into four regions in the top left A square image divided into four regions in the top left

But what happens when it’s the <clipPath> that isn’t square?

Non-Square Clip Paths

So far we’ve been taking advantage of a very nice property of our <clipPath>: its width and height are the same, so we could scale them down by the same amount. This meant that if the original path was centered in its 100×100 bounding box, then the clipping region would also be centered in the element it was clipping.

But what if it weren’t square? Here is an up-arrow icon that which is taller (16) than it is wide (10):

<svg viewBox="0 0 10 16" xmlns="http://www.w3.org/2000/svg">
<path d="M4.990885 1.8584614a.81151495.81151495 0 00-.6168162.2852696L.56793697 6.6167599a.8114338.8114338 0 00.0919202 1.1436149.8114338.8114338 0 001.14361473-.091921L4.1883263 4.865838v8.464269a.8114338.8114338 0 00.8114339.811433.8114338.8114338 0 00.8114338-.811433V4.8772487L8.1966825 7.669722a.8114338.8114338 0 001.1442485.090018.8114338.8114338 0 00.090019-1.1442482l-3.82198-4.473029a.81151495.81151495 0 00-.618085-.2840014Z" />
</svg>

Now is a good time to talk about the SVG’s viewBox attribute. If you imagine the SVG’s contents lying in the infinite Cartesian plane, the viewBox tells us what rectangle in the plane to restrict our attention to; nothing outside this rectangle is drawn. (In a sense, the viewBox is like a rectangular clip-path of the whole SVG, which would otherwise be infinitely large and almost entirely empty.) viewBoxes take the form of "x y w h", where x is the x-position of the rectangle’s upper left corner, y is that corner’s y-position, and w and h are the rectangle’s width and height, respectively. The up-arrow SVG has its origin at (x, y) = (0, 0) and has a width of 10 and a height of 16.

SVGs also can have width and height attributes, but these are unrelated to viewBox; they merely dictate to the program displaying the SVG what the dimensions of the rendered SVG should be, in pixels on the screen. They do not affect the region of the SVG that is rendered. If an SVG has viewBox="0 0 10 16" width="20" height="64", then

  1. It will be first be rendered in a 10×16 rectangle

  2. Then its width will be scaled by a factor of 2 to achieve a final width of 20

  3. Then its height will be scaled by a factor of 4 to achieve a final height of 64

Anyway, here’s that up-arrow icon. (The border is just a guide; it’s not actually part of the icon.)

How do we use this as a clip-path? What we’d like to achieve is an arrow-shaped clipping region the same size as, and centered on the image, as shown below.

A square image divided into four regions in the top left A square image divided into four regions in the top left

So, how do we make this happen?

Let’s try our transform trick from above; maybe it’ll still work here.

<svg>
<clipPath id="arrow-1" clipPathUnits="objectBoundingBox">
	<path
		transform="scale(0.0625)" (1)(2)
		d="same circled-star path"
	/>
</clipPath>
</svg>
1 0.0625 = 1/16, which gets our shape with dimensions 10×16 to lie entirely in a 1×1 square.
2 Unfortunately, SVG doesn’t let you write out the scale factor as a literal division like 1 / 16 (no, not even in calc()), so you’ve got to plug the division into a calculator and write out the resulting decimal number.

A square image divided into four regions in the top left A square image divided into four regions in the top left

This is close, but not quite right — it isn’t centered. Eyeballing, it looks like it only covers the left five-eighths of the image. Hmm.

Why isn’t it centered? Well, the original icon had a width of 10 and a height of 16. When we scaled it by 1/16 = 0.0625, we made sure the new height went from 0 to 1. But the new width also got divided by 16, which means that it only goes from 0 to 10/16 = 5/8 = 0.625, which indeed is not all the way over to 1. That’s why the arrow above only seems to cover the left five-eighths of the image — its maximum x-coordinate is only 5/8.

So what can we do about this? A bad solution would be to scale the width and height separately. While this would get the <clipPath>’s <path> to have dimensions 1×1, it would not preserve the original aspect ratio, and so we’d be using a fundamentally different shape. Here’s what that would look like, with transform: scale(0.1 0.625).

A square image divided into four regions in the top left A square image divided into four regions in the top left

Yech.

Centering the <clipPath>

What we need to do is translate our correctly-scaled-down <path> so that it’s centered in the 1×1 box. But how much do we need to translate it by? After scaling it down, its left edge was at 0 and its right edge was at 5/8, so we need to shift it to the right by (1 - 5/8)/2, or 3/16 = 0.1875. If an arbitrary shape’s upper left corner is at (x, y) = (0, 0) and the shape has width w and height h, then its center is at (w/2, h/2). Assuming without loss of generality that w < h, after scaling it down by 1/h the center would end up at (w/(2*h), 1/2). The translation that moves this point to (0.5, 0.5) would be (0.5-w/(2*h), 0), or ((1-w/h)/2, 0), which is indeed what we found above. So the correct transformation to apply to the <path> would be transform: translate(0.1875 0) scale(0.0625) — that’s “scale by 1/16, then translate x and y by 3/16 and 0, respectively”.[8] If our <clipPath> was wider than it was tall, say, 200×100, then we’d scale it by 1/200 = .005 and translate it downward by (1 - 100/200)/2 = 0.25.

We’ve solved the problem for SVGs whose viewBox's origin is at (x, y) = (0, 0). What about for SVGs whose origin is elsewhere?[9] If the viewBox is "x y w h", then the center of the shape would be at (x+w/2, y+h/2). Again assuming that w < h, we’d scale by 1/h to get it to fit in a a 1×1 square, which would move the center to ((x+w/2)/h, (y+h/2)/h). Then, to get the center to be located at (0.5, 0.5), we’d translate it by (0.5-(x+w/2)/h, 0.5-(y+h/2)/h). The resulting transformation would be

transform="translate(0.5-(x+w/2)/h 0.5-(y+h/2)/h) scale(1/h)"

A Better Solution

At this point we’ve technically solved the problem. But the solution is pretty ugly; it requires an annoying amount of busywork with a calculator and there is no way to see where the decimals in the transform came from at a glance. There is also a strong implicit dependence on w being smaller than h. We can do better!

What we would really like to do, if it were possible, is center the <path> first, and then scale it down. However, the scale(…​) transform always scales relative to the origin: the result of scaling a point (x, y) by s will always be (s*x, s*y). You don’t get to specify your own “scale origin”, so translating and then scaling won’t work.[10]

If what we’re looking for is a simpler way to translate our post-scale <path> — something simpler than translate(0.5-(x+w/2)/h, 0.5-(y+h/2)/h) — maybe we should perform the scaling on the shape when it’s centered at the origin. Then translating it to the correct final position would simply be translate(.5 .5) — that’s the center of a 1×1 square, after all. But how can we get the shape to be centered at the origin?

Easy: we simply apply translate(-(x+w/2) -(y+h/2)) first! This moves the shape’s center to the origin of the coordinate system, which means the “scale origin”, which is always the actual origin, now corresponds to the object’s center, which is exactly the point about which we would like to scale. So, to scale our <path> correctly, we simply need the following:

transform="translate(.5 .5) scale(1/max(w, h)) translate(-(x+w/2) -(y+h/2))"
Remember, you actually have to do these divisions out to get a decimal number. You can’t literally write e.g., 1 / 16.

To make the translations really explicit, we can even split them up:

transform="translate(.5 .5) scale(1/max(w, h)) translate(-w/2 -h/2) translate(-x -y)"

In English (remember, the functions are applied right to left):

  1. Translate the SVG so that its upper left corner is at (0, 0).

  2. Then, translate it so that its center is at the origin.

  3. Then, scale it so that it fits in a 1×1 square.

  4. Then, translate it so that its center is at (0.5, 0.5), the center of a 1×1 square.

Why is this better? To start, we’ve got only one calculation we might need a calculator for, and that’s 1/max(w, h); w/h is just gone altogether. In addition, if, say, w changes, it’s trivial to update translate(-w/2 -h/2) with the new value of w/2, and if w < h remains the same then that’s the only change you have to make at all. Finally, this transform is self-documenting in two ways. First, you have the original viewBox of the <path> written out in translate(-w/2 -h/2) translate(-x -y). And second, while the decimal number in the scale(…​) is inscrutable except in the simplest cases, when you write the transform this way, you know it’s just the reciprocal of twice the larger of the two numbers in translate(-w/2 -h/2).

Putting it all together, then:

<svg>
<clipPath id="arrow-2" clipPathUnits="objectBoundingBox">
	<path
		transform="translate(.5 .5) scale(0.0625) translate(-5 -8)" (1)
		d="same arrow path"
	/>
</clipPath>
</svg>
1 Negative one-half of 10 and 16, respectively. We know, then, that 0.0625 must be the reciprocal of the larger of 10 and 16. The original SVG had its upper left corner at (0, 0), so we don’t need to handle that here.

Which, as expected, leads to this:

A square image divided into four regions in the top left A square image divided into four regions in the top left

Success!

Going Further

Other Transforms

If we only ever wanted to place a <clipPath> in the center of our element and have it cover the whole element, we know everything we need to know. But we can use this same technique to apply more exotic transformations to <clipPath> elements as well.

A simple next step would be to have our <clipPath> remain centered, but be smaller than its full size. This is easy to do: we just change the scale from 1/h to something smaller. If we want our arrow to be half-size, we’ll scale it by half of 1/16, or 1/32 = 0.03125.

<svg>
<clipPath id="arrow-half" clipPathUnits="objectBoundingBox">
	<path transform="translate(.5 .5) scale(0.03125) translate(-5 -8)" d="same arrow path" />
</clipPath>
</svg>

This gets us

A square image divided into four regions in the top left A square image divided into four regions in the top left

To go even further, suppose we wanted a <clipPath> consisting of four copies of the arrow, each of which clips one of the corners of the original image and is rotated 90° from the previous one. Rather than scale the <path>s down to 1×1, we’ll scale them down to 0.5×0.5. And instead of translating them to (0.5, 0.5), we’ll translate them to (0.5±0.25, 0.5±0.25). Since, like scaling transformations, rotations are always applied about the origin, we apply the rotation before the final translation so that the shapes are rotated about their center.

<svg>
<clipPath id="four-arrows" clipPathUnits="objectBoundingBox">
	<path transform="translate(.25 .25) scale(0.03125) translate(-5 -8)" d="same arrow path" />
	<path transform="translate(.75 .25) rotate(90) scale(0.03125) translate(-5 -8)" d="same arrow path" />
	<path transform="translate(.75 .75) rotate(180) scale(0.03125) translate(-5 -8)" d="same arrow path" />
	<path transform="translate(.25 .75) rotate(270) scale(0.03125) translate(-5 -8)" d="same arrow path" />
</clipPath>
</svg>

A square image divided into four regions in the top left A square image divided into four regions in the top left

Multiple Shapes in One <clipPath>, and CSS transforms

We now know how to take just about any SVG shape at all, such as <path>, <ellipse>, and <polygon>, and turn it into a <clipPath>. But what about collections of shapes? If an SVG contains several shapes, what’s the right way to form a <clipPath> out of them? We’ll need a way to transform them all in lock step.

It’s simple to adapt the above technique to this more complex problem.

  1. In the <clipPath>, add all of the shapes from the original SVG.

  2. Set all of those shapes’ transforms (not the <clipPath>'s transform!) to transform: translate(.5 .5) scale(1/max(w, h)) translate(-(x+w/2) -(y+h/2)). You can do this by adding the transform to each shape individually, but it’s probably easier to use CSS.[11]

    The syntax used to specify the transform property in CSS is a bit different from the syntax used when transform is an SVG attribute. Most importantly, numbers in CSS require units after them. When using CSS to style SVG elements, you almost certainly want to use px, pixels, as your units. (Numbers in SVGs don’t take explicit units because they are already implicitly in units of pixels.) There are other differences as well, e.g., translate(dx, dy) needs a comma between dx and dy in CSS, whereas the comma is optional in an SVG.

So, suppose we had the following SVG, which contains four circles equally spaced around its center.

<!-- Origin at (x, y) = (-25, -100), dimensions (w, h) = (50, 100) -->
<svg viewBox="-25 -100 50 100">
	<circle cx="0" cy="-65" r="6"></circle> <!-- Top circle -->
	<circle cx="0" cy="-35" r="6"></circle> <!-- Bottom -->
	<circle cx="-15" cy="-50" r="6"></circle> <!-- Left -->
	<circle cx="15" cy="-50" r="6"></circle> <!-- Right -->
</svg>

Again, I’ll draw a border around the SVG.

To turn these four circles into a <clipPath>, we’ll just follow the instructions above. The viewBox is "-25 -100 50 100", so the initial translation is translate(-25 -50) translate(25 100), or translate(0 50). The largest dimension is the height, 100. So, the transformation we need is (in CSS syntax, since that’s how it happens to actually be applied on this page):

#four-circles > circle {
	transform: translate(.5px,.5px) scale(.01) translate(0px,50px);
}
Don’t forget the px and commas!

And the SVG we need is

<svg>
<clipPath id="four-circles" clipPathUnits="objectBoundingBox">
	<circle cx="0" cy="-65" r="6"></circle> <!-- Top circle -->
	<circle cx="0" cy="-35" r="6"></circle> <!-- Bottom -->
	<circle cx="-15" cy="-50" r="6"></circle> <!-- Left -->
	<circle cx="15" cy="-50" r="6"></circle> <!-- Right -->
</clipPath>
</svg>

This results in

A square image divided into four regions in the top left A square image divided into four regions in the top left

It works!

Appendix A: d: SVG Attribute or CSS Property?

As we mentioned above, many attributes of SVG elements can also be styled using a CSS property. transform was one such attribute. Throughout this article, I wrote d="same circled-star path" and d="same arrow path" several times. Of course, in the source for this page, those paths are actually written out in full each time. To avoid repeating myself, shouldn’t I have just used a CSS d attribute to apply these path strings everywhere they were used?

First, we would need to check that d is indeed a presentation attribute (not every attribute is). And it is!

In all browsers, that is, except Safari. Unfortunately, this was enough to force me to write out the path strings in full each time. Once Safari supports using d as a presentation attribute, the “right” way to do the four-rotated-arrows <clipPath> would be this:

<style>
#four-arrows > path {
	d: path("same arrow path");
}
</style>
<svg>
<clipPath id="four-arrows" clipPathUnits="objectBoundingBox">
	<path transform="translate(.25 .25) scale(0.03125) translate(-5 -8)" />
	<path transform="translate(.75 .25) rotate(90) scale(0.03125) translate(-5 -8)" />
	<path transform="translate(.75 .75) rotate(180) scale(0.03125) translate(-5 -8)" />
	<path transform="translate(.25 .75) rotate(270) scale(0.03125) translate(-5 -8)" />
</clipPath>
</svg>

Appendix B: Matrices

This section assumes some basic linear algebra knowledge.

Above, we found two ways of transforming our <clipPath>, one of the form translate(…​) scale(…​) and the other of the form translate(…​) scale(…​) translate(…​). How can we verify that these were, in fact, equivalent?

As mentioned above, the transformations in transform are linear transformations, which can be represented by matrices. We should verify that the two forms of our transform do in fact encode the same linear transformation. We can do this by comparing their matrices.

Since we’re in a 2D vector space, we might expect that our matrices would be 2×2 and operate on 2D vectors. Unfortunately, were this the case, we’d have no way of representing translations, which are not linear transformations.[12] There is a trick to fix this: we work in a 3D vector space and write the point (x, y) as (x, y, 1). Then, the translation matrix \(T_{t_x,t_y}\) and the scale matrix \(S_s\) can be written as follows;

\[\begin{aligned} T_{t_x,t_y}&=\begin{bmatrix} 1&0&t_x\\0&1&t_y\\0&0&1 \end{bmatrix}\\ S_{s}&=\begin{bmatrix} s&0&0\\0&s&0\\0&0&1 \end{bmatrix} \end{aligned}\]

In general, an arbitrary transformation has six[13] parameters and has the form

\[\begin{bmatrix} a&b&c\\d&e&f\\0&0&1 \end{bmatrix}\]

Actually, this isn’t quite true. Transformations can also act along the z-axis, transforming shapes so that they no longer lie in the plane of the screen. These transformations are 4×4 and have 4×(4-1)=12 parameters. We won’t consider them further.

The full list of transform functions is available here. The elem.getCTM() function will retrieve the computed DOMMatrix that is applied to an SVG element. For an HTML element, you can use window.getComputedStyle(elem).transform, but note that this might return none if there is no transform in the element’s style.

One can verify that these act as expected, i.e., that

\[\begin{aligned} T_{t_x,t_y}\begin{bmatrix}x\\y\\1\end{bmatrix}&=\begin{bmatrix}x+t_x\\y+t_y\\1\end{bmatrix}\\ S_s\begin{bmatrix}x\\y\\1\end{bmatrix}&=\begin{bmatrix}sx\\sy\\1\end{bmatrix} \end{aligned}\]

Now, assuming the SVG’s viewBox is "x y w h" with w < h, the first version of the transform was

\[\begin{aligned} T_{\frac{1}{2}-(x+\tfrac{w}{2})/h,\tfrac{1}{2}-(y+\tfrac{h}{2})/h}S_{\tfrac{1}{h}}&=\begin{bmatrix} 1&0&\tfrac{1}{2}-(x+\tfrac{w}{2})/h\\0&1&\tfrac{1}{2}-(y+\tfrac{h}{2})/h\\0&0&1 \end{bmatrix}\begin{bmatrix} \tfrac{1}{h}&0&0\\0&\tfrac{1}{h}&0\\0&0&1 \end{bmatrix}\\ &=\begin{bmatrix} \tfrac{1}{h}&0&\tfrac{1}{2}-(x+\frac{w}{2})/h\\0&\tfrac{1}{h}&\tfrac{1}{2}-(y+\tfrac{h}{2})/h\\0&0&1 \end{bmatrix} \end{aligned}\]

whereas the second version was

\[\begin{aligned} T_{\tfrac{1}{2},\tfrac{1}{2}}S_{\tfrac{1}{h}}T_{-(x+\tfrac{w}{2}),-(y+\tfrac{h}{2})}&=\begin{bmatrix} 0&0&\tfrac{1}{2}\\0&0&\tfrac{1}{2}\\0&0&1 \end{bmatrix}\begin{bmatrix} \tfrac{1}{h}&0&0\\0&\tfrac{1}{h}&0\\0&0&1 \end{bmatrix}\begin{bmatrix} 0&0&-\left(x+\tfrac{w}{2}\right)\\0&0&-\left(y+\tfrac{h}{2}\right)\\0&0&1 \end{bmatrix}\\ &=\begin{bmatrix} \tfrac{1}{h}&0&\tfrac{1}{2}-(x+\frac{w}{2})/h\\0&\tfrac{1}{h}&\tfrac{1}{2}-(y+\tfrac{h}{2})/h\\0&0&1 \end{bmatrix} \end{aligned}\]

They’re equal, so, as we’d hope, the two solutions we found correspond to the same linear transformation.


1. This is much easier than sprinkling SVGs and <use> elements throughout the page, as <use>s are quite tricky to style because they live in a shadow DOM. Styling an element with a clip-path is as easy as setting its background-color, assuming you just want a single-colored icon, which is often the case. If you need multi-colored icons, you can overlay elements and apply separate background-colors and clip-paths to each. Animating the icon’s color is then as simple as animating its background-color.
2. At the time of writing, Firefox was the only browser that Can I Use listed as fully supporting the feature. Other browsers had the following limitation: “Partial support refers to supporting shapes and the url(#foo) syntax for inline SVG, but not shapes in external SVGs.”
3. But not to be confused with an “actual” CSS mask, which acts like a translucent film covering the background image. It’s possible for parts of the background image to show through only partially, which is not the case for clip-paths.
4. Except for 0; 0 is 0 is 0 regardless of the units.
5. These shapes don’t have to be <path>s; they can be any SVG shape with a closed boundary, which includes <path> (which are automatically closed in this context) but also <circle>, <rect>, <polygon>, etc., but not, for instance, <line>.
6. Which, among other things, demonstrates just how permissive (to put it charitably) the parsing of SVG path strings is. For instance,0046.4606395 means 0 0 46.4606395 and 0050 means 0 0 50. Of the seven numbers after A and a, three are boolean flags and must be written as integers (0 or 1), not floats (0.0 or 1.0).
7. Technically, nothing says that a <path> actually has to fit in its SVG’s viewBox. But parts of the SVG outside the viewBox won’t be rendered, so the <path> should fit.
8. The individual transformations in a transform are applied right to left, not left to right. This seems backwards until you consider transformations as left-multiplication of some element: translate(…​) * scale(…​) * element. Since we only know how to apply a transformation to an element, not another transformation, this must be parenthesized as translate(…​) * (scale(…​) * element), from which it follows that translate(…​) scale(…​) (we’ve dropped the explicit asterisk now) must represent a scale(…​) followed by a translate(…​). Naturally, this right-to-left reading follows the mathematical rules of composing linear transformations.
9. In practice, you almost never see an SVG whose upper left corner isn’t at (0, 0). But the spec allows it, so we have to support it.
10. Actually, that’s not true; there is transform-origin, which lets you specify an origin other than (0, 0) as the origin of the transform. However, it pretty much only makes scaling and rotating more convenient. Once you need to translate, all bets are off; you need the dimensions of the transformed element to know how to translate it, and then you’re back to the situation above. But, if you do just need to scale and rotate, transform-origin probably does make things simpler.
11. If you’re familiar with the <g> element, which is a simple “container element” that groups SVG elements together, you might be wondering why we didn’t put the circles in a <g> inside the <clipPath> and then apply the transform to the <g>. Surely this would be simpler? The short answer is that you “just can’t”the spec forbids placing <g> elements in a <clipPath>, period.
12. For one, they don’t send (0, 0) to itself.
13. Why six? Because a 3×3 matrix has nine entries, but its bottom row must be all 0s and then a 1.

This page was built using Antora with a theme forked from the default UI. Search is powered by Lunr.

The source code for this UI is licensed under the terms of the MPL-2.0 license.