Jump to content

Welcome to CodeNameJessica

Welcome to CodeNameJessica!

💻 Where tech meets community.

Hello, Guest! 👋
You're just a few clicks away from joining an exclusive space for tech enthusiasts, problem-solvers, and lifelong learners like you.

🔐 Why Join?
By becoming a member of CodeNameJessica, you’ll get access to:
In-depth discussions on Linux, Security, Server Administration, Programming, and more
Exclusive resources, tools, and scripts for IT professionals
A supportive community of like-minded individuals to share ideas, solve problems, and learn together
Project showcases, guides, and tutorials from our members
Personalized profiles and direct messaging to collaborate with other techies

🌐 Sign Up Now and Unlock Full Access!
As a guest, you're seeing just a glimpse of what we offer. Don't miss out on the complete experience! Create a free account today and start exploring everything CodeNameJessica has to offer.

  • Entries

    171
  • Comments

    0
  • Views

    2221

Entries in this blog

by: Temani Afif
Fri, 30 May 2025 13:45:43 +0000


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.

A series of three semi-circles.

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.

Diagram showing the fixed and variable lengths of an arc shape.

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.

A series of three circular rings at various lengths.

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.

Diagram showing the arc points of a semi-circle shape. There are two arcs, one on the outside and one on the inside. They are joined by straight lines.
.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:

.arc {
  --v: 35;
  --b: 30px;
  
  --_v: min(99.99, var(--v));
  --_f: round(down,var(--_v), 50);
  --_c: if(style(--_f: 0): small; else: large);
  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),
    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)) var(--_c)
  );
}

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 an if() 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:

.arc {
  --v: 35;
  --b: 30px;
  
  --_v: min(99.99, var(--v));
  --_f: round(down, var(--_v), 50);
  aspect-ratio: 1;
  container-name: arc;
}
.arc:before {
  content: "";
  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),
    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)) var(--_c, large)
  );
  @container style(--_f: 0) { --_c: small }
}

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?

A series of three semi-circles with rounded edges at varying lengths.

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.

Three shapes. The first looks like a flower. The second looks like a sun. The third looks like a blob.

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).


Better CSS Shapes Using shape() — Part 2: More on Arcs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Daniel Schwarz
Tue, 27 May 2025 13:02:32 +0000


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>
  <a>1</a>
  <a>2</a>
  <a>3</a>
  <a>4</a>
  <a>5</a>
</div>
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:

<div>
  <a>1</a>
  <a>2</a>
  <a>3</a>
  <a>4</a>
  <a>5</a>
  <a>6</a>
  <a>7</a>
  <a>8</a>
  <a>9</a>
  <a>10</a>
  <a>11</a>
  <a>12</a>
</div>
div {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 100px;
  reading-flow: grid-rows;
  
  a:nth-child(2) {
    grid-row: 2 / 4;
    grid-column: 3;
  }

  a:nth-child(5) {
    grid-row: 1 / 3;
    grid-column: 1 / 3;
  }
}

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:

div {
  display: flex;
  reading-flow: source-order;

  a:nth-child(1) {
    order: 1;
    reading-order: 1;
  }

  a:nth-child(5) {
    order: -1;
    reading-order: -1;
  }
}

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 highest order 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.


What We Know (So Far) About CSS Reading Order originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 26 May 2025 15:54:28 +0000


This is a great story from Dan North about “The Worst Programmer I know”, Tim MacKinnon. It’s a story about measuring developer performance with metrics:

We were working for a well-known software consultancy at a Big Bank that decided to introduce individual performance metrics, “for appraisal and personal development purposes”. This was cascaded through the organisation, and landed in our team in terms of story points delivered. This was after some considered discussion from the department manager, who knew you shouldn’t measure things like lines of code or bugs found, because people can easily game these.

Scared? Maybe you can guess. Tim was very bad at metrics.

Tim’s score was consistently zero. Zero! Not just low, or trending downwards, but literally zero. Week after week, iteration after iteration. Zero points for Tim.

Why? Maybe you can guess again. Tim wasn’t playing that game, he was a true senior developer in the sense that he nurtured his team.

… he would spend his day pairing with different teammates. With less experienced developers he would patiently let them drive whilst nudging them towards a solution. He would not crowd them or railroad them, but let them take the time to learn whilst carefully crafting moments of insight and learning

Tim wasn’t delivering software; Tim was delivering a team that was delivering software.

Every organization is different though. Mercifully in the situation above, Dan protected Tim. But we can all imagine a situation where Tim was fired because of this silly measurement system. (That always reminds me of Cathy O’Neils Weapons of Math Destruction). Getting to know how the organization works, so you can work within it, is another approach that Cindy Sridharan advocates for. See: How to become a more effective engineer.

You can either complain and pontificate on Twitter on how the tech industry *should* ideally work, or you can learn how your org *really* works and what’s rewarded, and optimize for that.

Different organizations will have different paths to these answers. I’ll pluck off a few bullet points:

  • exactly what technical skill you need to invest effort into getting better at, which will actually be rewarded
  • how to build lasting relationships with other people on your team or organization that ultimately dictate the success of a project
  • how to effectively pitch projects or improvements to leadership and actually see these through to completion
  • how to weigh the pros and cons of technical choices in the larger context of the organizational realities and needs
  • how to discern what’s achievable, and in precisely what time frame

Figure out who matters, what they care about, and how to effectively get things done. And don’t wait!

To build credibility, you need to demonstrate some impact early on, instead of waiting months to get the lie of the land before you start getting anything done. Chasing small wins and low-hanging fruit can be an easy path to productivity. Don’t underestimate their importance.

Another one I like in this realm of being a truly effective developer is Artem Zakirullin’s Cognitive load is what matters. A good developer can write code that themselves and others can read without being so complex that, well, you run out of mental capacity.

When reading code, you put things like values of variables, control flow logic and call sequences into your head. The average person can hold roughly four such chunks in working memory. Once the cognitive load reaches this threshold, it becomes much harder to understand things.

That tracks for me. I start tracing how code works, and I’m all good and have it in my head, then it feels like right at the fifth logical jump, my brain just dumps it all out and I’m lost.

I suspect it’s a talent of really great programmers that they can hold a bit more in their head, but it’s not smart to assume that of your fellow developers. And remember that even the very smart appreciate things that are very simple and clear, perhaps especially.

You know what strikes me as a smart developer move? When they work together even across organizations. It’s impressive to me to see Standard Schema an effort by all the people who work on any library that deals with JavaScript/TypeScript schemas to make them easier to use and implement.

The goal is to make it easier for ecosystem tools to accept user-defined type validators, without needing to write custom logic or adapters for each supported library. And since Standard Schema is a specification, they can do so with no additional runtime dependencies. Integrate once, validate anywhere.

There are heaps of libraries and tools that already support it, so I’d call that a big success. I see Zod released Mini recently, which uses functions instead of methods, making it tree-shakable, but otherwise works exactly the same. Likely a nod to Validbot which was always the “Zod but smaller” choice.

Another thing I think is smart: seeing what developers are already doing and making that thing better. Like, I’m sure there are very fancy exotic ways to debug JavaScript in powerful ways. But we all know most of us just console.log() stuff. So I like how Microsoft is like, let’s just make that better with console.context(). This allows for better filtering and styling of messages and such, which would surely be welcome. Might as well steal formatting strings from Node as well.

by: Temani Afif
Fri, 23 May 2025 13:02:32 +0000


Creating CSS Shapes is a classic and one of my favorite exercise. Indeed, I have one of the biggest collections of CSS Shapes from where you can easily copy the code of any shape. I also wrote an extensive guide on how to create them: The Modern Guide For Making CSS Shapes.

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.

What is shape()?

Let me quote the description from the official specification:

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:

<svg width="35px" height="35px" viewBox="0 0 362.62 388.52" >
  <path d="M156.58,239l-88.3,64.75c-10.59,7.06-18.84,11.77-29.43,11.77-21.19,0-38.85-18.84-38.85-40C0,257.83,14.13,244.88,27.08,239l103.6-44.74L27.08,148.34C13,142.46,0,129.51,0,111.85,0,90.66,18.84,73,40,73c10.6,0,17.66,3.53,28.25,11.77l88.3,64.75L144.81,44.74C141.28,20,157.76,0,181.31,0s40,18.84,36.5,43.56L206,149.52l88.3-64.75C304.93,76.53,313.17,73,323.77,73a39.2,39.2,0,0,1,38.85,38.85c0,18.84-12.95,30.61-27.08,36.5L231.93,194.26,335.54,239c14.13,5.88,27.08,18.83,27.08,37.67,0,21.19-18.84,38.85-40,38.85-9.42,0-17.66-4.71-28.26-11.77L206,239l11.77,104.78c3.53,24.72-12.95,44.74-36.5,44.74s-40-18.84-36.5-43.56Z"></path>
</svg>

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.

Two by two grid of shapes comparing those with and without curves and those with and without repetition.

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!

Rectangle

Take the following polygon:

clip-path: polygon(
  0 0,
  100% 0,
  100% 100%,
  0 100%
);

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:

A square shape with a scalloped half circle cut out of the top.

For this, we are going to rely on the arc command, so let’s understand how it works.

Diagram showing two intersecting circles with points indicating where they cross and arrows indicating the large and small clockwise and counterclockwise directions.

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.

A circle with two points at the top and bottom highlighted to show movement in the clockwise and counterclockwise directions.

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:

A rectangular shape with a scalloped half circle cut out of the top. Arrows indicate the circle's radius.

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:

A rectangular tab shape with rounded corners on the top and a flat, hard edge across the bottom. The words 'Rounded tab' are in white inside the rectangle.

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.

Diagram of the left side of a rounded rectangular tab, showing the rounded edge's radius and the arcs that are used to make 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.

A square with rounded edges and a a circlular arc cut out of the top-right corner of the shape.

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.


Better CSS Shapes Using shape() — Part 1: Lines and Arcs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Geoff Graham
Thu, 22 May 2025 14:43:09 +0000


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:

const images = document.querySelectorAll("img");

if (images) {
  images.forEach((image) => {
    image.onerror = () => {
      image.setAttribute("data-img-loading-error", "");
    };
  });
}

That slaps an attribute on the image — data-img-loading-error — that is selected in CSS:

img[data-img-loading-error] {
  --img-border-style: 0.25em solid
    color-mix(in srgb, currentColor, transparent 75%);
  --img-border-space: 1em;

  border-inline-start: var(--img-border-style);
  border-block-end: var(--img-border-style);
  padding-inline-start: var(--img-border-space);
  padding-block: var(--img-border-space);
  max-width: 42ch;
  margin-inline: auto;
}

And what you get is a lovely little presentation of the alt that looks a bit like a blockquote and is is only displayed when needed.

Andy does note, however, that Safari does not render alt text if it goes beyond a single line, which 🤷‍♂️.


You can style alt text like any other text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Geoff Graham
Wed, 21 May 2025 15:09:29 +0000


Shape master Temani Afif has what might be the largest collection of CSS shapes on the planet with all the tools to generate them on the fly. There’s a mix of clever techniques he’s typically used to make those shapes, many of which he’s covered here at CSS-Tricks over the years.

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.

But Temani has gone all-in on the newly-released shape() function which recently rolled out in both Chromium browsers and Safari. That includes a brand-new generator that converts path() shapes in shape() commands instead.

So, if we had a shape that was originally created with an SVG path, like this:

