Rounded triangular boxes in CSS/SVG
Posted on in WebIn the Motorway brand refresh, I had to build a lot of rounded rectangular & triangular shapes (I don’t know what they’re officially called), and got quite good and understanding the pro’s and con’s of each approach. Depending on your use-case, there’s a few possible options available to you.
The filled box
The ‘simpler’ use-case is the filled box. Bennett Feely’s Clippy continues to be the best tool for the job of generating clip-path
shapes, and even has ‘left/right point’ options. Clip-path gives us a pointy edge, but doesn’t give us rounded edges, or a particularly responsive solution out of the box - wider screens will have a more pronounced point than smaller screens when using percentages:
.box {
clip-path: polygon(0% 0%, 75% 0%, 100% 50%, 75% 100%, 0% 100%);
}
To improve the point, you can use calc
with clip-path
to determine where the point starts/ends. border-radius
gets us some roundedness, but only on the flat edge, not on the pointy edge:
.box {
--point-inset: calc(100% - 2rem);
clip-path: polygon(0% 0%, var(--point-inset) 0%, 100% 50%, var(--point-inset) 100%, 0% 100%);
border-radius: 1rem;
}
To properly round this out, we need to employ a lesser-used bit of SVG: <feGaussianBlur>
. There’s a wonderful article by Temani Afif that explains the technique. This snippet can be popped at the bottom of your page, and sits there as an invisible SVG.
<svg height="0" style="position:absolute; visibility:hidden" version="1.1" width="0" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="rounding-filter">
<feGaussianBlur in="SourceGraphic" result="blur" stdDeviation="5"></feGaussianBlur>
<feColorMatrix in="blur" mode="matrix" result="goo" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 25 -9"></feColorMatrix>
<feComposite in="SourceGraphic" in2="goo" operator="atop"></feComposite>
</filter>
</defs>
</svg>
We can then apply this filter to our box and it will round the corners of child elements. Change stdDeviation
to increase/decrease the amount of rounding.
.box {
filter: url(#rounding-filter);
}
.box > * {
--point-inset: calc(100% - 2rem);
clip-path: polygon(0% 0%, var(--point-inset) 0%, 100% 50%, var(--point-inset) 100%, 0% 100%);
}
filter: url()
will only apply to child elements including :before/:after
, so you need a bit more markup to make this work.
In addition, you’ll want to avoid putting any text/images that don’t want to be blurred into pulp as direct descendants of the box. This filter works (I believe) by blurring the child elements and the lopping off the blurred edges.
The outlined box
Outlined boxes are a bit more tricky. You’d think it would just be a case of swapping background for a border, but alas not. Again, because this filter works by blurring the edges of the component, all borders effectively disappear. To compound the problem, box-shadow: inset
, filter: drop-shadow()
also get blurred, and clip-path
and border
do not play nicely (subtle background added for effect):
The best solution I found was to employ an additional SVG element an append it to the right-side of the box (as demonstrated by the orange flashing line).
<div class="box-wrapper">
<div class="box"></div>
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" fill="none" viewBox="0 0 127 478">
<path stroke="var(--theme)" stroke-width="1" d="M0 477h11.33a15.5 15.5 0 0 0 14.17-9.22l98.67-222.51a15.5 15.5 0 0 0 0-12.56L25.51 10.22A15.5 15.5 0 0 0 11.34 1H0" vector-effect="non-scaling-stroke"/>
</svg>
</div>
.box-wrapper {
--theme: rebeccapurple;
display: grid;
grid-template-columns: auto 3rem;
}
.box {
border: 1px solid var(--theme);
border-inline-end: none;
border-radius: 0.5rem 0 0 0.5rem;
}
Firstly, the SVG needs to be pre-rounded for this to work. You also need to decide your stroke-width
beforehand to get this to work consisently:
Next, it’s essential that you use preserveAspectRatio="none"
on the SVG. This allows you to squash and squeeze the SVG into whatever shape you require.
Finally, you need to set vector-effect="non-scaling-stroke"
on the <path />
element. This ensures that the stroke width remains the consistent with the box border, regardless of the size & shape of the SVG. If you don’t do this, you’ll end up with a very thin line when the SVG is scaled down.
Closing note
You may be asking yourself, why not just set an SVG background image and have done with it? Well, if you want control over the corner rounding, triangle size, and most importantly, stay responsive to differing viewports and quantities of content, then this is probably the way to go. If the requirement is for a simple empty box, then an SVG directly in the DOM is a simpler bet.
Posted on in Web