Ready for the second part? We are still exploring the shape() function, and more precisely, the arc command. I hope you took the time to digest the first part because we will jump straight into creating more shapes!
As a reminder, the shape() function is only supported in Chrome 137+ and Safari 18.4+ as I’m writing this in May 2025.
Sector shape
Another classic shape that can also be used in pie-like charts.
It’s already clear that we have one arc. As for the points, we have two points that don’t move and one that moves depending on how much the sector is filled.
The code will look like this:
.sector {
--v: 35; /* [0 100]*/
aspect-ratio: 1;
clip-path: shape(from top, arc to X Y of R, line to center);
}
We define a variable that will control the filling of the sector. It has a value between 0 and 100. To draw the shape, we start from the top, create an arc until the point (X, Y), and then we move to the center.
Are we allowed to use keyword values like top and center?
Yes! Unlike the polygon() function, we have keywords for the particular cases such as top, bottom, left, etc. It’s exactly like background-position that way. I don’t think I need to detail this part as it’s trivial, but it’s good to know because it can make your shape a bit easier to read.
The radius of the arc should be equal to 50%. We are working with a square element and the sector, which is a portion of a circle, need to fill the whole element so the radius is equal to half the width (or height).1
As for the point, it’s placed within that circle, and its position depends on the V value. You don’t want a boring math explanation, right? No need for it, here is the formula of X and Y:
X = 50% + 50% * sin(V * 3.6deg)
Y = 50% - 50% * cos(V * 3.6deg)
Our code becomes:
.sector {
--v: 35; /* [0 100] */
aspect-ratio: 1;
clip-path: shape(from top,
arc to calc(50% + 50% * sin(var(--v) * 3.6deg))
calc(50% - 50% * cos(var(--v) * 3.6deg)) of 50%,
line to center);
}
Hmm, the result is not good, but there are no mistakes in the code. Can you figure out what we are missing?
It’s the size and direction of the arc!
Remember what I told you in the last article? You will always have trouble with them, but if we try the different combinations, we can easily fix the issue. In our case, we need to use: small cw.
Better! Let’s try it with more values and see how the shape behaves:
Oops, some values are good, but others not so much. The direction needs to be clockwise, but maybe we should use large instead of small? Let’s try:
Still not working. The issue here is that we are moving one point of the arc based on the V value, and this movement creates a different configuration for the arc command.
Here is an interactive demo to better visualize what is happening:
When you update the value, notice how large cw always tries to follow the largest arc between the points, while small cw tries to follow the smallest one. When the value is smaller than 50, small cw gives us a good result. But when it’s bigger than 50, the large cw combination is the good one.
I know, it’s a bit tricky and I wanted to study this particular example to emphasize the fact that we can have a lot of headaches working with arcs. But the more issues we face, the better we get at fixing them.
The solution in this case is pretty simple. We keep the use of large cw and add a border-radius to the element. If you check the previous demo, you will notice that even if large cw is not producing a good result, it’s filling the area we want. All we need to do is clip the extra space and a simple border-radius: 50% will do the job!
I am keeping the box-shadow in there so we can see the arc, but we can clearly see how border-radius is making a difference on the main shape.
There is still one edge case we need to consider. When the value is equal to 100, both points of the arc will have the same coordinates, which is logical since the sector is full and we have a circle. But when it’s the case, the arc will do nothing by definition and we won’t get a full circle.
To fix this, we can limit the value to, for example, 99.99 to avoid reaching 100. It’s kind of hacky, but it does the job.
.sector {
--v: 35; /* [0 100]*/
--_v: min(99.99, var(--v));
aspect-ratio: 1;
clip-path: shape(from top,
arc to calc(50% + 50% * sin(var(--_v) * 3.6deg))
calc(50% - 50% * cos(var(--_v) * 3.6deg)) of 50% large cw,
line to center);
border-radius: 50%;
}
Now our shape is perfect! And don’t forget that you can apply it to image elements:
Arc shape
Similar to the sector shape, we can also create an arc shape. After all, we are working with the arc command, so we have to do it.
We already have half the code since it’s basically a sector shape without the inner part. We simply need to add more commands to cut the inner part.
.arc {
--v: 35;
--b: 30px;
--_v: min(99.99, var(--v));
aspect-ratio: 1;
clip-path: shape(from top,
arc to calc(50% + 50% * sin(var(--_v) * 3.6deg))
calc(50% - 50% * cos(var(--_v) * 3.6deg)) of 50% cw large,
line to calc(50% + (50% - var(--b)) * sin(var(--_v) * 3.6deg))
calc(50% - (50% - var(--b)) * cos(var(--_v) * 3.6deg)),
arc to 50% var(--b) of calc(50% - var(--b)) large
);
border-radius: 50%;
}
From the sector shape, we remove the line to center piece and replace it with another line command that moves to a point placed on the inner circle. If you compare its coordinates with the previous point, you will see an offset equal to --b, which is a variable that defines the arc’s thickness. Then we draw an arc in the opposite direction (ccw) until the point 50% var(--b), which is also a point with an offset equal to --b from the top.
I am not defining the direction of the second arc since, by default, the browser will use ccw.
Ah, the same issue we hit with the sector shape is striking again! Not all the values are giving a good result due to the same logic we saw earlier, and, as you can see, border-radius is not fixing it. This time, we need to find a way to conditionally change the size of the arc based on the value. It should be large when V is bigger than 50, and small otherwise.
Conditions in CSS? Yes, it’s possible! First, let’s convert the V value like this:
--_f: round(down, var(--_v), 50)
The value is within the range [0 99.99] (don’t forget that we don’t want to reach the value 100). We use round() to make sure it’s always equal to a multiple of a specific value, which is 50 in our case. If the value is smaller than 50, the result is 0, otherwise it’s 50.
There are only two possible values, so we can easily add a condition. If --_f is equal to 0 we use small; otherwise, we use large:
I know what you are thinking, but let me tell you that the above code is valid. You probably don’t know it yet, but CSS has recently introduced inline conditionals using anif() syntax. It’s still early to play with it, but we have found a perfect use case for it. Here is a demo that you can test using Chrome Canary:
Another way to express conditions is to rely on style queries that have better support:
The logic is the same but, this feature requires a parent-child relation, which is why I am using a pseudo-element. By default, the size will be large, and if the value of --_f is equal to 0, we switch to small.
Note that we have to register the variable --_f using @property to be able to either use the if() function or style queries.
Did you notice another subtle change I have made to the shape? I removed border-radius and I applied the conditional logic to the first arc. Both have the same issue, but border-radius can fix only one of them while the conditional logic can fix both, so we can optimize the code a little.
Arc shape with rounded edges
What about adding rounded edges to our arc? It’s better, right?
Can you see how it’s done? Take it as a small exercise and update the code from the previous examples to add those rounded edges. I hope you are able to find it by yourself because the changes are pretty straightforward — we update one line command with an arc command and we add another arc command at the end.
clip-path: shape(from top,
arc to calc(50% + 50% * sin(var(--_v) * 3.6deg))
calc(50% - 50% * cos(var(--_v) * 3.6deg)) of 50% cw var(--_c, large),
arc to calc(50% + (50% - var(--b)) * sin(var(--_v) * 3.6deg))
calc(50% - (50% - var(--b)) * cos(var(--_v) * 3.6deg)) of 1% cw,
arc to 50% var(--b) of calc(50% - var(--b)) var(--_c, large),
arc to top of 1% cw
);
If you do not understand the changes, get out a pen and paper, then draw the shape to better see the four arcs we are drawing. Previously, we had two arcs and two lines, but now we are working with arcs instead of lines.
And did you remember the trick of using a 1% value for the radius? The new arcs are half circles, so we can rely on that trick where you specify a tiny radius and the browser will do the job for you and find the correct value!
Conclusion
We are done — enough about the arc command! I had to write two articles that focus on this command because it’s the trickiest one, but I hope it’s now clear how to use it and how to handle the direction and size thing, as that is probably the source of most headaches.
By the way, I have only studied the case of circular arcs because, in reality, we can specify two radii and draw elliptical ones, which is even more complex. Unless you want to become a shape() master, you will rarely need elliptical arcs, so don’t bother yourself with them.
Until the next article, I wrote an article for Frontend Masters where you can create more fancy shapes using the arc command that is a good follow-up to this one.
Footnotes
(1) The arc command is defined to draw elliptical arcs by taking two radii, but if we define one radius value, it means that the vertical and horizontal radius will use that same value and we have circular arcs. When it’s a length, it’s trivial, but when we use percentages, the value will resolve against the direction-agnostic size, which is equal to the length of the diagonal of the box, divided by sqrt(2).
In our case, we have a square element so 50% of the direction-agnostic size will be equal to 50% of sqrt(Width² + Height²)/sqrt(2). And since both width and height are equal, we end with 50% of the width (or the height). ⮑
The reading-flow and reading-order proposed CSS properties are designed to specify the source order of HTML elements in the DOM tree, or in simpler terms, how accessibility tools deduce the order of elements. You’d use them to make the focus order of focusable elements match the visual order, as outlined in the Web Content Accessibility Guidelines (WCAG 2.2).
To get a better idea, let’s just dive in!
(Oh, and make sure that you’re using Chrome 137 or higher.)
reading-flow
reading-flow determines the source order of HTML elements in a flex, grid, or block layout. Again, this is basically to help accessibility tools provide the correct focus order to users.
The default value is normal (so, reading-flow: normal). Other valid values include:
flex-visual
flex-flow
grid-rows
grid-columns
grid-order
source-order
Let’s start with the flex-visual value. Imagine a flex row with five links. Assuming that the reading direction is left-to-right (by the way, you can change the reading direction with the direction CSS property), that’d look something like this:
Now, if we apply flex-direction: row-reverse, the links are displayed 5-4-3-2-1. The problem though is that the focus order still starts from 1 (tab through them!), which is visually wrong for somebody that reads left-to-right.
But if we also apply reading-flow: flex-visual, the focus order also becomes 5-4-3-2-1, matching the visual order (which is an accessibility requirement!):
div {
display: flex;
flex-direction: row-reverse;
reading-flow: flex-visual;
}
To apply the default flex behavior, reading-flow: flex-flow is what you’re looking for. This is very akin to reading-flow: normal, except that the container remains a reading flow container, which is needed for reading-order (we’ll dive into this in a bit).
For now, let’s take a look at the grid-y values. In the grid below, the grid items are all jumbled up, and so the focus order is all over the place.
We can fix this in two ways. One way is that reading-flow: grid-rows will, as you’d expect, establish a row-by-row focus order:
Or, reading-flow: grid-columns will establish a column-by-column focus order:
reading-flow: grid-order will give us the default grid behavior (i.e., the jumbled up version). This is also very akin to reading-flow: normal (except that, again, the container remains a reading flow container, which is needed for reading-order).
There’s also reading-flow: source-order, which is for flex, grid, and block containers. It basically turns containers into reading flow containers, enabling us to use reading-order. To be frank, unless I’m missing something, this appears to make the flex-flow and grid-order values redundant?
reading-order
reading-order sort of does the same thing as reading-flow. The difference is that reading-order is for specific flex or grid items, or even elements in a simple block container. It works the same way as the order property, although I suppose we could also compare it to tabindex.
Note: To use reading-order, the container must have the reading-flow property set to anything other than normal.
I’ll demonstrate both reading-order and order at the same time. In the example below, we have another flex container where each flex item has the order property set to a different random number, making the order of the flex items random. Now, we’ve already established that we can use reading-flow to determine focus order regardless of visual order, but in the example below we’re using reading-order instead (in the exact same way as order):
div {
display: flex;
reading-flow: source-order; /* Anything but normal */
/* Features at the end because of the higher values */
a:nth-child(1) {
/* Visual order */
order: 567;
/* Focus order */
reading-order: 567;
}
a:nth-child(2) {
order: 456;
reading-order: 456;
}
a:nth-child(3) {
order: 345;
reading-order: 345;
}
a:nth-child(4) {
order: 234;
reading-order: 234;
}
/* Features at the beginning because of the lower values */
a:nth-child(5) {
order: -123;
reading-order: -123;
}
}
Yes, those are some rather odd numbers. I’ve done this to illustrate how the numbers don’t represent the position (e.g., order: 3 or reading-order: 3 doesn’t make it third in the order). Instead, elements with lower numbers are more towards the beginning of the order and elements with higher numbers are more towards the end. The default value is 0. Elements with the same value will be ordered by source order.
In practical terms? Consider the following example:
Of the five flex items, the first one is the one with order: -1 because it has the lowest order value. The last one is the one with order: 1 because it has the highestorder value. The ones with no declaration default to having order: 0 and are thus ordered in source order, but otherwise fit in-between the order: -1 and order: 1 flex items. And it’s the same concept for reading-order, which in the example above mirrors order.
However, when reversing the direction of flex items, keep in mind that order and reading-order work a little differently. For example, reading-order: -1 would, as expected, and pull a flex item to the beginning of the focus order. Meanwhile, order: -1 would pull it to the end of the visual order because the visual order is reversed (so we’d need to use order: 1 instead, even if that doesn’t seem right!):
div {
display: flex;
flex-direction: row-reverse;
reading-flow: source-order;
a:nth-child(5) {
/* Because of row-reverse, this actually makes it first */
order: 1;
/* However, this behavior doesn’t apply to reading-order */
reading-order: -1;
}
}
reading-order overrides reading-flow. If we, for example, apply reading-flow: flex-visual, reading-flow: grid-rows, or reading-flow: grid-columns (basically, any declaration that does in fact change the reading flow), reading-order overrides it. We could say that reading-order is applied after reading-flow.
What if I don’t want to use flexbox or grid layout?
Well, that obviously rules out all of the flex-y and grid-y reading-flow values; however, you can still set reading-flow: source-order on a block element and then manipulate the focus order with reading-order (as we did above).
How does this relate to the tabindex HTML attribute?
They’re not equivalent. Negative tabindex values make targets unfocusable and values other than 0 and -1 aren’t recommended, whereas a reading-order declaration can use any number as it’s only contextual to the reading flow container that contains it.
For the sake of being complete though, I did test reading-order and tabindex together and reading-order appeared to override tabindex.
Going forward, I’d only use tabindex (specifically, tabindex="-1") to prevent certain targets from being focusable (the disabled attribute will be more appropriate for some targets though), and then reading-order for everything else.
Closing thoughts
Being able to define reading order is useful, or at least it means that the order property can finally be used as intended. Up until now (or rather when all web browsers support reading-flow and reading-order, because they only work in Chrome 137+ at the moment), order hasn’t been useful because we haven’t been able to make the focus order match the visual order.
Even if I have detailed most of the modern techniques and tricks, CSS keeps evolving, and new stuff always emerges to simplify our developer life. Recently, clip-path was upgraded to have a new shape() value. A real game changer!
Before we jump in, it’s worth calling out that the shape() function is currently only supported in Chrome 137+ and Safari 18.4+ as I’m writing this in May 2025.
While the path() function allows reuse of the SVG path syntax to define more arbitrary shapes than allowed by more specialized shape functions, it requires writing a path as a single string (which is not compatible with, for example, building a path piecemeal with var()), and inherits a number of limitations from SVG, such as implicitly only allowing the px unit.
The shape() function uses a set of commands roughly equivalent to the ones used by path(), but does so with more standard CSS syntax, and allows the full range of CSS functionality, such as additional units and math functions.
In other words, we have the SVG features in the CSS side that we can combine with existing features such as var(), calc(), different units, etc. SVG is already good at drawing complex shapes, so imagine what is possible with something more powerful.
If you keep reading the spec, you will find:
In that sense, shape() is a superset of path(). A path() can be easily converted to a shape(), but to convert a shape() back to a path() or to SVG requires information about the CSS environment.
And guess what? I already created an online converter from SVG to CSS. Save this tool because it will be very handy. If you are already good at creating SVG shapes or you have existing codes, no need to reinvent the wheel. You paste your code in the generator, and you get the CSS code that you can easily tweak later.
Let’s try with the CSS-Tricks logo. Here is the SVG I picked from the website:
You take the value inside the d attribute, paste it in the converter, and boom! You have the following CSS:
.shape {
aspect-ratio: 0.933;
clip-path: shape(from 43.18% 61.52%,line by -24.35% 16.67%,curve by -8.12% 3.03% with -2.92% 1.82%/-5.2% 3.03%,curve by -10.71% -10.3% with -5.84% 0%/-10.71% -4.85%,curve to 7.47% 61.52% with 0% 66.36%/3.9% 63.03%,line by 28.57% -11.52%,line to 7.47% 38.18%,curve to 0% 28.79% with 3.59% 36.67%/0% 33.33%,curve to 11.03% 18.79% with 0% 23.33%/5.2% 18.79%,curve by 7.79% 3.03% with 2.92% 0%/4.87% 0.91%,line by 24.35% 16.67%,line to 39.93% 11.52%,curve to 50% 0% with 38.96% 5.15%/43.51% 0%,smooth by 10.07% 11.21% with 11.03% 4.85%,line to 56.81% 38.48%,line by 24.35% -16.67%,curve to 89.29% 18.79% with 84.09% 19.7%/86.36% 18.79%,arc by 10.71% 10% of 10.81% 10.09% small cw,curve by -7.47% 9.39% with 0% 4.85%/-3.57% 7.88%,line to 63.96% 50%,line to 92.53% 61.52%,curve by 7.47% 9.7% with 3.9% 1.51%/7.47% 4.85%,curve by -11.03% 10% with 0% 5.45%/-5.2% 10%,curve by -7.79% -3.03% with -2.6% 0%/-4.87% -1.21%,line to 56.81% 61.52%,line by 3.25% 26.97%,curve by -10.07% 11.52% with 0.97% 6.36%/-3.57% 11.52%,smooth by -10.07% -11.21% with -11.03% -4.85%,close);
}
Note that you don’t need to provide any viewBox data. The converter will automatically find the smallest rectangle for the shape and will calculate the coordinates of the points accordingly. No more viewBox headaches and no need to fight with overflow or extra spacing!
Here is another example where I am applying the shape to an image element. I am keeping the original SVG so you can compare both shapes.
When to use shape()
I would be tempted to say “all the time” but in reality, not. In my guide, I distinguish between two types of shapes: The ones with only straight lines and the ones with curves. Each type can either have repetition or not. In the end, we have four categories of shapes.
If we don’t have curves and we don’t have repetition (the easiest case), then clip-path: polygon() should do the job. If we have a repetition (with or without curves), then mask is the way to go. With mask, we can rely on gradients that can have a specific size and repeat, but with clip-path we don’t have such options.
If you have curves and don’t have a repetition, the new shape() is the best option. Previously, we had to rely on mask since clip-path is very limited, but that’s no longer the case. Of course, these are not universal rules, but my own way to identify which option is the most suitable. At the end of the day, it’s always a case-by-case basis as we may have other things to consider, such as the complexity of the code, the flexibility of the method, browser support, etc.
Let’s draw some shapes!
Enough talking, let’s move to the interesting part: drawing shapes. I will not write a tutorial to explain the “complex” syntax of shape(). It will be boring and not interesting. Instead, we will draw some common shapes and learn by practice!
Technically, this will do nothing since it will draw a rectangle that already follows the element shape which is a rectangle, but it’s still the perfect starting point for us.
Now, let’s write it using shape().
clip-path: shape(
from 0 0,
line to 100% 0,
line to 100% 100%,
line to 0 100%
);
The code should be self-explanatory and we already have two commands. The from command is always the first command and is used only once. It simply specifies the starting point. Then we have the line command that draws a line to the next point. Nothing complex so far.
We can still write it differently like below:
clip-path: shape(
from 0 0,
hline to 100%,
vline to 100%,
hline to 0
);
Between the points 0 0 and 100% 0, only the first value is changing which means we are drawing a horizontal line from 0 0 to 100% 0, hence the use of hline to 100% where you only need to specify the horizontal offset. It’s the same logic using vline where we draw a vertical line between 100% 0 and 100% 100%.
I won’t advise you to draw your shape using hline and vline because they can be tricky and are a bit difficult to read. Always start by using line and then if you want to optimize your code you can replace them with hline or vline when applicable.
We have our first shape and we know the commands to draw straight lines:
Circular Cut-Out
Now, let’s try to add a circular cut-out at the top of our shape:
For this, we are going to rely on the arc command, so let’s understand how it works.
If we have two points, A and B, there are exactly two circles with a given radius that intersect with both points like shown in the figure. The intersection gives us four possible arcs we can draw between points A and B. Each arc is defined by a size and a direction.
There is also the particular case where the radius is equal to half the distance between A and B. In this case, only two arcs can be drawn and the direction will decide which one.
The syntax will look like this:
clip-path: shape(
from Xa Ya,
arc to Xb Yb of R [large or small] [cw or ccw]
);
Let’s add this to our previous shape. No need to think about the values. Instead, let’s use random ones and see what happens:
clip-path: shape(
from 0 0,
arc to 40% 0 of 50px,
line to 100% 0,
line to 100% 100%,
line to 0 100%
);
Not bad, we can already see the arc between 0 0 and 40% 0. Notice how I didn’t define the size and direction of the arc. By default, the browser will use small and ccw.
Let’s explicitly define the size and direction to see the four different cases:
Hmm, it’s working for the first two blocks but not the other ones. Quite strange, right?
Actually, everything is working fine. The arcs are drawn outside the element area so nothing is visible. If you add some box-shadow, you can see them:
Arcs can be tricky due to the size and direction thing, so get ready to be confused. If that happens, remember that you have four different cases, and trying all of them will help you find which one you need.
Now let’s try to be accurate and draw half a circle with a specific radius placed at the center:
We can define the radius as a variable and use what we have learned so far:
.shape {
--r: 50px;
clip-path: shape(
from 0 0,
line to calc(50% - var(--r)) 0,
arc to calc(50% + var(--r)) 0 of var(--r),
line to 100% 0,
line to 100% 100%,
line to 0 100%
);
}
It’s working fine, but the code can still be optimized. We can replace all the line commands with hline and vline like below:
.shape {
--r: 50px;
clip-path: shape(from 0 0,
hline to calc(50% - var(--r)),
arc to calc(50% + var(--r)) 0 of var(--r),
hline to 100%,
vline to 100%,
hline to 0
);
}
We can also replace the radius with 1%:
.shape {
--r: 50px;
clip-path: shape(from 0 0,
hline to calc(50% - var(--r)),
arc to calc(50% + var(--r)) 0 of 1%,
hline to 100%,
vline to 100%,
hline to 0
);
}
When you define a small radius (smaller than half the distance between both points), no circle can meet the condition we explained earlier (an intersection with both points), so we cannot draw an arc. This case falls within an error handling where the browser will scale the radius until we can have a circle that meets the condition. Instead of considering this case as invalid, the browser will fix “our mistake” and draw an arc.
This error handling is pretty cool as it allows us to simplify our shape() function. Instead of specifying the exact radius, I simply put a small value and the browser will do the job for me. This trick only works when the arc we want to draw is half a circle. Don’t try to apply it with any arc command because it won’t always work.
Another optimization is to update the following:
arc to calc(50% + var(--r)) 0 of 1%,
…with this:
arc by calc(2 * var(--r)) 0 of 1%,
Almost all the commands can either use a to directive or a by directive. The first one defines absolute coordinates like the one we use with polygon(). It’s the exact position of the point we are moving to. The second defines relative coordinates which means we need to consider the previous point to identify the coordinates of the next point.
In our case, we are telling the arc to consider the previous point (50% - R) 0 and move by 2*R 0, so the final point will be (50% - R + 2R) (0 + 0), which is the same as (50% + R) 0.
.shape {
--r: 50px;
clip-path: shape(from 0 0,
hline to calc(50% - var(--r)),
arc by calc(2 * var(--r)) 0 of 1px,
hline to 100%,
vline to 100%,
hline to 0
);
}
This last optimization is great because if we want to move the cutout from the center, we only need to update one value: the 50%.
.shape {
--r: 50px;
--p: 40%;
clip-path: shape(
from 0 0,
hline to calc(var(--p) - var(--r)),
arc by calc(2 * var(--r)) 0 of 1px,
hline to 100%,
vline to 100%,
hline to 0
);
}
How would you adjust the above to have the cut-out at the bottom, left, or right? That’s your first homework assignment! Try to do it before moving to the next part.
I will give my implementation so that you can compare with yours, but don’t cheat! If you can do this without referring to my work, you will be able to do more complex shapes more easily.
Rounded Tab
Enough cut-out, let’s try to create a rounded tab:
Can you see the puzzle of this one? Similar to the previous shape, it’s a bunch of arc and line commands. Here is the code:
.shape {
--r: 26px;
clip-path: shape(
/* left part */
from 0 100%,
arc by var(--r) calc(-1 * var(--r)) of var(--r),
vline to var(--r),
arc by var(--r) calc(-1 * var(--r)) of var(--r) cw,
/* right part */
hline to calc(100% - 2 * var(--r)),
arc by var(--r) var(--r) of var(--r) cw,
vline to calc(100% - var(--r)),
arc by var(--r) var(--r) of var(--r)
);
}
It looks a bit scary, but if you follow it command by command, it becomes a lot clearer to see what’s happening. Here is a figure to help you visualize the left part of it.
All the arc commands are using the by directive because, in all the cases, I always need to move by an offset equal to R, meaning I don’t have to calculate the coordinates of the points. And don’t try to replace the radius by 1% because it won’t work since we are drawing a quarter of a circle rather than half of a circle.
From this, we can easily achieve the left and right variations:
Notice how I am only using two arc commands instead of three. One rounded corner can be done with a classic border radius, so this can help us simplify the shape.
Inverted Radius
One last shape, the classic inner curve at the corner also called an inverted radius. How many arc commands do we need for this one? Check the figure below and think about it.
If your answer is six, you have chosen the difficult way to do it. It’s logical to think about six arcs since we have six curves, but three of them can be done with a simple border radius, so only three arcs are needed. Always take the time to analyze the shape you are creating. Sometimes, basic CSS properties can help with creating the shape.
What are you waiting for? This is your next homework and I won’t help you with a figure this time. You have all that you need to easily create it. If you are struggling, give the article another read and try to study the previous shapes more in depth.
Here is my implementation of the four variations:
Conclusion
That’s all for this first part. You should have a good overview of the shape() function. We focused on the line and arc commands which are enough to create most of the common shapes.
Don’t forget to bookmark the SVG to CSS converter and keep an eye on my CSS Shape collection where you can find the code of all the shapes I create. And here is a last shape to end this article.
Clever, clever that Andy Bell. He shares a technique for displaying image alt text when the image fails to load. Well, more precisely, it’s a technique to apply styles to the alt when the image doesn’t load, offering a nice UI fallback for what would otherwise be a busted-looking error.
The recipe? First, make sure you’re using alt in the HTML. Then, a little JavaScript snippet that detects when an image fails to load:
Some of the more complex shapes were commonly clipped with the path() function. That makes a lot of sense because it literally accepts SVG path coordinates that you can draw in an app like Figma and export.
.shape {
clip-path: shape(
from 97.54% 10.91%,
curve by -10.93% -10.76% with -2.11% -5.38%/-6.13% -9.91%,
curve by -15.78% 7.98% with -5.83% -1.03%/-11.32% 3.26%,
curve by -14.36% 12.27% with -4.46% 4.71%/-8.72% 10.15%,
curve by -30.93% -4.53% with -10.05% 3.75%/-20.44% -4.47%,
curve to 7.15% 25.66% with 18.67% 15.81%/11.86% 19.43%,
curve by 19.9% 70.23% with -17.4% 23.09%/-0.05% 60.08%,
curve by 49.46% -9.07% with 16.08% 8.22%/35.34% 3.57%,
curve by 23.23% -53.55% with 13.43% -12.03%/21.71% -33.18%,
curve by 0.25% -4.77% with 0.1% -1.63%/0.2% -3.2%,
curve to 97.54% 10.91% with 100.09% 22.46%/99.64% 16.29%,
close
);
}
Pretty cool!
Honestly, I’m not sure how often I’ll need to convert path() to shape(). Seems like a stopgap sorta thing where the need for it dwindles over time as shape() is used more often — and it’s not like the existing path() function is broken or deprecated… it’s just different. But still, I’m using the generator a LOT as I try to wrap my head around shape() commands. Seeing the commands in context is invaluable which makes it an excellent learning tool.
A couple of days back, among the tens of crypto-scams that flood our contact inbox, we found an interesting question on nested lists from one of our readers.
I have a problem (related to list-numbering) that seems commonplace, but I can’t seem to solve it or find any solution for. If any of your geniuses can answer this, I’m sure there are going to be a lot of people interested.
Here’s the problem. It’s a routine numbering sequence, of different levels, found in (for example) [government], legislation, and in my case, condominium bylaws. I have five levels represented by the first number at each level of 1., (1), (a) (lower-alpha), (i) (lower-roman), (A) (upper-alpha). Of course, I have 5 levels here, but if you could demonstrate a solution for 3 levels.
Fair enough! So, what we are looking to achieve is a nested list, where each sublist marker/counter is of a different kind. The example linked in the message is the following:
8 The strata corporation must repair and maintain all of the following:
(a) common assets of the strata corporation;
(b) common property that has not been designated as limited common property;
(c) limited common property, but the duty to repair and maintain it is restricted to
(i) repair and maintenance that in the ordinary course of events occurs less often than once a year, and
(ii) the following, no matter how often the repair or maintenance ordinarily occurs:
(A) the structure of a building;
(B) the exterior of a building;
(C) chimneys, stairs, balconies and other things attached to the exterior of a building;
(D) doors, windows and skylights on the exterior of a building or that front on the common property;
While simple at first glance, it still has some nuance, so let’s try to come up with the most maintainable solution here.
The ugly way
My first approach to this problem was no approach at all; I just opened CodePen, wrote up the HTML, and tried to get my CSS to work towards the final result. After translating the Markdown into ol and li elements, and with no special styling on each list, the base list would look like the following:
Once there, my first instinct was to select each ol element and then change its list-style-type to the desired one. To target each level, I selected each ol depending on its number of ol ancestors, then let the specificity handle the rest:
ol {
list-style-type: decimal; /* Unnecessary; just for demo */
}
ol ol {
list-style-type: lower-alpha;
}
ol ol ol {
list-style-type: lower-roman;
}
ol ol ol ol {
list-style-type: upper-alpha;
}
And as you can see, this works… But we can agree it’s an ugly way to go about it.
Nesting to the rescue
Luckily, CSS nesting has been baseline for a couple of years now, so we could save ourselves a lot of ol selectors by just nesting each element inside the next one.
ol {
list-style-type: decimal;
ol {
list-style-type: lower-alpha;
ol {
list-style-type: lower-roman;
ol {
list-style-type: upper-alpha;
}
}
}
}
While too much nesting is usually frowned upon, I think that, for this case in particular, it makes the CSS clearer on what it intends to do — especially since the CSS structure matches the HTML itself, and it also keeps all the list styles in one place. All to the same result:
It’s legal
I don’t know anything about legal documents, nor do I intend to learn about them. However, I do know the law, and by extension, lawyers are finicky about how they are formatted because of legal technicalities and whatnot. The point is that for a legal document, those parentheses surrounding each list marker — like (A) or (ii) — are more than mere decoration and have to be included in our lists, which our current solution doesn’t.
A couple of years back, we would have needed to set a counter for each list and then include the parentheses along the counter() output; repetitive and ugly. Nowadays, we can use the @counter-style at rule, which as its name implies, allows us to create custom counter styles that can be used (among other places) in the list-style-type property.
In case you’re unfamiliar with the @counter-style syntax, what we need to know is that it can be used to extend predefined counter styles (like decimal or upper-alpha), and attach to them a different suffix or prefix. For example, the following counter style extends the common decimal style and adds a dash (-) as a prefix and a colon (:) as a suffix.
And then, we just gotta replace each with its equivalent in our initial ol declarations:
ol {
list-style-type: trimmed-decimal;
ol {
list-style-type: enclosed-lower-alpha;
ol {
list-style-type: enclosed-lower-roman;
ol {
list-style-type: enclosed-upper-alpha;
}
}
}
}
It should work without CSS!
Remember, though, it’s a legal document, so what happens if the internet is weak enough so that only the HTML loads correctly, or if someone checks the page from an old browser that doesn’t support nesting or @counter-style?
Thinking only about the list, in most websites, it would be a mild annoyance where the markers go back to decimal, and you have to go by padding to know where each sublist starts. However, in a legal document, it can be a big deal. How big? I am no lawyer, so it beats me, but we still can make sure the list keeps its original numbering even without CSS.
For the task, we can use the HTML type attribute. It’s similar to CSS list-style-type but with its own limited uses. First, its use with ul elements is deprecated, while it can be used in ol elements to keep the lists correctly numbered even without CSS, like in legal or technical documents such as ours. It has the following values:
"1" for decimal numbers (default)
"a" for lowercase alphabetic
"A" for uppercase alphabetic
"i" for lowercase Roman numbers
"I" for uppercase Roman numbers
Inside our HTML list, we would assign the correct numbering for each ol level:
Depending on how long the document is, it may be more the hassle than the benefit, but it is still good to know. Although this kind of document doesn’t change constantly, so it wouldn’t hurt to add this extra safety net.
Welp, that was kinda too much for a list! But that’s something intrinsic to legal documents. Still, I think it’s the simplest way to achieve the initial reader’s goal. Let me know in the comments if you think this is overengineered or if there is an easier way.