.shape {
  clip-path: path(
    M199.6,
    18.9c-4.3-8.9-12.5-16.4-22.3-17.8c-11.9-1.7-23.1,
    5.4-32.2,
    13.2c-9.1,
    7.8-17.8,
    16.8-29.3,
    20.3c-20.5,
    6.2-41.7-7.4-63.1-7.5c38.7,
    27,
    24.8,
    33,
    15.2,
    43.3c-35.5,
    38.2-0.1,
    99.4,
    40.6,
    116.2c32.8,
    13.6,
    72.1,
    5.9,
    100.9-15c27.4-19.9,
    44.3-54.9,
    47.4-88.6c0.2-2.7,
    0.4-5.3,
    0.5-7.9c204.8,
    38,
    203.9,
    27.8,
    199.6,
    18.9z
  );
}

…the generator will spit this out:

.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.


SVG to CSS Shape Converter originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 19 May 2025 16:36:15 +0000


I admit I’m a sucker for “do this; don’t do that” (can’t you read the sign) blog posts when it comes to design. Screw nuance, gimme answers. Anthony Hobday has a pretty good one in Visual design rules you can safely follow every time.

Two stacked frames illustrating depth perception: the left frame shows a darkening gradient as it approaches, while the right frame displays a lightening gradient.
Makes sense to me; ship it.

Erik Kennedy does a pretty good job with posts in this vein, and I just read one about designing sidebars in his email newsletter. But he didn’t blog it so I can’t link to it. Instead I’ll link to his 15 Tips for Better Signup / Login UX which is the same kinda “do this” advice.

A user interface mockup showing a 'Create account' form with labeled fields for 'Email' and 'Password'. An on-screen keyboard is displayed below, indicating that the email field should be set to type=email to trigger a specialized mobile keyboard.
I perish each time I have to hunt manually for the @

Jon Yablonski’s Laws of UX site is a pretty good example of this too, except the “do this” advice is more like “think about this principle”. They are pretty straightforward though, like:

An infographic about choice overload, featuring a grid of colored circles and a definition explaining the concept as the tendency to feel overwhelmed by too many options, also known as the paradox of choice.

Welp now that we’ve linked up a bunch of design related stuff I’d better keep going. My pockets are always hot with links. I’m like and old man except instead of Wether’s Originals I have great URLs.

If I had to design some shirts and hoodies and coats and stuff, I’d definitely want some clean templates to use, so I think these 45 fully editable apparel templates from atipo is pretty sweet. (€30.00 with a bunch of fonts too)

Not Boring software rules. They have some really nice apps that feel very designed. I use their Habits app every day. They’ve got a nice blog post on the role of sound in software design. It’s common to think that software that makes any sound is a mistake as it’s just obnoxious or superfluous. I like this one:

4. Shape a feeling. Sound can play a key role in queueing us in on how we should feel in a moment—happy, reflective, alert. In the todo app Clear, successively checking things off a list plays a rising set of notes that builds a sense of accomplishment.

Is “good” and “bad” web design subjective (like just our opinions) or objective (provable with data)? I mean, that’s subjective, lol. Remy Sharp was thinking about it recently and has developed his own set of criteria. A simple one:

Is the main content the main content item? Put another way, is the content hidden (or fighting) clutter on the page?

Seems simple, but… not always. I was reviewing a site recently and the homepage had just a zillion things it was trying to say. It was a store, so there were locations, an invite to search, an invite to call, and invite to chat, discounts available, a current promotion, financing available, categories for browsing, upcoming events, etc, etc. The thing is, it’s easy to point at it and say Mess! — all that stuff is there because it’s important to somebody in the organization. Deciding on what even “the content” is can be tricky. I always think the homepage probably isn’t the best place to start a big debate like this. Clean up the more focused pages first.

Let’s end with something beautiful, like these flowing WebGL gradients by Alex Harri. I loved the mathematical intro on doing all this pixel by pixel work manually, then how to shift the responsibility of that work:

But consider the amount of work that needs to be done. A 1,000✕300 canvas, for example, contains 300,000 pixels. That’s 300,000 invocations of our pixel function every frame — a ton of work for a CPU to perform 60 times a second! This is where WebGL comes in.

Shaders are a real journey, but I bet if you read every word of this post closely you’d be well on your way.

by: Juan Diego Rodríguez
Mon, 19 May 2025 12:32:22 +0000


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.

Styling lists? Enough to catch my attention. After all, I just completed an entire guide about CSS counters. The message continues:

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:

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.

@counter-style my-counter-style {
  system: extends decimal;
  prefix: "- ";
  suffix: ": ";
}

ol {
  list-style-type: my-counter-style;
}

In our case, we’ll need four counter styles:

  • A decimal marker, without the ending dot. The initial submission doesn’t make it clear if it’s with or without the dot, but let’s assume it’s without.
  • A lower alpha marker, enclosed in parentheses.
  • A lower Roman marker, also enclosed in parentheses.
  • An upper alpha marker, enclosed in parentheses as well.

All these would translate to the following @counter-style rules:

@counter-style trimmed-decimal {
  system: extends decimal;
  suffix: " ";
}

@counter-style enclosed-lower-alpha {
  system: extends lower-alpha;
  prefix: "(";
  suffix: ") ";
}

@counter-style enclosed-lower-roman {
  system: extends lower-roman;
  prefix: "(";
  suffix: ") ";
}

