Gradients, masks and clip paths in icon SVGs
How SVG gradients, clip paths and masks work, what gradientUnits changes, and why they break when an icon is resized, recolored or inlined many times.
5 min read
Flat shapes almost never cause trouble in an icon set. Gradients, clip paths and masks do. They are defined in one place and referenced from another, they have their own coordinate rules, and they react badly to recoloring and repetition. Plenty of crypto logos use them, from coin highlights to multi-stop chain marks, so it pays to understand what each one does.
Linear gradients
A gradient is a paint, defined inside <defs> and applied with fill="url(#id)":
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<defs>
<linearGradient id="coin-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#e4e4e7"/>
<stop offset="1" stop-color="#52525b"/>
</linearGradient>
</defs>
<circle cx="12" cy="12" r="11" fill="url(#coin-fill)"/>
</svg>
x1 y1 and x2 y2 set the start and end of the gradient's direction. Here it runs from top (0) to bottom (1), so the coin is light at the top and dark at the bottom. Without those attributes, a linear gradient runs left to right. Each <stop> places a color at an offset between 0 and 1, and stop-opacity can fade it.
Radial gradients
Radial gradients spread out from a center:
<radialGradient id="glow" cx="0.35" cy="0.3" r="0.7">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.6"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
cx, cy and r define the outer circle. fx and fy move the focal point, where the first stop sits, which is how off-center highlights on a "shiny coin" are made. Layering a translucent radial gradient like this over a flat fill is a common way to add depth without changing the brand color underneath.
gradientUnits: the attribute behind most bugs
Those numbers between 0 and 1 are not coordinates on the viewBox. By default they are fractions of the bounding box of the shape being painted. That default is gradientUnits="objectBoundingBox". The alternative is userSpaceOnUse, where the numbers are ordinary coordinates in the drawing.
objectBoundingBox (default) |
userSpaceOnUse |
|
|---|---|---|
| Values mean | Fractions of the shape's bounding box | Coordinates in the viewBox |
| Gradient follows the shape when it moves | Yes | No, it stays put |
| Same gradient on two shapes | Each shape gets the full gradient | Each shape shows a different slice |
| Radial gradient on a wide shape | Stretched into an ellipse | Stays circular |
| Shape with zero width or height (a straight line) | Gradient not applied | Works |
| Typical source | Hand-written SVG | Many design tool exports |
Common bugs that follow from this:
- A shape moved during an edit loses its gradient look. Exports often use
userSpaceOnUsewith absolute coordinates likex1="3.2" y1="1" x2="20.8" y2="23". Move the shape and the gradient stays where it was. Move both or neither. - A horizontal stroke with a gradient disappears. Its bounding box has zero height, so an
objectBoundingBoxgradient cannot be computed. Switch touserSpaceOnUse. - A radial highlight looks squashed on a pill-shaped badge. The bounding box is wider than it is tall, so the gradient is scaled more in one direction. Use
userSpaceOnUse, or add agradientTransformto compensate.
Clip paths
A clip path cuts away everything outside a shape. Only the clip shape's geometry matters; its fill, stroke and color are ignored, and the edge is hard.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<defs>
<clipPath id="coin-shape">
<circle cx="12" cy="12" r="12"/>
</clipPath>
</defs>
<g clip-path="url(#coin-shape)">
<rect width="24" height="24" fill="#3f3f46"/>
<path d="M0 16h24v8H0z" fill="#a1a1aa"/>
</g>
</svg>
This draws square artwork and crops it to a circle, a handy way to fit a square mark into a round token container. Unlike gradients, clipPathUnits defaults to userSpaceOnUse, so clip coordinates are ordinary viewBox coordinates. If the clip shape has holes, clip-rule plays the role that fill-rule plays for fills.
Design tools add clip paths generously, often one per frame that simply matches the whole canvas. Those can be removed. A clip path smaller than the canvas is doing real work.
Masks
A mask is a soft version of a clip path. Instead of geometry, it uses brightness: by default, white areas of the mask keep the content visible, black areas hide it, and grays make it partly transparent.
The most common icon use is a cutout. Here a chain badge sits on the corner of a token icon, and a mask cuts a ring out of the token so the two stay visually separate on any background:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<mask id="badge-cutout">
<rect width="32" height="32" fill="#fff"/>
<circle cx="26" cy="26" r="7" fill="#000"/>
</mask>
</defs>
<circle cx="16" cy="16" r="16" fill="#52525b" mask="url(#badge-cutout)"/>
<circle cx="26" cy="26" r="5" fill="#d4d4d8"/>
</svg>
The token circle is hidden wherever the mask is black (a circle of radius 7), and the badge of radius 5 sits inside that hole, leaving a 2-unit gap. Because it is a real transparent gap rather than a ring painted in the page's background color, it works on light and dark surfaces alike. Network badges on token icons covers the design side of this pattern.
Setting mask-type="alpha" on the <mask> element makes it use opacity instead of brightness, which is easier to reason about when the mask content is colored.
Where things go wrong
Recoloring. Recoloring through a CSS mask, or any method that paints the icon's whole silhouette one color, turns gradients into a flat shape and fills in the subtle highlights. Overlapping shapes that were separated only by color merge into one blob. A good single-color version is a separate drawing, not a filter applied to the branded one; making a mono version of a colorful logo explains why. If you do want an inline icon's gradient to follow a theme, stop-color="currentColor" makes a stop take the CSS color of the icon.
Inlining many times. Every gradient, clip path and mask is wired up by ID. When the same or different icons are inlined into one HTML page, matching IDs cross-wire: one logo paints with another's gradient, or loses its clip when the first copy is hidden. That deserves its own article: SVG ID collisions.
Resizing. Scaling through the viewBox is safe; everything scales together. Trouble comes from non-uniform scaling (preserveAspectRatio="none" stretches radial gradients along with the shapes) and from very small sizes, where a subtle gradient becomes a muddy average color. At 16 px, check whether the gradient still reads at all, and consider a flat version.
Other renderers. Browsers handle all of this consistently, but slide software, email clients, some SVG-to-PNG converters and design tool importers vary in how well they support masks and complex gradients. After converting or importing a logo, compare it against the original in a browser.
Rendering cost. Masks, and especially filters, ask more of the renderer than flat fills. One icon is nothing; a grid of hundreds of masked icons animating on scroll can be noticeable on low-end phones. Test on real devices if your grid is large and animated.