Drawing Double Borders on SVG Shapes Without an Editor
When SVG shapes need a double border at runtime, CSS and SVG both lack a direct stroke-style: double equivalent. The available workarounds vary widely in complexity, shape support, and visual quality. Here is a breakdown of the practical options and their trade-offs.
Quick Wins: Bounding-Box CSS
If the target shape is a square or rectangle, CSS properties applied to the wrapping SVG element offer the shortest path.
outline
Two CSS declarations suffice using outline. This keeps a transparent background visible through the shape and produces smooth borders.
- Works only with rectangular shapes
- Minimal code
- Smooth border rendering
- Background remains transparent
box-shadow
A single CSS declaration with box-shadow achieves the effect, but each shape must live in its own SVG element. The background color must be hard-coded into the shadow value, which prevents transparency.
- Works only with rectangular shapes
- Minimal code
- Smooth border rendering
- No transparent background support
Limited Gradient Trick
SVG radial gradients can fake a double border, but only for circles. Applying the gradient directly to the stroke works, though the color stops need frequent repetition, making CSS variables worthwhile.
- Works only with circles
- Simple code
- Border smoothness is inconsistent
- No transparent background support
Filters for Every Shape
CSS drop-shadow()
Applying filter: drop-shadow() works for any shape type, provided each sits in its own SVG. Colors stay flexible via CSS variables with a single declaration. The main drawback is visible pixelation around the borders.
- Universal shape support
- Simple code
- Borders appear pixelated
- No transparent background support
SVG Filters
A custom SVG filter referenced through the shape's filter attribute is more flexible but requires three compositing passes: one for the outer stroke, one for a background flood fill, and one to redraw the original shape on top. Output quality improves on the CSS drop-shadow approach, yet the border edges still look jagged.
- Universal shape support
- Complex filter setup
- Borders appear pixelated
- No transparent background support
Reusing Shape Definitions
Two duplication strategies work across all shapes and render clean borders, with different code and transparency characteristics.
Transforms
Define the shape once in <defs>, then render two instances. The base instance carries the fill and the outer stroke. The overlay instance has no fill, a contrasting stroke, and is centered using a scale transform. To scale from the shape's center, translate by half of the negative viewBox width and height.
- Universal shape support
- Shapes defined once but instantiated twice
- Smooth border rendering
- Background remains transparent
The <use> Pattern
Doug Schepers' approach from the www-svg mailing list relies on two <use> references to a single definition. The first instance uses a thicker stroke for the outer ring. The second instance halves that stroke width, removes the fill, and sets a stroke matching the page background, carving a gap that reveals the original fill underneath.
- Universal shape support
- Shapes defined once but instantiated twice
- Smooth border rendering
- Depends on a known background color, so no transparency
Side-by-Side Comparison
The reference implementation below gathers all discussed techniques in one view for direct comparison.
| Solution | All shapes | Simple code | Smooth borders | Transparent background |
|---|---|---|---|---|
outline | 🙁 | ✅ | ✅ | ✅ |
box-shadow | 🙁 | ✅ | ✅ | 🙁 |
| SVG gradients | 🙁 | ✅ | 🙁 | 🙁 |
filter: drop-shadow() | ✅ | ✅ | 🙁 | 🙁 |
| SVG filters | ✅ | 🙁 | 🙁 | 🙁 |
| Reusing shapes: Tranforms | ✅ | 🙁 | ✅ | ✅ |
Reusing shapes:<use> | ✅ | 🙁 | ✅ | 🙁 |