@counter-style enclosed-upper-alpha {
  system: extends upper-alpha;
  prefix: "(";
  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.

More on lists!


A Reader’s Question on Nested Lists originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Geoff Graham
Fri, 16 May 2025 14:38:19 +0000


Some weekend reading on the heels of Global Accessibility Awareness Day (GAADM), which took place yesterday. The Email Markup Consortium (EMC) released its 2025 study on the accessibility in HTML emails, and the TL;DR is not totally dissimilar from what we heard from WebAIM’s annual web report:

This is the third full year for this report and we are disappointed to see the same issues as we have in previous years. The top 10 issues haven’t changed order since last year, apart from the addition of color contrast, which can be put down to a change in the testing and reporting.

The results come from an analysis of 443,585 emails collected from the past year. According to EMC, only 21 emails passed all accessibility checks — and they were all written by the same author representing two different brands. And, further, that author represents one of the companies that not only sponsors the study, but develops the automated testing tool powering the analysis.

Automated testing is the key here. That’s needed for a project looking at hundreds of thousands of emails, but it won’t surface everything, as noted:

Email that pass our checks may still have accessibility issues that we cannot pick up through automated testing. For example, we check if an alt attribute is present on an image, but we do not check if the text is suitable for that image in the context of that message.

The most common issues relate to internationalization, like leaving out the lang (96% of emails) and dir (98% of emails) attributes. But you’ll be familiar with most of what rounds up the top 10, because it lines up with WebAIM’s findings:

  • Links must have discernible text
  • Element has insufficient color contrast
  • Images must have alternate text
  • Link text should be descriptive
  • Links must be distinguishable without relying on color

I appreciate that the report sheds a light on what accessibility features are supported by specific email clients, such as Gmail. The report outlines a set of 20 HTML, CSS, and ARIA features they look for and found that only one email client (SFR Mail?) of the 44 evaluated supports all of the features. Apple Mail and Samsung Email are apparently close behind, but the other 41? Not so much.

AilSo, yeah. Email has a ways to go, like a small microcosm of the web itself.


HTML Email Accessibility Report 2025 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Geoff Graham
Thu, 15 May 2025 12:30:59 +0000


I was reflecting on what I learned about CSS Carousels recently. There’s a lot they can do right out of the box (and some things they don’t) once you define a scroll container and hide the overflow.

Hey, isn’t there another fairly new CSS feature that works with scroll regions? Oh yes, that’s Scroll-Driven Animations. Shouldn’t that mean we can trigger an animation while scrolling through the items in a CSS carousel?

Why yes, that’s exactly what it means. At least in Chrome at the time I’m playing with this:

It’s as straightforward as you might expect: define your keyframes and apply them on the carousel items:

@keyframes foo {
  from {
    height: 0;
  }
  to {
    height: 100%;
    font-size: calc(2vw + 1em);
  }
}

.carousel li {
  animation: foo linear both;
  animation-timeline: scroll(inline);
}

There are more clever ways to animate these things of course. But what’s interesting to me is that this demo now combines CSS Carousels with Scroll-Driven Animations. The only rub is that the demo also slaps CSS Scroll Snapping in there with smooth scrolling, which is effectively wiped out when applying the scroll animation.

I thought I might work around that with a view() timeline instead. That certainly makes for a smoother animation that is applied to each carousel item as they scroll into view, but no dice on smooth scrolling.


Scroll-Driven Animations Inside a CSS Carousel originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: John Rhea
Wed, 14 May 2025 14:01:42 +0000


I recently rebuilt my portfolio (johnrhea.com). After days and days of troubleshooting and fixing little problems on my local laptop, I uploaded my shiny new portfolio to the server — and triumphantly watched it not work at all…

The browser parses and runs JavaScript, right? Maybe Chrome will handle something a little different from Firefox, but if the same code is on two different servers it should work the same in Chrome (or Firefox) no matter which server you look at, right? Right?

First, the dynamically generated stars wouldn’t appear and when you tried to play the game mode, it was just blank. No really terrible website enemies appeared, nor could they shoot any bad experience missiles at you, at least, not in the game mode, but I guess my buggy website literally sent a bad experience missile at you. Over on the page showing my work, little cars were supposed to zoom down the street, but they didn’t show up, either.

Let me tell you, there was no crying or tears of any kind. I was very strong and thrilled, just thrilled, to accept the challenge of figuring out what was going on. I frantically googled things like “What could cause JavaScript to act differently on two servers?”, “Why would a server change how JavaScript works?”, and “Why does everyone think I’m crying when I’m clearly not?” But to no avail.

There were some errors in the console, but not ones that made sense. I had an SVG element that we’ll call car (because that’s what I named it). I created it in vanilla JavaScript, added it to the page, and zoomed it down the gray strip approximating a street. (It’s a space theme where you can explore planets. It’s really cool. I swear.) I was setting transforms on car using car.style.transform and it was erroring out. car.style was undefined.

Uncaught TypeError: Cannot set properties of undefined.

I went back to my code on my laptop. Executes flawlessly. No errors.

To get past the initial error, I switched it from car.style to using setAttribute e.g. car.setAttribute('style', 'transform: translate(100px, 200px)');. This just got me to the next error. car was erroring out on some data-* attributes I was using to hold information about the car, e.g. car.dataset.xspeed would also come back undefined when I tried to access them. This latter technology has been supported in SVG elements since 2015, yet it was not working on the server, and worked fine locally. What the Hoobastank could be happening? (Yes, I’m referencing the 1990s band and, no, they have nothing to do with the issue. I just like saying… errr… writing… their name.)

With search engines not being much help (mostly because the problem isn’t supposed to exist), I contacted my host thinking maybe some kind of server configuration was the issue. The very polite tech tried to help, checking for server errors and other simple misconfigurations, but there were no issues he could find. After reluctantly serving as my coding therapist and listening to my (tearless) bemoaning of ever starting a career in web development, he basically said they support JavaScript, but can’t really go into custom code, so best of luck. Well, thanks for nothing, person whom I will call Truckson! (That’s not his real name, but I thought “Carson” was too on the nose.)

Next, and still without tears, I tried to explain my troubles to ChatGPT with the initial prompt: “Why would JavaScript on two different web servers act differently?” It was actually kind of helpful with a bunch of answers that turned out to be very wrong.

  • Maybe there was an inline SVG vs SVG in an img issue? That wasn’t it.
  • Could the browser be interpreting the page as plain text instead of HTML through some misconfiguration? Nope, it was pulling down HTML, and the headers were correct.
  • Maybe the browser is in quirks mode? It wasn’t.
  • Could the SVG element be created incorrectly? You can’t create an SVG element in HTML using document.createElement('svg') because SVG actually has a different namespace. Instead, you have to use document.createElementNS("http://www.w3.org/2000/svg", 'svg'); because SVG and HTML use similar, but very different, standards. Nope, I’d used the createElementNS function and the correct namespace.

Sidenote: At several points during the chat session, ChatGPT started replies with, “Ah, now we’re getting spicy 🔥” as well as, “Ah, this is a juicy one. 🍇” (emojis included). It also used the word “bulletproof” a few times in what felt like a tech-bro kind of way. Plus there was a “BOOM. 💥 That’s the smoking gun right there”, as well as an “Ahhh okay, sounds like there’s still a small gremlin in the works.” I can’t decide whether I find this awesome, annoying, horrible, or scary. Maybe all four?

Next, desperate, I gave our current/future robot overlord some of my code to give it context and show it that none of these were the issue. It still harped on the misconfiguration and kept having me output things to check if the car element was an SVG element. Again, locally it was an SVG element, but on the server it came back that it wasn’t.

  • Maybe using innerHTML to add some SVG elements to the car element garbled the car element into not being an SVG element? ChatGPT volunteered to rewrite a portion of code to fix this. I put the new code into my system. It worked locally! Then I uploaded it to the server and… no dice. Same error was still happening.

I wept openly. I mean… I swallowed my emotions in a totally healthy and very manly way. And that’s the end of the article, no redemption, no solution, no answer. Just a broken website and the loud sobs of a man who doesn’t cry… ever…

…You still here?

Okay, you’re right. You know I wouldn’t leave you hanging like that. After the non-existent sob session, I complained to ChatGPT, it again gave me some console logs including having the car element print out its namespace and that’s when the answer came to me. You see the namespace for an SVG is this:

http://www.w3.org/2000/svg

What it actually printed was this:

https://www.w3.org/2000/svg

One letter. That’s the difference.

Normally you want everything to be secure, but that’s not really how namespaces work. And while the differences between these two strings is minimal, I might as well have written document.createElementNS("Gimme-them-SVGzers", "svg");. Hey, W3C, can I be on the namespace committee?

But why was it different? You’d be really mad if you read this far and it was just a typo in my code. Right?

You’ve invested some time into this article, and I already did the fake-out of having no answer. So, having a code typo would probably lead to riots in the streets and hoards of bad reviews.

Don’t worry. The namespace was correct in my code, so where was that errant “s” coming from?

I remembered turning on a feature in my host’s optimization plugin: automatically fix insecure pages. It goes through and changes insecure links to secure ones. In 99% of cases, it’s the right choice. But apparently it also changes namespace URLs in JavaScript code.

I turned that feature off and suddenly I was traversing the galaxy, exploring planets with cars zooming down gray pseudo-elements, and firing lasers at really terrible websites instead of having a really terrible website. There were no tears (joyful or otherwise) nor were there celebratory and wildly embarrassing dance moves that followed.

Have a similar crazy troubleshooting issue? Have you solved an impossible problem? Let me know in the comments.


This Isn’t Supposed to Happen: Troubleshooting the Impossible originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 12 May 2025 17:00:57 +0000


Sometimes we gotta get into the unglamorous parts of CSS. I mean *I* think they are pretty glamorous: new syntax, new ideas, new code doing foundational and important things. I just mean things that don’t demo terribly well. Nothing is flying across the screen, anyway.

  • The Future of CSS: Construct <custom-ident> and <dashed-ident> values with ident() by Bramus Van Damme — When you go anchor-name: --name; the --name part is a custom property, right? No. It is a “custom ident”. It doesn’t have a value, it’s just a name. Things get more interesting with ident() as a function, which can help us craft them from other attributes and custom properties, making for much less repetitive code in some situations.
  • Beating !important user agent styles (sort of) by Noah Liebman — Using !important is a pretty hardcore way for a rule to apply, made even more hardcore when used by a low level stylesheet, of which user agent styles are the lowest. So is it even possible to beat a style set that way? Click to find out.
  • Here’s Why Your Anchor Positioning Isn’t Working by James Stuckey Weber — There is a whole host of reasons why including DOM positioning and order. If you ask Una she’ll say it’s probably the inset property.
  • Faux Containers in CSS Grids by Tyler Sticka — Elements that stick out of their “container” is a visually compelling look. A classic way to do it is with negative margins and absolute positioning and the like. But those things are a smidge “dangerous” in that they can cause overlaps and unexpected behavior due to being out of regular flow. I like Tyler’s idea here of keeping it all contained to a grid and just making it look like it’s breaking out.
  • Introducing @bramus/style-observer, a MutationObserver for CSS by Bramus Van Damme — A regular MutationObserver watches the DOM for changes. But not style changes. Bramus has created a version of it that does, thanks to a very newfangled CSS property that helps it work efficiently. I’m not overflowing with use case ideas, but I have a feeling that when you need it, you need it.
  • Using the upcoming CSS when/else rules by Christiana Uloma — There is a working draft spec for @when/@else so while these aren’t real right now, maybe they will be? The if() function seems more real and maybe that is enough here? The if() function would just be a value though not a whole block of stuff, so maybe we’ll get both.
by: Ryan Trimble
Mon, 12 May 2025 12:42:10 +0000


Friends, I’ve been on the hunt for a decent content management system for static sites for… well, about as long as we’ve all been calling them “static sites,” honestly.

I know, I know: there are a ton of content management system options available, and while I’ve tested several, none have really been the one, y’know? Weird pricing models, difficult customization, some even end up becoming a whole ‘nother thing to manage.

Also, I really enjoy building with site generators such as Astro or Eleventy, but pitching Markdown as the means of managing content is less-than-ideal for many “non-techie” folks.

A few expectations for content management systems might include:

  • Easy to use: The most important feature, why you might opt to use a content management system in the first place.
  • Minimal Requirements: Look, I’m just trying to update some HTML, I don’t want to think too much about database tables.
  • Collaboration: CMS tools work best when multiple contributors work together, contributors who probably don’t know Markdown or what GitHub is.
  • Customizable: No website is the same, so we’ll need to be able to make custom fields for different types of content.

Not a terribly long list of demands, I’d say; fairly reasonable, even. That’s why I was happy to discover Pages CMS.

According to its own home page, Pages CMS is the “The No-Hassle CMS for Static Site Generators,” and I’ll to attest to that. Pages CMS has largely been developed by a single developer, Ronan Berder, but is open source, and accepting pull requests over on GitHub.

Taking a lot of the “good parts” found in other CMS tools, and a single configuration file, Pages CMS combines things into a sleek user interface.

Pages CMS includes lots of options for customization, you can upload media, make editable files, and create entire collections of content. Also, content can have all sorts of different fields, check the docs for the full list of supported types, as well as completely custom fields.

There isn’t really a “back end” to worry about, as content is stored as flat files inside your git repository. Pages CMS provides folks the ability to manage the content within the repo, without needing to actually know how to use Git, and I think that’s neat.

User Authentication works two ways: contributors can log in using GitHub accounts, or contributors can be invited by email, where they’ll receive a password-less, “magic-link,” login URL. This is nice, as GitHub accounts are less common outside of the dev world, shocking, I know.

Oh, and Pages CMS has a very cheap barrier for entry, as it’s free to use.

Pages CMS and Astro content collections

I’ve created a repository on GitHub with Astro and Pages CMS using Astro’s default blog starter, and made it available publicly, so feel free to clone and follow along.

I’ve been a fan of Astro for a while, and Pages CMS works well alongside Astro’s content collection feature. Content collections make globs of data easily available throughout Astro, so you can hydrate content inside Astro pages. These globs of data can be from different sources, such as third-party APIs, but commonly as directories of Markdown files. Guess what Pages CMS is really good at? Managing directories of Markdown files!

Content collections are set up by a collections configuration file. Check out the src/content.config.ts file in the project, here we are defining a content collection named blog:

import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
// Load Markdown in the `src/content/blog/` directory.
loader: glob({ base: './src/content/blog', pattern: '**/*.md' }),
  // Type-check frontmatter using a schema
  schema: z.object({
    title: z.string(),
   description: z.string(),
    // Transform string to Date object
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
  }),
});

export const collections = { blog };

