Transforms, clipping and masking
Understand the transform list and its order, set a sane transform origin, and choose between clipPath and mask for the result you want.
The transform list is applied right to left
<svg viewBox="0 0 240 120" xmlns="http://www.w3.org/2000/svg">
<!-- Left: translate then rotate. The rotation happens around the translated
origin, so the square orbits. -->
<g transform="translate(60 60) rotate(30)">
<rect x="-20" y="-20" width="40" height="40" fill="#6d28d9" opacity="0.85"/>
</g>
<!-- Right: rotate then translate. The rotation is around the SVG origin
first, then the whole thing moves. -->
<g transform="rotate(30) translate(60 60)">
<rect x="-20" y="-20" width="40" height="40" fill="#0ea5e9" opacity="0.85"/>
</g>
</svg>- Transforms compose right to left, exactly like matrix multiplication. The rightmost transform is the innermost, applied to the shape first.
rotate(a)rotates about the origin of the current coordinate system.rotate(a cx cy)rotates about a point, which is usually what you want and saves a translate pair.skewXandskewYtake an angle in degrees. They shear the shape and are rarely what a design calls for.transformas a presentation attribute and as a CSS property behave the same way in modern browsers, but a CSStransformon an SVG element also supportstransform-origin, which is where the two diverge.
| Situation | Write | Why |
|---|---|---|
| Rotate a mark about its own centre | rotate(30 cx cy) | No translate pair needed |
| Move then rotate about the new centre | translate(x y) rotate(a) | Applied right to left |
| Scale a whole group | translate(...) scale(...) | Scaling also scales the coordinates |
| Mirror horizontally | scale(-1 1) | Then translate to bring it back into view |
| Rotate about the element's own box | transform-box: fill-box; transform-origin: center | CSS only |
| Animate a rotation | rotate(a cx cy) in the animation | Keeps the pivot stable |
transform-origin defaults to the centre of the element's bounding box for HTML, but for SVG elements it defaults to 0 0 of the SVG user space. Set transform-box: fill-box if you want the CSS behaviour you were expecting — this is the single most common reason an SVG animation spins around the wrong point.clipPath
<svg viewBox="0 0 240 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- A hard-edged mask. Anything outside is fully removed. -->
<clipPath id="circle-clip">
<circle cx="60" cy="60" r="45"/>
</clipPath>
<!-- clipPathUnits: userSpaceOnUse (default) or objectBoundingBox -->
<clipPath id="half-clip" clipPathUnits="objectBoundingBox">
<rect x="0" y="0" width="0.5" height="1"/>
</clipPath>
<!-- A clip can be a path, which allows any shape at all. -->
<clipPath id="blob-clip">
<path d="M120 15 C 170 15, 200 45, 195 75 C 190 105, 150 115, 120 105 C 80 95, 70 60, 80 40 C 88 24, 100 15, 120 15 Z"/>
</clipPath>
</defs>
<g clip-path="url(#circle-clip)">
<image href="/img/photo.jpg" x="15" y="15" width="90" height="90" preserveAspectRatio="xMidYMid slice"/>
</g>
<g clip-path="url(#blob-clip)">
<rect x="70" y="0" width="140" height="120" fill="#6d28d9"/>
</g>
</svg>| Attribute | Values | Meaning |
|---|---|---|
clipPathUnits | userSpaceOnUse (default) | Coordinates are in the user space of the element being clipped |
clipPathUnits | objectBoundingBox | Coordinates are 0-1 within the element's own bounding box |
clip-path | url(#id) | References the clipPath |
clip-rule | nonzero or evenodd | How overlapping subpaths in the clip are filled |
clipPath on a <use> | Any | The reference is resolved in the source tree |
/* CSS also exposes clip-path with shape functions, which is often simpler
than an SVG clipPath for a rectangle, circle, ellipse or polygon. */
.card-image {
clip-path: circle(50% at 50% 50%);
/* or */
clip-path: inset(0 0 20% 0 round 12px);
/* or */
clip-path: polygon(0 0, 100% 0, 100% 85%, 50% 100%, 0 85%);
}
/* Animated reveal: animate a shape function's inset. */
.reveal { clip-path: inset(0 100% 0 0); transition: clip-path .5s ease-out; }
.reveal.is-open { clip-path: inset(0 0 0 0); }The practical split: use CSS clip-path shape functions for simple geometry because they are easier to write and animate, and use an SVG clipPath when the clip must be an arbitrary path, must be shared between several elements, or must clip content inside an SVG document.
mask and the difference from clipPath
<svg viewBox="0 0 240 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- A mask uses luminance (default) or alpha to decide visibility.
White = fully visible, black = fully hidden, grey = partial. -->
<linearGradient id="fade" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#000"/>
</linearGradient>
<mask id="gradient-mask" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="120">
<rect x="0" y="0" width="240" height="120" fill="url(#fade)"/>
</mask>
<!-- A soft radial mask: the classic vignette. -->
<radialGradient id="spot">
<stop offset="0.4" stop-color="#fff"/>
<stop offset="1" stop-color="#000"/>
</radialGradient>
<mask id="vignette">
<rect x="0" y="0" width="240" height="120" fill="url(#spot)"/>
</mask>
</defs>
<g mask="url(#gradient-mask)">
<rect x="20" y="20" width="200" height="80" fill="#6d28d9"/>
</g>
<g mask="url(#vignette)" transform="translate(0 0)">
<rect x="20" y="20" width="200" height="80" fill="#0ea5e9" opacity="0.6"/>
</g>
</svg>| Aspect | clipPath | mask |
|---|---|---|
| Edges | Hard | Soft or hard |
| Partial transparency | No | Yes, via grey or alpha |
| Gradients | Not applicable | The main use case |
| Rendering cost | Cheap | Expensive: an offscreen buffer per frame |
| Animation | Usually static | Commonly animated, at a cost |
| Support in exported images | Widely supported | Supported, but some converters ignore it |
| Default mask mode | — | luminance; mask-type: alpha switches it |
/* A CSS mask avoids the SVG document entirely for simple fades. */
.fade-out-bottom {
-webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent 100%);
mask-image: linear-gradient(to bottom, #000 60%, transparent 100%);
mask-size: 100% 100%;
mask-repeat: no-repeat;
}
/* Masking a sprite image to recolour it: the mask supplies the shape and the
background supplies the colour, which is a cheap way to theme an icon. */
.recoloured-icon {
width: 24px; height: 24px;
background-color: currentColor;
-webkit-mask: url(/icons/bell.svg) no-repeat center / contain;
mask: url(/icons/bell.svg) no-repeat center / contain;
}Both a mask and a clip force the browser to allocate an offscreen buffer and composite. On a static illustration the cost is invisible; on an animated element it is one of the most expensive things you can ask a compositor to do. If an animation stutters after adding a mask, that is the cause.
FAQ
Why does my rotation spin around the wrong point?
transform is being used with the default SVG transform-origin. Use rotate(angle cx cy) in an SVG transform attribute, or add transform-box: fill-box; transform-origin: center to the CSS rule.Should I use a mask or clipPath?
mask when you need a gradient, a soft edge or partial transparency. Reach for the CSS equivalents first for simple geometry on HTML elements.Related
Filters and visual effects Paths in depth: commands, curves and arcs
Last refreshed 2026-09-18.