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.
  • skewX and skewY take an angle in degrees. They shear the shape and are rarely what a design calls for.
  • transform as a presentation attribute and as a CSS property behave the same way in modern browsers, but a CSS transform on an SVG element also supports transform-origin, which is where the two diverge.
SituationWriteWhy
Rotate a mark about its own centrerotate(30 cx cy)No translate pair needed
Move then rotate about the new centretranslate(x y) rotate(a)Applied right to left
Scale a whole grouptranslate(...) scale(...)Scaling also scales the coordinates
Mirror horizontallyscale(-1 1)Then translate to bring it back into view
Rotate about the element's own boxtransform-box: fill-box; transform-origin: centerCSS only
Animate a rotationrotate(a cx cy) in the animationKeeps the pivot stable
💡
In CSS, 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>
AttributeValuesMeaning
clipPathUnitsuserSpaceOnUse (default)Coordinates are in the user space of the element being clipped
clipPathUnitsobjectBoundingBoxCoordinates are 0-1 within the element's own bounding box
clip-pathurl(#id)References the clipPath
clip-rulenonzero or evenoddHow overlapping subpaths in the clip are filled
clipPath on a <use>AnyThe 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>
AspectclipPathmask
EdgesHardSoft or hard
Partial transparencyNoYes, via grey or alpha
GradientsNot applicableThe main use case
Rendering costCheapExpensive: an offscreen buffer per frame
AnimationUsually staticCommonly animated, at a cost
Support in exported imagesWidely supportedSupported, but some converters ignore it
Default mask modeluminance; 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?
Either the pivot is the SVG origin rather than the element, or a CSS 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?
clipPath when the shape has hard edges — cheaper, simpler and better supported. A mask when you need a gradient, a soft edge or partial transparency. Reach for the CSS equivalents first for simple geometry on HTML elements.

Filters and visual effects Paths in depth: commands, curves and arcs

Last refreshed 2026-09-18.