The blog content collection checks the /src/content/blog directory for files matching the **/*.md file type, the Markdown file format. The schema property is optional, however, Astro provides helpful type-checking functionality with Zod, ensuring data saved by Pages CMS works as expected in your Astro site.

Pages CMS Configuration

Alright, now that Astro knows where to look for blog content, let’s take a look at the Pages CMS configuration file, .pages.config.yml:

content:
  - name: blog
    label: Blog
    path: src/content/blog
    filename: '{year}-{month}-{day}-{fields.title}.md'
    type: collection
    view:
      fields: [heroImage, title, pubDate]
    fields:
      - name: title
        label: Title
        type: string

      - name: description
        label: Description
        type: text

      - name: pubDate
        label: Publication Date
        type: date
        options:
          format: MM/dd/yyyy

      - name: updatedDate
        label: Last Updated Date
        type: date
        options:
          format: MM/dd/yyyy

      - name: heroImage
        label: Hero Image
        type: image

      - name: body
        label: Body
        type: rich-text

  - name: site-settings
    label: Site Settings
    path: src/config/site.json
    type: file
    fields:
      - name: title
        label: Website title
        type: string

      - name: description
        label: Website description
        type: string
        description: Will be used for any page with no description.

      - name: url
        label: Website URL
        type: string
        pattern: ^(https?:\/\/)?(www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$

      - name: cover
        label: Preview image
        type: image
        description: Image used in the social preview on social networks (e.g. Facebook, Twitter...)

media:
  input: public/media
  output: /media

There is a lot going on in there, but inside the content section, let’s zoom in on the blog object.

- name: blog
  label: Blog
  path: src/content/blog
  filename: '{year}-{month}-{day}-{fields.title}.md'
  type: collection
  view:
    fields: [heroImage, title, pubDate]
  fields:
    - name: title
      label: Title
      type: string

    - name: description
      label: Description
      type: text

    - name: pubDate
      label: Publication Date
      type: date
      options:
        format: MM/dd/yyyy

    - name: updatedDate
      label: Last Updated Date
      type: date
      options:
        format: MM/dd/yyyy

    - name: heroImage
      label: Hero Image
      type: image

    - name: body
      label: Body
      type: rich-text

We can point Pages CMS to the directory we want to save Markdown files using the path property, matching it up to the /src/content/blog/ location Astro looks for content.

path: src/content/blog

For the filename we can provide a pattern template to use when Pages CMS saves the file to the content collection directory. In this case, it’s using the file date’s year, month, and day, as well as the blog item’s title, by using fields.title to reference the title field. The filename can be customized in many different ways, to fit your scenario.

filename: '{year}-{month}-{day}-{fields.title}.md'

The type property tells Pages CMS that this is a collection of files, rather than a single editable file (we’ll get to that in a moment).

type: collection

In our Astro content collection configuration, we define our blog collection with the expectation that the files will contain a few bits of meta data such as: title, description, pubDate, and a few more properties.

We can mirror those requirements in our Pages CMS blog collection as fields. Each field can be customized for the type of data you’re looking to collect. Here, I’ve matched these fields up with the default Markdown frontmatter found in the Astro blog starter.

fields:
  - name: title
    label: Title
    type: string

  - name: description
    label: Description
    type: text

  - name: pubDate
    label: Publication Date
    type: date
    options:
      format: MM/dd/yyyy

  - name: updatedDate
    label: Last Updated Date
    type: date
    options:
      format: MM/dd/yyyy

  - name: heroImage
    label: Hero Image
    type: image

  - name: body
    label: Body
    type: rich-text

Now, every time we create a new blog item in Pages CMS, we’ll be able to fill out each of these fields, matching the expected schema for Astro.

Aside from collections of content, Pages CMS also lets you manage editable files, which is useful for a variety of things: site wide variables, feature flags, or even editable navigations.

Take a look at the site-settings object, here we are setting the type as file, and the path includes the filename site.json.

- name: site-settings
  label: Site Settings
  path: src/config/site.json
  type: file
  fields:
    - name: title
      label: Website title
      type: string

    - name: description
      label: Website description
      type: string
      description: Will be used for any page with no description.

    - name: url
      label: Website URL
      type: string
      pattern: ^(https?:\/\/)?(www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$

    - name: cover
      label: Preview image
      type: image
      description: Image used in the social preview on social networks (e.g. Facebook, Twitter...)

The fields I’ve included are common site-wide settings, such as the site’s title, description, url, and cover image.

Speaking of images, we can tell Pages CMS where to store media such as images and video.

media:
  input: public/media
  output: /media

The input property explains where to store the files, in the /public/media directory within our project.

The output property is a helpful little feature that conveniently replaces the file path, specifically for tools that might require specific configuration. For example, Astro uses Vite under the hood, and Vite already knows about the public directory and complains if it’s included within file paths. Instead, we can set the output property so Pages CMS will only point image path locations starting at the inner /media directory instead.

To see what I mean, check out the test post in the src/content/blog/ folder:

---
title: 'Test Post'
description: 'Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.'
pubDate: 05/03/2025
heroImage: '/media/blog-placeholder-1.jpg'
---

The heroImage now property properly points to /media/... instead of /public/media/....

As far as configurations are concerned, Pages CMS can be as simple or as complex as necessary. You can add as many collections or editable files as needed, as well as customize the fields for each type of content. This gives you a lot of flexibility to create sites!

Connecting to Pages CMS

Now that we have our Astro site set up, and a .pages.config.yml file, we can connect our site to the Pages CMS online app. As the developer who controls the repository, browse to https://app.pagescms.org/ and sign in using your GitHub account.

You should be presented with some questions about permissions, you may need to choose between giving access to all repositories or specific ones. Personally, I chose to only give access to a single repository, which in this case is my astro-pages-cms-template repo.

granting permission to a single repository, called mrtrimble/astro-pages-cms-template

After providing access to the repo, head on back to the Pages CMS application, where you’ll see your project listed under the “Open a Project” headline.

text that reads "Open a project" with a list of projects, the astro pages cms template is visible at the top

Clicking the open link will take you into the website’s dashboard, where we’ll be able to make updates to our site.

Creating content

Taking a look at our site’s dashboard, we’ll see a navigation on the left side, with some familiar things.

Blog dashboard page, displaying the blog entries, there is only one at the moment, called Test Post. At the top, a button labeled "Add an entry"
  • Blog is the collection we set up inside the .pages.config.yml file, this will be where we we can add new entries to the blog.
  • Site Settings is the editable file we are using to make changes to site-wide variables.
  • Media is where our images and other content will live.
  • Settings is a spot where we’ll be able to edit our .pages.config.yml file directly.
  • Collaborators allows us to invite other folks to contribute content to the site.

We can create a new blog post by clicking the Add Entry button in the top right

create a new entry page, with fields for title, description publication date, last updated date, hero image, and body

Here we can fill out all the fields for our blog content, then hit the Save button.

After saving, Pages CMS will create the Markdown file, store the file in the proper directory, and automatically commit the changes to our repository. This is how Pages CMS helps us manage our content without needing to use git directly.

Automatically deploying

The only thing left to do is set up automated deployments through the service provider of your choice. Astro has integrations with providers like Netlify, Cloudflare Pages, and Vercel, but can be hosted anywhere you can run node applications.

Astro is typically very fast to build (thanks to Vite), so while site updates won’t be instant, they will still be fairly quick to deploy. If your site is set up to use Astro’s server-side rendering capabilities, rather than a completely static site, the changes might be much faster to deploy.

Wrapping up

Using a template as reference, we checked out how Astro content collections work alongside Pages CMS. We also learned how to connect our project repository to the Pages CMS app, and how to make content updates through the dashboard. Finally, if you are able, don’t forget to set up an automated deployment, so content publishes quickly.


Using Pages CMS for Static Site Content Management originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: John Rhea
Thu, 08 May 2025 12:33:29 +0000


I recently updated my portfolio at johnrhea.com. (If you’re looking to add a CSS or front-end engineer with storytelling and animation skills to your team, I’m your guy.) I liked the look of a series of planets I’d created for another personal project and decided to reuse them on my new site. Part of that was also reusing an animation I’d built circa 2019, where a moon orbited around the planet.

Initially, I just plopped the animations into the new site, only changing the units (em units to viewport units using some complicated math that I was very, very proud of) so that they would scale properly because I’m… efficient with my time. However, on mobile, the planet would move up a few pixels and down a few pixels as the moons orbited around it. I suspected the plopped-in animation was the culprit (it wasn’t, but at least I got some optimized animation and an article out of the deal).

Here’s the original animation:

My initial animation for the moon ran for 60 seconds. I’m folding it inside a disclosure widget because, at 141 lines, it’s stupid long (and, as we’ll see, emphasis on the stupid). Here it is in all its “glory”:

Open code
#moon1 {
  animation: moon-one 60s infinite;
}

@keyframes moon-one {
  0% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
  5% {
    transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
    z-index: 2;
    animation-timing-function: ease-out;
  }
  9.9% {
    z-index: 2;
  }
  10% {
    transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
    z-index: -1;
    animation-timing-function: ease-in;
  }
  15% {
    transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
    z-index: -1;
    animation-timing-function: ease-out;
  }
  19.9% {
    z-index: -1;
  }
  20% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
  25% {
    transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
    z-index: 2;
    animation-timing-function: ease-out;
  }
  29.9% {
    z-index: 2;
  }
  30% {
    transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
    z-index: -1;
    animation-timing-function: ease-in;
  }
  35% {
    transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
    z-index: -1;
    animation-timing-function: ease-out;
  }
  39.9% {
    z-index: -1;
  }
  40% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
  45% {
    transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
    z-index: 2;
    animation-timing-function: ease-out;
  }
  49.9% {
    z-index: 2;
  }
  50% {
    transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
    z-index: -1;
    animation-timing-function: ease-in;
  }
  55% {
    transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
    z-index: -1;
    animation-timing-function: ease-out;
  }
  59.9% {
    z-index: -1;
  }
  60% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
  65% {
    transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
    z-index: 2;
    animation-timing-function: ease-out;
  }
  69.9% {
    z-index: 2;
  }
  70% {
    transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
    z-index: -1;
    animation-timing-function: ease-in;
  }
  75% {
    transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
    z-index: -1;
    animation-timing-function: ease-out;
  }
  79.9% {
    z-index: -1;
  }
  80% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
  85% {
    transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
    z-index: 2;
    animation-timing-function: ease-out;
  }
  89.9% {
    z-index: 2;
  }
  90% {
    transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
    z-index: -1;
    animation-timing-function: ease-in;
  }
  95% {
    transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
    z-index: -1;
    animation-timing-function: ease-out;
  }
  99.9% {
    z-index: -1;
  }
  100% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
}

If you look at the keyframes in that code, you’ll notice that the 0% to 20% keyframes are exactly the same as 20% to 40% and so on up through 100%. Why I decided to repeat the keyframes five times infinitely instead of just repeating one set infinitely is a decision lost to antiquity, like six years ago in web time. We can also drop the duration to 12 seconds (one-fifth of sixty) if we were doing our due diligence.

I could thus delete everything from 20% on, instantly dropping the code down to 36 lines. And yes, I realize gains like this are unlikely to be possible on most sites, but this is the first step for optimizing things.

#moon1 {
  animation: moon-one 12s infinite;
}

@keyframes moon-one {
  0% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
  5% {
    transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
    z-index: 2;
    animation-timing-function: ease-out;
  }
  9.9% {
    z-index: 2;
  }
  10% {
    transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
    z-index: -1;
    animation-timing-function: ease-in;
  }
  15% {
    transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
    z-index: -1;
    animation-timing-function: ease-out;
  }
  19.9% {
    z-index: -1;
  }
  20% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
}

Now that we’ve gotten rid of 80% of the overwhelming bits, we can see that there are five main keyframes and two additional ones that set the z-index close to the middle and end of the animation (these prevent the moon from dropping behind the planet or popping out from behind the planet too early). We can change these five points from 0%, 5%, 10%, 15%, and 20% to 0%, 25%, 50%, 75%, and 100% (and since the 0% and the former 20% are the same, we can remove that one, too). Also, since the 10% keyframe above is switching to 50%, the 9.9% keyframe can move to 49.9%, and the 19.9% keyframe can switch to 99.9%, giving us this:

#moon1 {
  animation: moon-one 12s infinite;
}

@keyframes moon-one {
  0%, 100% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
  25% {
    transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
    z-index: 2;
    animation-timing-function: ease-out;
  }
  49.9% {
    z-index: 2;
  }
  50% {
    transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
    z-index: -1;
    animation-timing-function: ease-in;
  }
  75% {
    transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
    z-index: -1;
    animation-timing-function: ease-out;
  }
  99.9% {
    z-index: -1;
  }
}

Though I was very proud of myself for my math wrangling, numbers like -3.51217391vw are really, really unnecessary. If a screen was one thousand pixels wide, -3.51217391vw would be 35.1217391 pixels. No one ever needs to go down to the precision of a ten-millionth of a pixel. So, let’s round everything to the tenth place (and if it’s a 0, we’ll just drop it). We can also skip z-index in the 75% and 25% keyframes since it doesn’t change.

Here’s where that gets us in the code:

#moon1 {
  animation: moon-one 12s infinite;
}

@keyframes moon-one {
  0%, 100% {
    transform: translate(0, 0) scale(1);
    z-index: 2;
    animation-timing-function: ease-in;
  }
  25% {
    transform: translate(-3.5vw, 3.5vw) scale(1.5);
    z-index: 2;
    animation-timing-function: ease-out;
  }
  49.9% {
    z-index: 2;
  }
  50% {
    transform: translate(-5vw, 6.5vw) scale(1);
    z-index: -1;
    animation-timing-function: ease-in;
  }
  75% {
    transform: translate(1vw, 2.5vw) scale(0.25);
    z-index: -1;
    animation-timing-function: ease-out;
  }
  99.9% {
    z-index: -1;
  }
}

After all our changes, the animation still looks pretty close to what it was before, only way less code:

One of the things I don’t like about this animation is that the moon kind of turns at its zenith when it crosses the planet. It would be much better if it traveled in a straight line from the upper right to the lower left. However, we also need it to get a little larger, as if the moon is coming closer to us in its orbit. Because both translation and scaling were done in the transform property, I can’t translate and scale the moon independently.

If we skip either one in the transform property, it resets the one we skipped, so I’m forced to guess where the mid-point should be so that I can set the scale I need. One way I’ve solved this in the past is to add a wrapping element, then apply scale to one element and translate to the other. However, now that we have individual scale and translate properties, a better way is to separate them from the transform property and use them as separate properties. Separating out the translation and scaling shouldn’t change anything, unless the original order they were declared on the transform property was different than the order of the singular properties.

#moon1 {
  animation: moon-one 12s infinite;
}

@keyframes moon-one {
  0%, 100% {
    translate: 0 0;
    scale: 1;
    z-index: 2;
    animation-timing-function: ease-in;
  }
  25% {
    translate: -3.5vw 3.5vw;
    z-index: 2;
    animation-timing-function: ease-out;
  }
  49.9% {
    z-index: 2;
  }
  50% {
    translate: -5vw 6.5vw;
    scale: 1;
    z-index: -1;
    animation-timing-function: ease-in;
  }
  75% {
    translate: 1vw 2.5vw;
    scale: 0.25;
    animation-timing-function: ease-out;
  }
  99.9% {
    z-index: -1;
  }
}

Now that we can separate the scale and translate properties and use them independently, we can drop the translate property in the 25% and 75% keyframes because we don’t want them placed precisely in that keyframe. We want the browser’s interpolation to take care of that for us so that it translates smoothly while scaling.

#moon1 {
  animation: moon-one 12s infinite;
}

@keyframes moon-one {
  0%, 100% {
    translate: 0 0;
    scale: 1;
    z-index: 2;
    animation-timing-function: ease-in;
  }
  25% {
    scale: 1.5;
    animation-timing-function: ease-out;
  }
  49.9% {
    z-index: 2;
  }
  50% {
    translate: -5vw 6.5vw;
    scale: 1;
    z-index: -1;
    animation-timing-function: ease-in;
  }
  75% {
    scale: 0.25;
    animation-timing-function: ease-out;
  }
  99.9% {
    z-index: -1;
  }
}

Lastly, those different timing functions don’t make a lot of sense anymore because we’ve got the browser working for us, and if we use an ease-in-out timing function on everything, then it should do exactly what we want.

#moon1 {
  animation: moon-one 12s infinite ease-in-out;
}

@keyframes moon-one {
  0%, 100% {
    translate: 0 0;
    scale: 1;
    z-index: 2;
  }
  25% {
    scale: 1.5;
  }
  49.9% {
    z-index: 2;
  }
  50% {
    translate: -5vw 6.5vw;
    scale: 1;
    z-index: -1;
  }
  75% {
    scale: 0.25;
  }
  99.9% {
    z-index: -1;
  }
}

And there you go: 141 lines down to 28, and I think the animation looks even better than before. It will certainly be easier to maintain, that’s for sure.

But what do you think? Was there an optimization step I missed? Let me know in the comments.


Orbital Mechanics (or How I Optimized a CSS Keyframes Animation) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Sunkanmi Fafowora
Wed, 07 May 2025 12:25:19 +0000


Okay, nobody is an exaggeration, but have you seen the stats for hwb()? They show a steep decline, and after working a lot on color in the CSS-Tricks almanac, I’ve just been wondering why that is.

Chrome Platform Status for hwb(), showing a steep decline in usage from 0.4 percent of all page loads in 2024 to less than .2 perfect in 2025.

hwb() is a color function in the sRGB color space, which is the same color space used by rgb()hsl() and the older hexadecimal color format (e.g. #f8a100). hwb() is supposed to be more intuitive and easier to work with than hsl(). I kinda get why it’s considered “easier” since you specify how much black or white you want to add to a given color. But, how is hwb() more intuitive than hsl()?

hwb() accepts three values, and similar to hsl(), the first value specifies the color’s hue (between 0deg360deg), while the second and third values add whiteness (0 – 100) and blackness (0 – 100) to the mix, respectively.

According to Google, the term “intuitive” means “what one feels to be true even without conscious reasoning; instinctive.” As such, it does truly seem that hwb() is more intuitive than hsl(), but it’s only a slight notable difference that makes that true.

Let’s consider an example with a color. We’ll declare light orange in both hsl() and hwb():

/* light orange in hsl */
.element-1 {
  color: hsl(30deg 100% 75%);
}

