Let’s say someone asks you to add a double border to some random geometric SVG shapes. For some reason, you can’t use any graphic editor — they need to be generated at runtime — so you have to solve it with CSS or within the SVG syntax.
Your first question might be: Is there anything like
Well, the answer is not yet and it’s not that easy. But I’ll attempt it anyway to see what methods I can uncover. I’ll explore the possibilities of three different basic shapes: circle, rectangle, and polygon. Pointing the ones that can keep a transparent color in the middle of the two lines.stroke-style: double
in SVG?
Spoiler alert: all the results have their downsides, at least with CSS and SVG, but let me walk you through my intents.
The simple solutions
These don’t work with all shapes, but they are the easiest of the solutions.
outline
and box-shadow
The CSS properties outline
and box-shadow
only apply to the bounding box of the shape or SVG, and so both are great solutions only for squares and rectangles. They also allow flexible colors using custom properties.
It only takes two lines of CSS with outline
, plus it keeps the background color visible through the shape.
- 🙁 Solution only for one shape.
- ✅ Simple code
- ✅ Borders are smooth
- ✅ Transparent background
box-shadow
only needs one line of CSS, but we have to make sure that each shape has its own SVG as we can’t apply box-shadow
directly to the shapes. Another thing to consider is that we have to apply the color of the background in the declaration.
- 🙁 Solution only for one shape
- ✅ Simple code
- ✅ Borders are smooth
- 🙁 No transparent background
SVG gradients
SVG radial gradients only work on circles ☺️. We can directly apply the gradient on the stroke, but it’s better to use variables as we have to declare the colors many times in the code.
- 🙁 Solution only for one shape
- ✅ Simple code
- 🙁 Borders are smooth
- 🙁 No transparent background
Solutions for all shapes
These will work with all shapes, but the code could become bloated or complex.
filter:
drop-shadow()
Finally, one solution for all shapes! We must have each shape in its own <svg>
since the filter
won’t apply directly to the shapes. We are using one declaration in CSS and have flexible colors using variables. The downside? The borders don’t look very smooth.
- ✅ One solution for all shapes
- ✅ Simple code
- 🙁 Borders look pixelated
- 🙁 No transparent background
SVG filters
This is a very flexible solution. We can create a filter and add it to the shapes through SVG’s filter
attribute. The complicated part here is the filter itself. We’ll need three paintings, one for the outside border, one for the background flood, and the last one to paint the shape on the front. The result looks better than using drop-shadow
, but the borders are still pixelated.
- ✅ One solution for all shapes
- 🙁 Complex code
- 🙁 Borders look pixelated
- 🙁 No transparent background
Reusing shapes
There are a couple of possible options here.
Option 1: Transforms
This solution requires transforms. We place one figure over the other, where the main figure has a fill color and a stroke color, and the other figure has no fill, a red stroke, and is scaled and repositioned to the center. We defined our shapes on the <defs>
. The trick is to translate half of the viewBox
to the negative space so that, when we scale them, we can do it from the center of the figure.
- ✅ One solution for all shapes
- 🙁 Duplicated code
- ✅ Borders are smooth
- ✅ Transparent background
<use>
Option 2: I found a clever solution in the www-svg mailing list by Doug Schepers that uses SVG <use>
. Again, it requires defining the shapes once and referring to them twice using <use>. This time the main shape has a bigger stroke. The second shape has half the stroke of the main shape, no fill, and a stroke matching the background color.
- ✅ One solution for all shapes
- 🙁 Duplicated code
- ✅ Borders are smooth
- 🙁 No transparent background
Here are the full results!
Just so you have them all in one place. Let me know it you can think of other possible solutions!
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> | ✅ | 🙁 | ✅ | 🙁 |
There is another approach: “reuse with mask”. I believe it would get at least three green ticks by your criteria.
BTW, I disagree that the “Reusing shapes” approach is not “simple code”. Are you rating it complex because it uses the
<use>
element? You could always just repeat the element if<use>
is too confusing.I really like this approach, I tried with ‘mask’ and couldn’t get anywhere, now I see what I was missing :)
Mmm yes, I admit that “simple code” is quite subjective, I was thinking of the way you need to declare the shape first and then duplicating the use of ‘use’. Having only one shape in the SVG was, in a way, simpler to me.
I came here to the comments section to give the solution Paul LeBeau already posted — which I won’t now — and to point out a problem with the transformations method. It doesn’t actually work with all shapes. Examples follow. Try with a rectangle or ellipse that’s much wider than it is tall, and you’ll see the second outline is too close to the original on the top and bottom. More complicated shapes, like the letter P, a crescent, or the state of Massachusetts, are sure to have places where the second outline is on the wrong side of the original.
The fallacy that a shape’s outset is just a larger copy of that shape is fairly common, and I’ve seen it result in some bad-looking graphics (including road signs, sadly).
good point, I didn’t have those shapes in mind, the transforms option won’t work there. Thanks for pointing it out!