Mon, 21 Apr 2025 17:10:35 +0000
I enjoyed Trys Mudford’s explanation of making rounded triangular boxes. It was a very real-world client need, and I do tend to prefer reading about technical solutions to real problems over theoretical ones. This one was tricky because this particular shape doesn’t have a terribly obvious way to draw it on the web.

CSS’ clip-path
is useful, but the final rounding was done with an unintuitive feGaussianBlur
SVG filter. You could draw it all in SVG, but I think the %
values you get to use with clip-path
are a more natural fit to web content than pure SVG is. SVG just wasn’t born in a responsive web design world.
The thing is: SVG has a viewBox
which is a fixed coordinate system on which you draw things. The final SVG can be scaled and squished and stuff, but it’s all happening on this fixed grid.
I remember when trying to learn the <path d="">
syntax in SVG how it’s almost an entire language unto itself, with lots of different letters issues commands to a virtual pen. For example:
M 100,100
means “Pick up the pen and move it to the exact coordinates 100,100”m 100,100
means “Move the Pen 100 down and 100 right from wherever you currently are.”
That syntax for the d
attribute (also expressed with the path()
function) can be applied in CSS, but I always thought that was very weird. The numbers are “unitless” in SVG, and that makes sense because the numbers apply to that invisible fixed grid put in place by the viewBox
. But there is no viewBox
in regular web layout., so those unitless numbers are translated to px
, and px
also isn’t particularly responsive web design friendly.
This was my mind’s context when I saw the Safari 18.4 new features. One of them being a new shape()
function:
For complex graphical effects like clipping an image or video to a shape, authors often fall back on CSS masking so that they can ensure that the mask adapts to the size and aspect ratio of the content being masked. Using the
clip-path
property was problematic, because the only way to specify a complex shape was with thepath()
function, which takes an SVG-style path, and the values in these paths don’t have units; they are just treated as CSS pixels. It was impossible to specify a path that was responsive to the element being clipped.
Yes! I’m glad they get it. I felt like I was going crazy when I would talk about this issue and get met with blank stares.
Tyrs got so close with clip-path: polygon()
alone on those rounded arrow shapes. The %
values work nicely for random amounts of content inside (e.g. the “nose” should be at 50% of the height) and if the shape of the arrow needed to be maintained px
values could be mix-and-matched in there.
But the rounding was missing. There is no rounding with polygon()
.
Or so I thought? I was on the draft spec anyway looking at shape()
, which we’ll circle back to, but it does define the same round
keyword and provide geometric diagrams with expectations on how it’s implemented.
An optional <length> after a round keyword defines rounding for each vertex of the polygon.
There are no code examples, but I think it would look something like this:
/* might work one day? */
clip-path: polygon(0% 0% round 0%, 75% 0% round 10px, 100% 50% round 10px, 75% 100% round 10px, 0% 100% round 0%);
I’d say “draft specs are just… draft specs”, but stable Safari is shipping with stuff in this draft spec so I don’t know how all that works. I did test this syntax across the browsers and nothing supports it. If it did, Trys’ work would have been quite a bit easier. Although the examples in that post where a border follows the curved paths… that’s still hard. Maybe we need clip-path-border
?
There is precedent for rounding in “basic shape” functions already. The inset()
function has a round
keyword which produces a rounded rectangle (think a simple border-radius
). See this example, which actually does work.
But anyway: that new shape()
function. It looks like it is trying to replicate (the entire?) power of <path d="">
but do it with a more CSS friendly/native syntax. I’ll post the current syntax from the spec to help paint the picture it’s a whole new language (🫥):
<shape-command> = <move-command> | <line-command> | close |
<horizontal-line-command> | <vertical-line-command> |
<curve-command> | <smooth-command> | <arc-command>
<move-command> = move <command-end-point>
<line-command> = line <command-end-point>
<horizontal-line-command> = hline
[ to [ <length-percentage> | left | center | right | x-start | x-end ]
| by <length-percentage> ]
<vertical-line-command> = vline
[ to [ <length-percentage> | top | center | bottom | y-start | y-end ]
| by <length-percentage> ]
<curve-command> = curve
[ [ to <position> with <control-point> [ / <control-point> ]? ]
| [ by <coordinate-pair> with <relative-control-point> [ / <relative-control-point> ]? ] ]
<smooth-command> = smooth
[ [ to <position> [ with <control-point> ]? ]
| [ by <coordinate-pair> [ with <relative-control-point> ]? ] ]
<arc-command> = arc <command-end-point>
[ [ of <length-percentage>{1,2} ]
&& <arc-sweep>? && <arc-size>? && [rotate <angle>]? ]
<command-end-point> = [ to <position> | by <coordinate-pair> ]
<control-point> = [ <position> | <relative-control-point> ]
<relative-control-point> = <coordinate-pair> [ from [ start | end | origin ] ]?
<coordinate-pair> = <length-percentage>{2}
<arc-sweep> = cw | ccw
<arc-size> = large | small
So instead of somewhat obtuse single-letter commands in the path syntax, these have more understandable names. Here’s an example again from the spec that draws a speech bubble shape:
.bubble {
clip-path:
shape(
from 5px 0,
hline to calc(100% - 5px),
curve to right 5px with right top,
vline to calc(100% - 8px),
curve to calc(100% - 5px) calc(100% - 3px) with right calc(100% - 3px),
hline to 70%,
line by -2px 3px,
line by -2px -3px,
hline to 5px,
curve to left calc(100% - 8px) with left calc(100% - 3px),
vline to 5px,
curve to 5px top with left top
);
}
You can see the rounded corners being drawn there with literal curve
commands. I think it’s neat. So again Trys’ shapes could be drawn with this once it has more proper browser support. I love how with this syntax we can mix and match units, we could abstract them out with custom properties, we could animate them, they accept readable position keywords like “right”, we can use calc()
, and all this really nice native CSS stuff that path()
wasn’t able to give us. This is born in a responsive web design world.
Very nice win, web platform.
Recommended Comments