/* light orange in hwb() */
.element-2 {
  color: hwb(30deg 50% 0%);
}

These two functions produce the exact same color, but while hwb() handles ligthness with two arguments, hsl() does it with just one, leaving one argument for the saturation. By comparison, hwb() provides no clear intuitive way to set just the saturation. I’d argue that makes the hwb() function less intuitive than hsl().

I think another reason that hsl() is generally more intuitive than hwb() is that HSL as a color model was created in the 1970s while HWB as a color model was created in 1996. We’ve had much more time to get acquainted with hsl() than we have hwb(). hsl() was implemented by browsers as far back as 2008, Safari being the first and other browsers following suit. Meanwhile, hwb() gained support as recently as 2021! That’s more than a 10-year gap between functions when it comes to using them and being familiar with them.

There’s also the fact that other color functions that are used to represent colors in other color spaces — such as lab()lch()oklab(), and oklch() — offer more advantages, such as access to more colors in the color gamut and perceptual uniformity. So, maybe being intuitive is coming at the expense of having a more robust feature set, which could explain why you might go with a less intuitive function that doesn’t use sRGB.

Look, I can get around the idea of controlling how white or black you want a color to look based on personal preferences, and for designers, it’s maybe easier to mix colors that way. But I honestly would not opt for this as my go-to color function in the sRGB color space because hsl() does something similar using the same hue, but with saturation and lightness as the parameters which is far more intuitive than what hwb() offers.

I see our web friend, Stefan Judis, preferring hsl() over hwb() in his article on hwb().

Lea Verou even brought up the idea of removing hwb() from the spec in 2022, but a decision was made to leave it as it was since browsers were already implementing the function. And although,I was initially pained by the idea of keeping hwb() around, I also quite understand the feeling of working on something, and then seeing it thrown in the bin. Once we’ve introduced something, it’s always tough to walk it back, especially when it comes to maintaining backwards compatibility, which is a core tenet of the web.

I would like to say something though: lab()lch()oklab()oklch() are already here and are better color functions than hwb(). I, for one, would encourage using them over hwb() because they support so many more colors that are simply missing from the hsl() and hwb() functions.

I’ve been exploring colors for quite some time now, so any input would be extremely helpful. What color functions are you using in your everyday website or web application, and why?

More on color


Why is Nobody Using the hwb() Color Function? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Ryan Trimble
Tue, 06 May 2025 14:14:41 +0000


Back in October, the folks behind the GreenSock Animation Platform (GSAP) joined forces with Webflow, the visual website builder. Now, the team’s back with another announcement: Along with the version 3.13 release, GSAP, and all its awesome plugins, are now freely available to everyone.

Thanks to Webflow GSAP is now 100% free including all of the bonus plugins like SplitTextMorphSVG, and all the others that were exclusively available to Club GSAP members. That’s right, the entire GSAP toolset is free, even for commercial use! 🤯

Webflow is celebrating over on their blog as well:

With Webflow’s support, the GSAP team can continue to lead the charge in product and industry innovation while allowing even more developers the opportunity to harness the full breadth of GSAP-powered motion.

Check out the GSAP blog to read more about the announcement, then go animate something awesome and share it with us!


GSAP is Now Completely Free, Even for Commercial Use! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 05 May 2025 17:00:34 +0000


The news is that GSAP, a hugely popular animation library on CodePen and the web writ large, is now entirely free to use thanks to their being acquired by Webflow.

Thanks to Webflow GSAP is now 100% FREE including ALL of the bonus plugins like SplitTextMorphSVG, and all the others that were exclusively available to Club GSAP members. That’s right – the entire GSAP toolset is FREE, even for commercial use! 🤯 You can read more about this on Webflow’s blog

Cool.

In celebration, they are also running a Community Challenge where you make stuff and submit it and maybe win some swag. You make something to submit either with Webflow or CodePen, and they provide a quick Pen template to get started.

As you can see in that template, GSAP is great at animating regular ol’ HTML content, and in this case text content that it splits into individual elements (accessibly!) with the brand-new entirely re-written for betterness SplitText plugin. But GSAP can animate… whatever. I actually think of it as being particularly good at animating SVG, so I figure we ought to spend the rest of our time together here looking at sweet SVG links that caught my eye recently.

  • Animating Figma’s SVG Exports by Nanda Syahrasyad — These interactive posts that Nanda does are amazing. It really doesn’t have anything to do with Figma, but that’s a clever title as it will help connect with the kind of developer who needs this. This made me think of GSAP a bit as the last demo relies on a bit of transform-origin which GSAP explicitly fixes cross-browser (or at least that used to be a big sticking point it smoothed over).
  • svg-gobbler by Ross Moody — Exporting SVG from a design tool, like above, is one way to get the SVG you need. Another is kiping it from existing sites! There is lots of SVG on the web already to get your hands on (be careful to account for copyright and taste). This browser extension helps you extract them cleanly.
  • SVG Coding Examples: Useful Recipes For Writing Vectors By Hand by Myriam Frisano — The other way to get your hands on the SVG you need is to roll up your sleeves and write it, which is an entirely possible thing to do in SVG syntax. This guide doesn’t use <path> on purpose because that’s a whole thing unto itself (which I once documented and have played with on a limited basis). Myriam’s guide here does get into using JavaScript to variable-ize things and do loops and stuff which is all smart and useful stuff.
  • From static to interactive: turn SVG diagrams into exciting experiences on your website by Vanessa Fillis — Flouish looks like a pretty cool tool. These demos by Vanessa to me feel like slightly fanci-fied image map demos, which is actually a perfectly great SVG use case.
  • Changing Colors in an SVG Element Using CSS and JavaScript by Kirupa Chinnathambi — Just some SVG 101 here, which is always appreciated.
  • Vectorpea by Ivan Kutskir— Web-based vector editor (ala Illustrator, with the Pen tool and such) that opens lots of file formats and works quite nicely in my limited experience.
  • Lissajous Curve SVG Generator by Eva Decker — So niche.
  • SVGFM by Chris Kirknielsen — SVG filters are ultra powerful and, I’ve always felt, a bit inscrutable. Chris brings some language and UI to the party making it a bit easier to experiment and play. But it’s still complex!
  • Revisiting SVG filters – my forgotten powerhouse for duotones, noise, and other effects by Brecht De Ruyte — My favorite kind of SVG filters are the ones with one clear purpose and one filter that does the thing. Duotone images are that.
  • The Truth(tm) about encoding SVG in data URIs by Stoyan Stefanov — When using SVG in CSS as a background, you can do: background: url('data:image/svg+xml,<svg ...></svg>'); and I mean that quite literally. You can put whatever SVG syntax in there and it’ll work generally as expected. No scripting or anything. There is only one thing to worry about: encode any # characters as %23.
by: Kevin Hamer
Mon, 05 May 2025 13:01:43 +0000


Using scroll shadows, especially for mobile devices, is a subtle bit of UX that Chris has covered before (indeed, it’s one of his all-time favorite CSS tricks), by layering background gradients with different attachments, we can get shadows that are covered up when you’ve scrolled to the limits of the element.

Geoff covered a newer approach that uses the animation-timeline property. Using animation-timeline, we can tie CSS animation to the scroll position. His example uses pseudo-elements to render the scroll shadows, and animation-range to animate the opacity of the pseudo-elements based on scroll.

Here’s yet another way. Instead of using shadows, let’s use a CSS mask to fade out the edges of the scrollable element. This is a slightly different visual metaphor that works great for horizontally scrollable elements — places where your scrollable element doesn’t have a distinct border of its own. This approach still uses animation-timeline, but we’ll use custom properties instead of pseudo-elements. Since we’re fading, the effect also works regardless of whether we’re on a dark or light background.

Getting started with a scrollable element

First, we’ll define our scrollable element with a mask that fades out the start and end of the container. For this example, let’s consider the infamous table that can’t be responsive and has to be horizontally scrollable on mobile.

Let’s add the mask. We can use the shorthand and find the mask as a linear gradient that fades out on either end. A mask lets the table fade into the background instead of overlaying a shadow, but you could use the same technique for shadows.

.scrollable {
  mask: linear-gradient(to right, #0000, #ffff 3rem calc(100% - 3rem), #0000);
}

Defining the custom properties and animation

Next, we need to define our custom properties and the animation. We’ll define two separate properties, --left-fade and --right-fade, using @property. Using @property is necessary here to specify the syntax of the properties so that browsers can animate the property’s values.

@property --left-fade {
  syntax: "<length>";
  inherits: false;
  initial-value: 0;
}

@property --right-fade {
  syntax: "<length>";
  inherits: false;
  initial-value: 0;
}

@keyframes scrollfade {
  0% {
    --left-fade: 0;
  }
  10%, 100% {
    --left-fade: 3rem;
  }
  0%, 90% {
    --right-fade: 3rem;
  }
  100% {
    --right-fade: 0;
  }
}

Instead of using multiple animations or animation-range, we can define a single animation where --left-fade animates from 0 to 3rem between 0-10%, and --right-fade animates from 3rem to 0 between 90-100%. Now we update our mask to use our custom properties and tie the scroll-timeline of our element to its own animation-timeline.

Putting it all together

Putting it all together, we have the effect we’re after:

We’re still waiting for some browsers (Safari) to support animation-timeline, but this gracefully degrades to simply not fading the element at all.

Wrapping up

I like this implementation because it combines two newer bits of CSS — animating custom properties and animation-timeline — to achieve a practical effect that’s more than just decoration. The technique can even be used with scroll-snap-based carousels or cards:

It works regardless of content or background and doesn’t require JavaScript. It exemplifies just how far CSS has come lately.


Modern Scroll Shadows Using Scroll-Driven Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Geoff Graham
Fri, 02 May 2025 12:36:10 +0000


The CSS shape() function recently gained support in both Chromium and WebKit browsers. It’s a way of drawing complex shapes when clipping elements with the clip-path property. We’ve had the ability to draw basic shapes for years — think circle, ellipse(), and polygon() — but no “easy” way to draw more complex shapes.

Well, that’s not entirely true. It’s true there was no “easy” way to draw shapes, but we’ve had the path() function for some time, which we can use to draw shapes using SVG commands directly in the function’s arguments. This is an example of an SVG path pulled straight from WebKit’s blog post linked above:

<svg viewBox="0 0 150 100" xmlns="http://www.w3.org/2000/svg">
  <path fill="black" d="M0 0 L 100 0 L 150 50 L 100 100 L 0 100 Q 50 50 0 0 z " />
</svg>

Which means we can yank those <path> coordinates and drop them into the path() function in CSS when clipping a shape out of an element:

.clipped {
  clip-path: path("M0 0 L 100 0 L 150 50 L 100 100 L 0 100 Q 50 50 0 0 z");
}

I totally understand what all of those letters and numbers are doing. Just kidding, I’d have to read up on that somewhere, like Myriam Frisano’s more recent “Useful Recipes For Writing Vectors By Hand” article. There’s a steep learning curve to all that, and not everyone — including me — is going down that nerdy, albeit interesting, road. Writing SVG by hand is a niche specialty, not something you’d expect the average front-ender to know. I doubt I’m alone in saying I’d rather draw those vectors in something like Figma first, export the SVG code, and copy-paste the resulting paths where I need them.

The shape() function is designed to be more, let’s say, CSS-y. We get new commands that tell the browser where to draw lines, arcs, and curves, just like path(), but we get to use plain English and native CSS units rather than unreadable letters and coordinates. That opens us up to even using CSS calc()-ulations in our drawings!

Here’s a fairly simple drawing I made from a couple of elements. You’ll want to view the demo in either Chrome 135+ or Safari 18.4+ to see what’s up.

So, instead of all those wonky coordinates we saw in path(), we get new terminology. This post is really me trying to wrap my head around what those new terms are and how they’re used.

In short, you start by telling shape() where the starting point should be when drawing. For example, we can say “from top left” using directional keywords to set the origin at the top-left corner of the element. We can also use CSS units to set that position, so “from 0 0” works as well. Once we establish that starting point, we get a set of commands we can use for drawing lines, arcs, and curves.

I figured a table would help.

CommandWhat it meansUsageExamples
lineA line that is drawn using a coordinate pairThe by keyword sets a coordinate pair used to determine the length of the line.line by -2px 3px
vlineVertical lineThe to keyword indicates where the line should end, based on the current starting point.

The by keyword sets a coordinate pair used to determine the length of the line.
vline to 50px
hlineHorizontal lineThe to keyword indicates where the line should end, based on the current starting point.

The by keyword sets a coordinate pair used to determine the length of the line.
hline to 95%
arcAn arc (oh, really?!). An elliptical one, that is, sort of like the rounded edges of a heart shape.The to keyword indicates where the arc should end.

The with keyword sets a pair of coordinates that tells the arc how far right and down the arc should slope.

The of keyword specifies the size of the ellipse that the arc is taken from. The first value provides the horizontal radius of the ellipse, and the second provides the vertical radius. I’m a little unclear on this one, even after playing with it.
arc to 10% 50% of 1%
curveA curved lineThe to keyword indicates where the curved line should end.

The with keyword sets “control points” that affect the shape of the curve, making it deep or shallow.
curve to 0% 100% with 50% 0%
smoothAdds a smooth Bézier curve command to the list of path data commandsThe to keyword indicates where the curve should end.

The by keyword sets a coordinate pair used to determine the length of the curve.

The with keyword specifies control points for the curve.
I have yet to see any examples of this in the wild, but let me know if you do, and I can add it here.

The spec is dense, as you might expect with a lot of moving pieces like this. Again, these are just my notes, but let me know if there’s additional nuance you think would be handy to include in the table.

Oh, another fun thing: you can adjust the shape() on hover/focus. The only thing is that I was unable to transition or animate it, at least in the current implementation.


CSS shape() Commands originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Sacha Greif
Thu, 01 May 2025 12:34:58 +0000


I don’t know if I should say this on a website devoted to programming, but I sometimes feel like *lowers voice* coding is actually the least interesting part of our lives.

After all, last time I got excited meeting someone at a conference it was because we were both into bouldering, not because we both use React. And The Social Network won an Oscar for the way it displayed interpersonal drama, not for its depiction of Mark Zuckerberg’s PHP code. 

Yet for the past couple years, I’ve been running developer surveys (such as the State of JS and State of CSS) that only ask about code. It was time to fix that. 

A new kind of survey

The State of Devs survey is now open to participation, and unlike previous surveys it covers everything except code: career, workplace, but also health, hobbies, and more. 

I’m hoping to answer questions such as:

  • What are developers’ favorite recent movies and video games?
  • What kind of physical activity do developers practice?
  • How much sleep are we all getting?

But also address more serious topics, including:

  • What do developers like about their workplace?
  • What factors lead to workplace discrimination?
  • What global issues are developers most concerned with?

Reaching out to new audiences

Another benefit from branching out into new topics is the chance to reach out to new audiences.

It’s no secret that people who don’t fit the mold of the average developer (whether because of their gender, race, age, disabilities, or a myriad of other factors) often have a harder time getting involved in the community, and this also shows up in our data. 

In the past, we’ve tried various outreach strategies to help address these imbalances in survey participation, but the results haven’t always been as effective as we’d hoped. 

So this time, I thought I’d try something different and have the survey itself include more questions relevant to under-represented groups, asking about workplace discrimination:

Question: Have you ever experiences discrimination in the workplace based on any of the following factors?

As well as actions taken in response to said discrimination:

Question: Have you taken any of the following actions in response to workplace issues?

Yet while obtaining a more representative data sample as a result of this new focus would be ideal, it isn’t the only benefit. 

The most vulnerable among us are often the proverbial canaries in the coal mine, suffering first from issues or policies that will eventually affect the rest of the community as well, if left unchecked. 

So, facing these issues head-on is especially valuable now, at a time when “DEI” is becoming a new taboo, and a lot of the important work that has been done to make things slightly better over the past decade is at risk of being reversed.

The big questions

Finally, the survey also tries to go beyond work and daily life to address the broader questions that keep us up at night:

Question: What global issues are currently most concerning to you?

There’s been talk in recent years about keeping the workplace free of politics. And why I can certainly see the appeal in that, in 2025, it feels harder than ever to achieve that ideal. At a time when people are losing rights and governments are sliding towards authoritarianism, should we still pretend that everything is fine? Especially when you factor in the fact that the tech community is now a major political player in its own right…

So while I didn’t push too far in that direction for this first edition of the survey, one of my goals for the future is to get a better grasp of where exactly developers stand in terms of ideology and worldview. Is this a good idea, or should I keep my distance from any hot-button issues? Don’t hesitate to let me know what you think, or suggest any other topic I should be asking about next time. 

In the meantime, go take the survey, and help us get a better picture of who exactly we all are!


State of Devs: A Survey for Every Developer originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Andy Clarke
Wed, 30 Apr 2025 12:12:45 +0000


I mentioned last time that I’ve been working on a new website for Emmy-award-winning game composer Mike Worth. He hired me to create a highly graphical design that showcases his work.

Mike loves ’90s animation, particularly Disney’s Duck Tales and other animated series. He challenged me to find a way to incorporate their retro ’90s style into his design without making it a pastiche. But that wasn’t my only challenge. I also needed to achieve that ’90s feel by using up-to-the-minute code to maintain accessibility, performance, responsiveness, and semantics.

Design by Andy Clarke, Stuff & Nonsense. Mike Worth’s website will launch in April 2025, but you can see examples from this article on CodePen.

Designing for Mike was like a trip back to when mainstream website design seemed more spontaneous and less governed by conventions and best practices. Some people describe these designs as “whimsical”:

adjective

  1. spontaneously fanciful or playful
  2. given to whims; capricious
  3. quaint, unusual, or fantastic

Collins English Dictionary

But I’m not so sure that’s entirely accurate. “Playful?” Definitely. “Fanciful?” Possibly. But “fantastic?” That depends. “Whimsy” sounds superfluous, so I call it “expressive” instead.

Studying design from way back, I remembered how websites often included graphics that combined branding, content, and navigation. Pretty much every reference to web design in the ’90s — when I designed my first website — talks about Warner Brothers’ Space Jam from 1996.

Space Jam website homepage. The movie logo is in the center of a series of planets that are navigation for other areas of the website against a dark starry background.
Warner Brothers’ Space Jam (1996)

So, I’m not going to do that.

Brands like Nintendo used their home pages to direct people to their content while making branded visual statements. Cheestrings combined graphics with navigation, making me wonder why we don’t see designs like this today. Goosebumps typified this approach, combining cartoon illustrations with brightly colored shapes into a functional and visually rich banner, proving that being useful doesn’t mean being boring.

Left to right: Nintendo, Cheestrings, Goosebumps.

In the ’90s, when I developed graphics for websites like these, I either sliced them up and put their parts in tables or used mostly forgotten image maps.

A brief overview of properties and values

Let’s run through a quick refresher. Image maps date all the way back to HTML 3.2, where, first, server-side maps and then client-side maps defined clickable regions over an image using map and area elements. They were popular for graphics, maps, and navigation, but their use declined with the rise of CSS, SVG, and JavaScript.

<map> adds clickable areas to a bitmap or vector image.

<map name="projects">
  ...
</map>

That <map> is linked to an image using the usemap attribute:

<img usemap="#projects" ...>

Those elements can have separate href and alt attributes and can be enhanced with ARIA to improve accessibility:

<map name="projects">
  <area href="" alt="" … />
  ...
</map>

The shape attribute specifies an area’s shape. It can be a primitive circle or rect or a polygon defined by a set of absolute x and y coordinates:

<area shape="circle" coords="..." ... />
<area shape="rect" coords="..." ... />
<area shape="poly" coords="..." ... />

Despite their age, image maps still offer plenty of benefits. They’re lightweight and need (almost) no JavaScript. More on that in just a minute. They’re accessible and semantic when used with alt, ARIA, and title attributes. Despite being from a different era, even modern mobile browsers support image maps.

A large illustrated map of a cartoon island.
Design by Andy Clarke, Stuff & Nonsense. Mike Worth’s website will launch in April 2025, but you can see examples from this article on CodePen.

My design for Mike Worth includes several graphic navigation elements, which made me wonder if image maps might still be an appropriate solution.

Image maps in action

Mike wants his website to showcase his past work and the projects he’d like to do. To make this aspect of his design discoverable and fun, I created a map for people to explore by pressing on areas of the map to open modals. This map contains numbered circles, and pressing one pops up its modal.

A large illustrated cartoon map of an island. 6 annotations are on the map and a tooltip is exposed showing detail of one of the annotations.

My first thought was to embed anchors into the external map SVG:

<img src="projects.svg" alt="Projects">

<svg ...>
  ...
  <a href="...">
    <circle cx="35" cy="35" r="35" fill="#941B2F"/>
    <path fill="#FFF" d="..."/>
  </a>
</svg>

This approach is problematic. Those anchors are only active when SVG is inline and don’t work with an <img> element. But image maps work perfectly, even though specifying their coordinates can be laborious. Fortunately, plenty of tools are available, which make defining coordinates less tedious. Upload an image, choose shape types, draw the shapes, and copy the markup:

<img src="projects.svg" usemap="#projects-map.svg">

<map name="projects-map.svg">
  <area href="" alt="" coords="..." shape="circle">
  <area href="" alt="" coords="..." shape="circle">
  ...
</map>

Image maps work well when images are fixed sizes, but flexible images present a problem because map coordinates are absolute, not relative to an image’s dimensions. Making image maps responsive needs a little JavaScript to recalculate those coordinates when the image changes size:

function resizeMap() {
  const image = document.getElementById("projects");
  const map = document.querySelector("map[name='projects-map']");
  
  if (!image || !map || !image.naturalWidth) return;
  
  const scale = image.clientWidth / image.naturalWidth;
  map.querySelectorAll("area").forEach(area => {
  
    if (!area.dataset.originalCoords) {
      area.dataset.originalCoords = area.getAttribute("coords");
    }

    const scaledCoords = area.dataset.originalCoords
    
    .split(",")
    .map(coord => Math.round(coord * scale))
    .join(",");
    area.setAttribute("coords", scaledCoords);
  });
}

["load", "resize"].forEach(event =>
  window.addEventListener(event, resizeMap)
);

I still wasn’t happy with this implementation as I wanted someone to be able to press on much larger map areas, not just the numbered circles.

Every <path> has coordinates which define how it’s drawn, and they’re relative to the SVG viewBox:

<svg width="1024" height="1024">
  <path fill="#BFBFBF" d="…"/>
</svg>

On the other hand, a map’s <area> coordinates are absolute to the top-left of an image, so <path> values need to be converted. Fortunately, Raphael Monnerat has written PathToPoints, a tool which does precisely that. Upload an SVG, choose the point frequency, copy the coordinates for each path, and add them to a map area’s coords:

<map>
  <area href="" shape="poly" coords="...">
  <area href="" shape="poly" coords="...">
  <area href="" shape="poly" coords="...">
  ...
</map>

More issues with image maps

Image maps are hard-coded and time-consuming to create without tools. Even with tools for generating image maps, converting paths to points, and then recalculating them using JavaScript, they could be challenging to maintain at scale.

<area> elements aren’t visible, and except for a change in the cursor, they provide no visual feedback when someone hovers over or presses a link. Plus, there’s no easy way to add animations or interaction effects.

But the deal-breaker for me was that an image map’s pixel-based values are unresponsive by default. So, what might be an alternative solution for implementing my map using CSS, HTML, and SVG?

A large illustrated map of a cartoon island cut out against a transparent background.

Anchors positioned absolutely over my map wouldn’t solve the pixel-based positioning problem or give me the irregular-shaped clickable areas I wanted. Anchors within an external SVG wouldn’t work either.

But the solution was staring me in the face. I realized I needed to:

  1. Create a new SVG path for each clickable area.
  2. Make those paths invisible.
  3. Wrap each path inside an anchor.
  4. Place the anchors below other elements at the end of my SVG source.
  5. Replace that external file with inline SVG.
The same illustrated map of an island but the six regions are represented by solid colors.

I created a set of six much larger paths which define the clickable areas, each with its own fill to match its numbered circle. I placed each anchor at the end of my SVG source:

<svg … viewBox="0 0 1024 1024">

  <!-- Visible content -->
  <g>...</g>

  <!-- Clickable areas -->`
  <g id="links">`
    <a href="..."><path fill="#B48F4C" d="..."/></a>`
    <a href="..."><path fill="#6FA676" d="..."/></a>`
    <a href="..."><path fill="#30201D" d="..."/></a>`
    ...
  </g>
</svg>

Then, I reduced those anchors’ opacity to 0 and added a short transition to their full-opacity hover state:

#links a {
  opacity: 0;
  transition: all .25s ease-in-out;
}

#links a:hover {
  opacity: 1;
}

While using an image map’s <area> sadly provides no visual feedback, embedded anchors and their content can respond to someone’s action, hint at what’s to come, and add detail and depth to a design.

The illustrated regions of the cartoon map in three states, from left to right: annotated, with visual markers, and with both visual markers and labels.

I might add gloss to those numbered circles to be consistent with the branding I’ve designed for Mike. Or, I could include images, titles, or other content to preview the pop-up modals:

<g id="links">
  <a href="…">
    <path fill="#B48F4C" d="..."/>
    <image href="..." ... />
  </a>
</g>

Try it for yourself:

Expressive design, modern techniques

Designing Mike Worth’s website gave me a chance to blend expressive design with modern development techniques, and revisiting image maps reminded me just how important a tool image maps were during the period Mike loves so much.

Ultimately, image maps weren’t the right tool for Mike’s website. But exploring them helped me understand what I really needed: a way to recapture the expressiveness and personality of ’90s website design using modern techniques that are accessible, lightweight, responsive, and semantic. That’s what design’s about: choosing the right tool for a job, even if that sometimes means looking back to move forward.


Biography: Andy Clarke

Often referred to as one of the pioneers of web design, Andy Clarke has been instrumental in pushing the boundaries of web design and is known for his creative and visually stunning designs. His work has inspired countless designers to explore the full potential of product and website design.

Andy’s written several industry-leading books, including Transcending CSS, Hardboiled Web Design, and Art Direction for the Web. He’s also worked with businesses of all sizes and industries to achieve their goals through design.

Visit Andy’s studio, Stuff & Nonsense, and check out his Contract Killer, the popular web design contract template trusted by thousands of web designers and developers.


Revisiting Image Maps originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Geoff Graham
Tue, 29 Apr 2025 14:27:25 +0000


Brad Frost is running this new little podcast called Open Up. Folks write in with questions about the “other” side of web design and front-end development — not so much about tools and best practices as it is about the things that surround the work we do, like what happens if you get laid off, or AI takes your job, or something along those lines. You know, the human side of what we do in web design and development.

Well, it just so happens that I’m co-hosting the show. In other words, I get to sprinkle in a little advice on top of the wonderful insights that Brad expertly doles out to audience questions.

Our second episode just published, and I thought I’d share it. We’re finding our sea legs with this whole thing and figuring things out as we go. We’ve opened things up (get it?!) to a live audience and even pulled in one of Brad’s friends at the end to talk about the changing nature of working on a team and what it looks like to collaborate in a remote-first world.

https://www.youtube.com/watch?v=bquVF5Cibaw


Open Up With Brad Frost, Episode 2 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 28 Apr 2025 17:20:59 +0000


I was listening to Wes and Scott on a recent episode of Syntax talking about RSCs (React Server Components). I wouldn’t say it was particularly glowing.

We use them here at CodePen, and will likely be more and more as we ship more with Next.js, which is part of our most modern stack that we are always moving toward. Me, I like Next.js. React makes good sense to me for use in a very interactive, long-session style application with oodles of state management. By virtue of being on the latest Next.js release, whatever we put in the app directory (“The App Router” as they call it) automatically uses RSCs when it can. I mostly like that. We do have to fight it sometimes, but those fights are generally about server-side rendering and making sure we are set up for that and doing things right to take advantage of it, which honestly we should be doing as much as possible anyway. I’ll also add some anecdotal data that we haven’t exactly seen huge drops in JavaScript bundle size when we move things that direction, which I was hoping would be a big benefit of that work.

But React is more than Next.js, right? Right?! Yes and no. I use React without Next.js sometimes, and we do at CodePen in plenty of places. Without Next.js, usage of RSCs is hard or not happening. Precious few other frameworks are using them, and some have thrown up their hands and refused. To be fair: Parcel has support in Beta and Waku also supports them.

A little hard to call them a big success in this state. But it’s also hard to call the concept of them a bad idea. It’s generally just a good idea to make the servers do more work than browsers, as well as send as little data across the network as possible. If the JavaScript in a React component can be run on the server, and we can make the server part kinda smart, let’s let it?

If you’ve got the time and inclination, Dam Abramov’s React for Two Computers is a massive post that is entirely a conceptual walkthrough abstracting the ideas of RSCs into an “Early World” and “Late World” to understand the why and where it all came from. He just recently followed it up with Impossible Components which gets more directly into using RSCs.

Welp — while we’re talking React lemme drop some related links I found interesting lately.

  • React itself, aside from RSCs, isn’t sitting idle. They’ve shipped an experimental <ViewTransition> component which is nice to see as someone who has struggled forcing React to do this before. They’ve also shipped an RC (Release Candidate) for the React Compiler (also RC? awkward?). The compiler is interesting in that it doesn’t necessarily make your bundles smaller it makes them run faster.
  • Fancy Components is a collection of “mainly React, TypeScript, Tailwind, Motion” components that are… fancy. I’ve seen a bit of pushback on the accessibility of some of them, but I’ve also poked through them and found what look like solid attempts at making them accessible, so YMMV.
  • Sahaj Jain says The URL is a great place to store state in React.
  • Joshua Wootonn details the construction of a Drag to Select interaction in React which is… pretty complicated.
  • The blog Expression Statement (no byline) says HTML Form Validation is heavily underused. I just added a bit of special validation to a form in React this week and I tend to agree. Short story: GMail doesn’t render <img>s where the src has a space in it. 😭. I used pattern directly on the input, and we have our own error message system, otherwise I would have also used setCustomValidity.
  • Thoughtbot: Superglue 1.0: React ❤️ Rails
by: Geoff Graham
Mon, 28 Apr 2025 12:43:01 +0000


Ten divs walk into a bar:

<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
<div>9</div>
<div>10</div>

There’s not enough chairs for them to all sit at the bar, so you need the tenth div to sit on the lap of one of the other divs, say the second one. We can visually cover the second div with the tenth div but have to make sure they are sitting next to each other in the HTML as well. The order matters.

<div>1</div>
<div>2</div>
<div>10</div><!-- Sitting next to Div #2-->
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
<div>9</div>

The tenth div needs to sit on the second div’s lap rather than next to it. So, perhaps we redefine the relationship between them and make this a parent-child sorta thing.

<div>1</div>
<div class="parent">
  2
  <div class="child">10</div><!-- Sitting in Div #2's lap-->
</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
<div>9</div>

Now we can do a little tricky positioning dance to contain the tenth div inside the second div in the CSS:

.parent {
  position: relative; /* Contains Div #10 */
}

.child {
  position: absolute;
}

We can inset the child’s position so it is pinned to the parent’s top-left edge:

.child {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
}

And we can set the child’s width to 100% of the parent’s size so that it is fully covering the parent’s lap and completely obscuring it.

.child {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  width: 100%;
}

Cool, it works!

Anchor positioning simplifies this process a heckuva lot because it just doesn’t care where the tenth div is in the HTML. Instead, we can work with our initial markup containing 10 individuals exactly as they entered the bar. You’re going to want to follow along in the latest version of Chrome since anchor positioning is only supported there by default at the time I’m writing this.

<div>1</div>
<div class="parent">2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
<div>9</div>
<div class="child">10</div>

Instead, we define the second div as an anchor element using the anchor-name property. I’m going to continue using the .parent and .child classes to keep things clear.

.parent {
  anchor-name: --anchor; /* this can be any name formatted as a dashed ident */
}

Then we connect the child to the parent by way of the position-anchor property:

.child {
  position-anchor: --anchor; /* has to match the `anchor-name` */
}

The last thing we have to do is position the child so that it covers the parent’s lap. We have the position-area property that allows us to center the element over the parent:

.child {
  position-anchor: --anchor;
  position-area: center;
}

If we want to completely cover the parent’s lap, we can set the child’s size to match that of the parent using the anchor-size() function:

.child {
  position-anchor: --anchor;
  position-area: center;
  width: anchor-size(width);
}

No punchline — just one of the things that makes anchor positioning something I’m so excited about. The fact that it eschews HTML source order is so CSS-y because it’s another separation of concerns between content and presentation.


Anchor Positioning Just Don’t Care About Source Order originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Blackle Mori
Thu, 24 Apr 2025 12:49:42 +0000


You would be forgiven if you’ve never heard of Cohost.org. The bespoke, Tumblr-like social media website came and went in a flash. Going public in June 2022 with invite-only registrations, Cohost’s peach and maroon landing page promised that it would be “posting, but better.” Just over two years later, in September 2024, the site announced its shutdown, its creators citing burnout and funding problems. Today, its servers are gone for good. Any link to cohost.org redirects to the Wayback Machine’s slow but comprehensive archive.

Screenshot of the Cohost.org homepage before it was shut down.

The landing page for Cohost.org, featuring our beloved eggbug.

Despite its short lifetime, I am confident in saying that Cohost delivered on its promise. This is in no small part due to its user base, consisting mostly of niche internet creatives and their friends — many of whom already considered “posting” to be an art form. These users were attracted to Cohost’s opinionated, anti-capitalist design that set it apart from the mainstream alternatives. The site was free of advertisements and follower counts, all feeds were purely chronological, and the posting interface even supported a subset of HTML.

It was this latter feature that conjured a community of its own. For security reasons, any post using HTML was passed through a sanitizer to remove any malicious or malformed elements. But unlike most websites, Cohost’s sanitizer was remarkably permissive. The vast majority of tags and attributes were allowed — most notably inline CSS styles on arbitrary elements.

Users didn’t take long to grasp the creative opportunities lurking within Cohost’s unassuming “new post” modal. Within 48 hours of going public, the fledgling community had figured out how to post poetry using the <details> tag, port the Apple homepage from 1999, and reimplement a quick-time WarioWare game. We called posts like these “CSS Crimes,” and the people who made them “CSS Criminals.” Without even intending to, the developers of Cohost had created an environment for a CSS community to thrive.

In this post, I’ll show you a few of the hacks we found while trying to push the limits of Cohost’s HTML support. Use these if you dare, lest you too get labelled a CSS criminal.

Width-hacking

Many of the CSS crimes of Cohost were powered by a technique that user @corncycle dubbed “width-hacking.” Using a combination of the <details> element and the CSS calc() function, we can get some pretty wild functionality: combination lockstile matching games, Zelda-style top-down movement, the list goes on.

If you’ve been around the CSS world for a while, there’s a good chance you’ve been exposed to the old checkbox hack. By combining a checkbox, a label, and creative use of CSS selectors, you can use the toggle functionality of the checkbox to implement all sorts of things. Tabbed areas, push toggles, dropdown menus, etc.

However, because this hack requires CSS selectors, that meant we couldn’t use it on Cohost — remember, we only had inline styles. Instead, we used the relatively new elements <details> and <summary>. These elements provide the same visibility-toggling logic, but now directly in HTML. No weird CSS needed.

These elements work like so: All children of the <details> element are hidden by default, except for the <summary> element. When the summary is clicked, it “opens” the parent details element, causing its children to become visible.

We can add all sorts of styles to these elements to make this example more interesting. Below, I have styled the constituent elements to create the effect of a button that lights up when you click on it.

This is achieved by giving the <summary> element a fixed position and size, a grey background color, and an outset border to make it look like a button. When it’s clicked, a sibling <div> is revealed that covers the <summary> with its own red background and border. Normally, this <div> would block further click events, but I’ve given it the declaration pointer-events: none. Now all clicks pass right on through to the <summary> element underneath, allowing you to turn the button back off.

This is all pretty nifty, but it’s ultimately the same logic as before: something is toggled either on or off. These are only two states. If we want to make games and other gizmos, we might want to represent hundreds to thousands of states.

Width-hacking gives us exactly that. Consider the following example:

In this example, three <details> elements live together in an inline-flex container. Because all the <summary> elements are absolutely-positioned, the width of their respective <details> elements are all zero when they’re closed.

Now, each of these three <details> has a small <div> inside. The first has a child with a width of 1px, the second a child with a width of 2px, and the third a width of 4px. When a <details> element is opened, it reveals its hidden <div>, causing its own width to increase. This increases the width of the inline-flex container. Because the width of the container is the sum of its children, this means its width directly corresponds to the specific <details> elements that are open.

For example, if just the first and third <details> are open, the inline-flex container will have the width 1px + 4px = 5px. Conversely, if the inline-flex container is 2px wide, we can infer that the only open <details> element is the second one. With this trick, we’ve managed to encode all eight states of the three <details> into the width of the container element.

This is pretty cool. Maybe we could use this as an element of some kind of puzzle game? We could show a secret message if the right combination of buttons is checked. But how do we do that? How do we only show the secret message for a specific width of that container div?

In the preceding CodePen, I’ve added a secret message as two nested divs. Currently, this message is always visible — complete with a TODO reminding us to implement the logic to hide it unless the correct combination is set.

You may wonder why we’re using two nested divs for such a simple message. This is because we’ll be hiding the message using a peculiar method: We will make the width of the parent div.secret be zero. Because the overflow: hidden property is used, the child div.message will be clipped, and thus invisible.

Now we’re ready to implement our secret message logic. Thanks to the fact that percentage sizes are relative to the parent, we can use 100% as a stand-in for the parent’s width. We can then construct a complicated CSS calc() formula that is 350px if the container div is our target size, and 0px otherwise. With that, our secret message will be visible only when the center button is active and the others are inactive. Give it a try!

This complicated calc() function that’s controlling the secret div’s width has the following graph:

Line chart showing the width of the secret div when the container div is at different widths.

You can see that it’s a piecewise linear curve, constructed from multiple pieces using min/max. These pieces are placed in just the right spots so that the function maxes out when the container div is 2px— which we’ve established is precisely when only the second button is active.

A surprising variety of games can be implemented using variations on this technique. Here is a tower of Hanoi game I had made that uses both width and height to track the game’s state.

SVG animation

So far, we’ve seen some basic functionality for implementing a game. But what if we want our games to look good? What if we want to add ✨animations?✨ Believe it or not, this is actually possible entirely within inline CSS using the power of SVG.

SVG (Scalable Vector Graphics) is an XML-based image format for storing vector images. It enjoys broad support on the web — you can use it in <img> elements or as the URL of a background-image property, among other things.

Like HTML, an SVG file is a collection of elements. For SVG, these elements are things like <rect><circle>, and <text>, to name a few. These elements can have all sorts of properties defined, such as fill color, stroke width, and font family.

A lesser-known feature of SVG is that it can contain <style> blocks for configuring the properties of these elements. In the example below, an SVG is used as the background for a div. Inside that SVG is a <style> block that sets the fillcolor of its <circle> to red.

An even lesser-known feature of SVG is that its styles can use media queries. The size used by those queries is the size of the div it is a background of.

In the following example, we have a resizable <div> with an SVG background. Inside this SVG is a media query which will change the fill color of its <circle> to blue when the width exceeds 100px. Grab the resize handle in its bottom right corner and drag until the circle turns blue.

Because resize handles don’t quite work on mobile, unfortunately, this and the next couple of CodePens are best experienced on desktop.

This is an extremely powerful technique. By mixing it with width-hacking, we could encode the state of a game or gizmo in the width of an SVG background image. This SVG can then show or hide specific elements depending on the corresponding game state via media queries.

But I promised you animations. So, how is that done? Turns out you can use CSS animations within SVGs. By using the CSS transition property, we can make the color of our circle smoothly transition from red to blue.

Amazing! But before you try this yourself, be sure to look at the source code carefully. You’ll notice that I’ve had to add a 1×1px, off-screen element with the ID #hack. This element has a very simple (and nearly unnoticeable) continuous animation applied. A “dummy animation” like this is necessary to get around some web browsers’ buggy detection of SVG animation. Without that hack, our transition property wouldn’t work consistently.

For the fun of it, let’s combine this tech with our previous secret message example. Instead of toggling the secret message’s width between the values of 0px and 350px, I’ve adjusted the calc formula so that the secret message div is normally 350px, and becomes 351px if the right combination is set.

Instead of HTML/CSS, the secret message is now just an SVG background with a <text> element that says “secret message.” Using media queries, we change the transform scale of this <text> to be zero unless the div is 351px. With the transition property applied, we get a smooth transition between these two states.

Click the center button to activate the secret message:

The first cohost user to discover the use of media queries within SVG backgrounds was @ticky for this post. I don’t recall who figured out they could animate, but I used the tech quite extensively for this quiz that tells you what kind of soil you’d like if you were a worm.

Wrapping up

And that’s will be all for now. There are a number of techniques I haven’t touched on — namely the fun antics one can get up to with the resize property. If you’d like to explore the world of CSS crimes further, I’d recommend this great linkdump by YellowAfterlife, or this video retrospective by rebane2001.


It will always hurt to describe Cohost in the past tense. It truly was a magical place, and I don’t think I’ll be able to properly convey what it was like to be there at its peak. The best I can do is share the hacks we came up with: the lost CSS tricks we invented while “posting, but better.”


The Lost CSS Tricks of Cohost.org originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Important Information

Terms of Use Privacy Policy Guidelines We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.