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

    195
  • Comments

    0
  • Views

    3387

Entries in this blog

by: Chris Coyier
Mon, 30 Jun 2025 17:04:57 +0000


Mr. Brad Frost, and his brother Ian, have a new course they are selling called Subatomic: The Complete Guide to Design Tokens.

To be honest, I was a smidge skeptical. I know what a design token is. It’s a variable of a color or font-family or something. I pretty much only work on websites, so that exposes itself as a --custom-property and I already know that using those to abstract common usage of colors and fonts is smart and helpful. Done. I get that people managing a whole fleet of sites (and apps running in who-knows-what technologies) need a fancier token system, but that ain’t me.

But then we had those fellas on ShopTalk Show and I’ve updated my thinking that you really do want to lean on the expertise of people that have done this time and time again at scale.

(p.s. they also gave us a 20% discount code when they were on the show: SHOPTALKSHOWISAWESOME)

Spoiler: they advocate for a three-tier system of custom properties. The first is just the raw ingredients. Colors, but you’re just naming the color; sizes, but you’re just naming the sizes. Then there is this middle tier where you are essentially crafting a theme from those raw ingredients. And this layer is the most important, as it gives you this perfect layer of abstraction where you’re both not reaching into the raw ingredients and you’re not being too overly specific, like naming individual parts of components. The third layer should be avoided as best as it can, but if you absolutely need to get hyper specific, this is where you do it, and are still keeping in the land of custom properties.

This feels particularly smart to me, and I wish I had the benefit of the Frost Brother’s expertise on this before building some custom property systems I have built in the past. I tend to have that first layer with just the raw ingredients, and then jump right to what they’d call the third tier, which leads to a real blowing up of how many custom properties are in use, to the point that it feels overly cumbersome and like the whole system isn’t even helping that much. I’ll definitely be thinking about the theming tier next time I have a good refactoring opportunity.

Brad has also been steady on his global design system idea. I’ve posted my thoughts on this before, but I keep coming back to this one:

It feels like every decision you make will chip away at who will use it.

I’m fascinated at seeing how decisions get made that keep this thing as “global” as possible. That absolutely must be done, otherwise it’s just another design system which I thinks falls short of the goal. I appreciated Brian’s deep thoughts on it all as well, and I’m basically writing all this as an excuse to link to that.

Would a global design system have any design to it at all? Maybe; maybe not. It makes me wonder if the era of “flat design” that it seems like we’ve been in for a decade or so was partially the result of design systems, where the simpler things look the more practical it is to build all the “lego blocks” of a cohesive aesthetic. But it’s likely design trends move on. Maybe flat is over. Are design systems ready for very fancy/complex looks? Definitely worth a read is Amelia’s thoughts on “balancing the hard structure and soft flexibility” of UIs.

Speaking of design tokens, designtokens.fyi is a nice site for defining all the terms that design systems/tokens people like to throw around. A site with word definitions can be awfully boring so I appreciate the fun design here. I like the idea of calling a value system a “t-shirt” where you’re actually defining, say, a set of padding options, but the options follow the mental model of t-shirt sizes.

Sometimes you just need to look and see what other people are doing. In design, there always has been and will be design galleries full of inspirational stuff. But instead of linking to one of those, I’m going to link to to the “Home of the internet’s finest website headlines.” I love a good headline, myself. I’ve seen far too many sites that do a terrible job of just saying what their point is.

by: Zell Liew
Mon, 30 Jun 2025 13:16:43 +0000


Adam Wathan has (very cleverly) built Tailwind with CSS Cascade Layers, making it extremely powerful for organizing styles by priority.

@layer theme, base, components, utilities;
@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/utilities.css' layer(utilities);

The core of Tailwind are its utilities. This means you have two choices:

  1. The default choice
  2. The unorthodox choice

The default choice

The default choice is to follow Tailwind’s recommended layer order: place components first, and Tailwind utilities last.

So, if you’re building components, you need to manually wrap your components with a @layer directive. Then, overwrite your component styles with Tailwind, putting Tailwind as the “most important layer”.

/* Write your components */
@layer components {
  .component {
    /* Your CSS here */
  }
}
<!-- Override with Tailwind utilities --> 
<div class="component p-4"> ... </div>

That’s a decent way of doing things.

But, being the bad boy I am, I don’t take the default approach as the “best” one. Over a year of (major) experimentation with Tailwind and vanilla CSS, I’ve come across what I believe is a better solution.

The Unorthodox Choice

Before we go on, I have to tell you that I’m writing a course called Unorthodox Tailwind — this shows you everything I know about using Tailwind and CSS in synergistic ways, leveraging the strengths of each.

Shameless plug aside, let’s dive into the Unorthodox Choice now.

In this case, the Unorthodox Choice is to write your styles in an unnamed layer — or any layer after utilities, really — so that your CSS naturally overwrites Tailwind utilities.

Of these two, I prefer the unnamed layer option:

/* Unnamed layer option */
@layer theme, base, components, utilities; 

/* Write your CSS normally here */ 
.component { /* ... */ }
/* Named layer option */
/* Use whatever layer name you come up with. I simply used css here because it made most sense for explaining things */
@layer theme, base, components, utilities, css; 

@layer css {
  .component { /* ... */ }
}

I have many reasons why I do this:

  1. I don’t like to add unnecessary CSS layers because it makes code harder to write — more keystrokes, having to remember the specific layer I used it in, etc.
  2. I’m pretty skilled with ITCSS, selector specificity, and all the good-old-stuff you’d expect from a seasoned front-end developer, so writing CSS in a single layer doesn’t scare me at all.
  3. I can do complex stuff that are hard or impossible to do in Tailwind (like theming and animations) in CSS.

Your mileage may vary, of course.

Now, if you have followed my reasoning so far, you would have noticed that I use Tailwind very differently:

  • Tailwind utilities are not the “most important” layer.
  • My unnamed CSS layer is the most important one.

I do this so I can:

  • Build prototypes with Tailwind (quickly, easily, especially with the tools I’ve created).
  • Shift these properties to CSS when they get more complex — so I don’t have to read messy utility-littered HTML that makes my heart sink. Not because utility HTML is bad, but because it takes lots of brain processing power to figure out what’s happening.

Finally, here’s the nice thing about Tailwind being in a utility layer: I can always !important a utility to give it strength.

<!-- !important the padding utility -->
<div class="component !p-4"> ... </div>

Whoa, hold on, wait a minute! Isn’t this wrong, you might ask?

Nope. The !important keyword has traditionally been used to override classes. In this case, we’re leveraging on the !important feature in CSS Layers to say the Tailwind utility is more important than any CSS in the unnamed layer.

This is perfectly valid and is a built-in feature for CSS Layers.

Besides, the !important is so explicit (and used so little) that it makes sense for one-off quick-and-dirty adjustments (without creating a brand new selector for it).

Tailwind utilities are more powerful than they seem

Tailwind utilities are not a 1:1 map between a class and a CSS property. Built-in Tailwind utilities mostly look like this so it can give people a wrong impression.

Tailwind utilities are more like convenient Sass mixins, which means we can build effective tools for layouts, theming, typography, and more, through them.

You can find out about these thoughts inside Unorthodox Tailwind.

Thanks for reading and I hope you’re enjoying a new way of looking at (or using) Tailwind!


Using CSS Cascade Layers With Tailwind Utilities originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Neeraj Mishra
Fri, 27 Jun 2025 17:48:27 +0000


MetaTrader 5 (MT5) is an advanced trading platform supporting a multitude of different assets like Forex, cryptos, commodities, and so on. It is incredibly popular among Japanese traders and regulated brokers. Many programmers in Japan are employing its MQL5 programming language to develop advanced trading algorithms and we are going to explain how they are using MT5 for advanced algorithm development and trading below.

Identical syntax to C/C++

MT5 is free and offered by many reputable brokers that are regulated in Japan, making it a simple process to use the platform’s advanced features. 

The main advantage of MQL5 is its similarity to the popular programming language C++, which makes it very easy to adopt and learn. The syntax of MQL5 is nearly identical and data types are also familiar, like int, double, char, bool, and string. Functions are declared and used the same way, and MQL5 also supports classes, inheritance, and other OOP (Object-Oriented Programming) objects like C++. You can also pass parameters by reference using &.

MQL5 programming language

Integrated IDE

MetaEditor, which is a native integrated development environment, is built into the MT5 trading platform. This is super flexible as users can switch back and forth between MT5 and MetaEditor with just one click of a mouse or F4 button.

After programming in the MQL5 editor, users can switch back to the MT5 platform quickly and test their indicators or Expert Advisors (EAs) using the strategy editor.

No need for APIs

The pricing data is also provided directly by the broker to your MT5 platform and when testing the algorithm, there is a strategy tester plugin on MT5 to test EAs. There is no need for API calls and other functions which makes the whole process not only comfortable but also very fast.

Built-in functions

Instead of writing your own low-level hardware codes, MQL5 comes with built-in functions like:

  • OrderSend() to open trades
  • iMA() – to call indicators like moving averages.
  • SymbolInfoDouble()

All built-in indicators come with built-in functions which makes it very comfortable to summon them in your EA.

Unlike other platforms or programming languages, developers do not need to construct candle data or anything. Instead, just apply your EA to your preferred instrument, timeframe, and chart types and it’s ready to go.

Push notifications and alerts

MQL5 comes with several alert functions which enable notifications. Users can define where their EAs will send notifications when predefined events occur. SMS, email, and platform alerts are all supported to develop powerful trading algorithms.

Faster trade execution and social features

MT5 supports even faster trade execution natively which is perfect for HFT and other algorithms that rely on fast trade execution for profits. Users can deploy their EA and be sure that it can open and close trades in milliseconds which enables them to deploy a wide range of trading strategies, including arbitrage and scalping techniques.

Trading signals and community integration

The platform integrates copy trading and community features. Traders can easily use copy trading services while developers can develop and sell their EAs to generate passive income. MT5 provides direct access to the MQL5.com community from the platform which makes it very easy to use EAs from the official store. Developers can deploy their EAs in the store to generate revenue which makes it very lucrative to learn and code robots.

Large community

Are newcomers to MT5 and MQL5? Then there is good news for you. There is a plethora of educational content provided freely on MQL5 forums where even beginners can learn MQL5 and MT5 programming. The built-in chat system enables communication with other users as well.

Free EAs and custom indicators

Another big advantage for Japanese programmers is the availability of free EAs and custom indicators. There is an online store to upload and sell or rent your algorithms, which is very flexible. The platform also supports scripts and utilities and by using a free code base, developers can quickly find complex functions and use them easily to enhance their algorithms and reduce time needed for development.

The bottom line

Japanese traders choose MT5 for algorithmic trading because it combines a familiar C/C++-like language (MQL5) with fully integrated IDE (MetaEditor) and built-in data feeds. MQL5 provides an extensive library of functions and supports OOP. As a result, Japanese developers can develop complex Expert Advisors with ease. Real-time alerts, and ultra-low latency trade execution, make MT5 perfect for scalping algorithms.

Overall, MT5’s MQL5 provides an all-in-one solution to develop, test, and sell or rent EAs quickly.

The post How Japanese Programmers Are Leveraging MT5 for Advanced Algorithmic Trading appeared first on The Crazy Programmer.

CSS Blob Recipes

by: Juan Diego Rodríguez
Fri, 27 Jun 2025 13:48:41 +0000


Blob, Blob, Blob. You hate them. You love them. Personally, as a design illiterate, I like to overuse them… a lot. And when you repeat the same process over and over again, it’s only a question of how much you can optimize it, or in this case, what’s the easiest way to create blobs in CSS? Turns out, as always, there are many approaches.

To know if our following blobs are worth using, we’ll need them to pass three tests:

  1. They can be with just a single element (and preferably without pseudos).
  2. They can be easily designed (ideally through an online tool).
  3. We can use gradient backgrounds, borders, shadows, and other CSS effects on them.

Without further ado, let’s Blob, Blob, Blob right in.

Just generate them online

I know it’s disenchanting to click on an article about making blobs in CSS just for me to say you can generate them outside CSS. Still, it’s probably the most common way to create blobs on the web, so to be thorough, these are some online tools I’ve used before to create SVG blobs.

  • Haikei. Probably the one I have used the most since, besides blobs, it can also generate lots of SVG backgrounds.
  • Blobmaker. A dedicated tool for making blobs. It’s apparently part of Haikei now, so you can use both.
  • Lastly, almost all graphic programs let you hand-draw blobs and export them as SVGs.

For example, this is one I generated just now. Keep it around, as it will come in handy later.

Randomly shaped blob in bright red.
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
  <path
    fill="#FA4D56"
    d="M65.4,-37.9C79.2,-13.9,81,17,68.1,38C55.2,59.1,27.6,70.5,1.5,69.6C-24.6,68.8,-49.3,55.7,-56,38.2C-62.6,20.7,-51.3,-1.2,-39,-24.4C-26.7,-47.6,-13.3,-72,6.2,-75.6C25.8,-79.2,51.6,-62,65.4,-37.9Z"
    transform="translate(100 100)"
  />
</svg>

Using border-radius

While counterintuitive, we can use the border-radius property to create blobs. This technique isn’t new by any means; it was first described by Nils Binder in 2018, but it is still fairly unknown. Even for those who use it, the inner workings are not entirely clear.

To start, you may know the border-radius is a shorthand to each individual corner’s radius, going from the top left corner clockwise. For example, we can set each corner’s border-radius to get a bubbly square shape:

<div class="blob"></div>
.blob {
  border-radius: 25% 50% 75% 100%;
}

However, what border-radius does — and also why it’s called “radius” — is to shape each corner following a circle of the given radius. For example, if we set the top left corner to 25%, it will follow a circle with a radius 25% the size of the shape.

.blob {
  border-top-left-radius: 25%;
}

What’s less known is that each corner property is still a shortcut towards its horizontal and vertical radii. Normally, you set both radii to the same value, getting a circle, but you can set them individually to create an ellipse. For example, the following sets the horizontal radius to 25% of the element’s width and the vertical to 50% of its height:

.blob {
  border-top-left-radius: 25% 50%;
}

We can now shape each corner like an ellipse, and it is the combination of all four ellipses that creates the illusion of a blob! Just take into consideration that to use the horizontal and vertical radii syntax through the border-radius property, we’ll need to separate the horizontal from the vertical radii using a forward slash (/).

.blob {
  border-radius:
    /* horizontal */
    100% 30% 60% 70% /
    /* vertical */
    50% 40% 70% 70%;
}

The syntax isn’t too intuitive, so designing a blob from scratch will likely be a headache. Luckily, Nils Binder made a tool exactly for that!

Blobbing blobs together

This hack is awesome. We aren’t supposed to use border-radius like that, but we still do. Admittedly, we are limited to boring blobs. Due to the nature of border-radius, no matter how hard we try, we will only get convex shapes.

Concave and convex shapes

Just going off border-radius, we can try to minimize it a little by sticking more than one blob together:

However, I don’t want to spend too much time on this technique since it is too impractical to be worth it. To name a few drawbacks:

  1. We are using more than one element or, at the very least, an extra pseudo-element. Ideally, we want to keep it to one element.
  2. We don’t have a tool to prototype our blobby amalgamations, so making one is a process of trial and error.
  3. We can’t use borders, gradients, or box shadows since they would reveal the element’s outlines.

Multiple backgrounds and SVG filters

This one is an improvement in the Gooey Effect, described here by Lucas Bebber, although I don’t know who first came up with it. In the original effect, several elements can be morphed together like drops of liquid sticking to and flowing out of each other:

It works by first blurring shapes nearby, creating some connected shadows. Then we crank up the contrast, forcing the blur out and smoothly connecting them in the process. Take, for example, this demo by Chris Coyer (It’s from 2014, so more than 10 years ago!):

If you look at the code, you’ll notice Chris uses the filter property along the blur() and contrast() functions, which I’ve also seen in other blob demos. To be specific, it applies blur() on each individual circle and then contrast() on the parent element. So, if we have the following HTML:

<div class="blob">
  <div class="subblob"></div>
  <div class="subblob"></div>
  <div class="subblob"></div>
</div>

…we would need to apply filters and background colors as such:

.blob {
  filter: contrast(50);
  background: white; /* Solid colors are necessary */
}

.subblob {
  filter: blur(15px);
  background: black; /* Solid colors are necessary */
}

However, there is a good reason why those demos stick to white shapes and black backgrounds (or vice versa) since things get unpredictable once colors aren’t contrast-y enough. See it for yourself in the following demo by changing the color. Just be wary: shades get ugly.

To solve this, we will use an SVG filter instead. I don’t want to get too technical on SVG (if you want to, read Luca’s post!). In a nutshell, we can apply blurring and contrast filters using SVGs, but now, we can also pick which color channel we apply the contrast to, unlike normal contrast(), which modifies all colors.

Since we want to leave color channels (RG and B) untouched, we will only crank the contrast up for the alpha channel. That translates to the next SVG filter, which can be embedded in the HTML:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" style="position: absolute;">
  <defs>
    <filter id="blob">
      <feGaussianBlur in="SourceGraphic" stdDeviation="12" result="blur" />
      <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -6" result="goo" />
      <feBlend in="SourceGraphic" in2="blob" />
    </filter>
  </defs>
</svg>

To apply it, we will use again filter, but this time we’ll set it to url("#blob"), so that it pulls the SVG from the HTML.

.blob {
  filter: url("#blob");
}

And now we can even use it with gradient backgrounds!

That being said, this approach comes with two small, but important, changes to common CSS filters:

  1. The filter is applied to the parent element, not the individual shapes.
  2. The parent element must be transparent (which is a huge advantage). To change the background color, we can instead change the body or other ancestors’ background, and it will work with no issues.

What’s left is to place the .subblob elements together such that they make a blobby enough shape, then apply the SVG filters to morph them:

Making it one element

This works well, but it has a similar issue to the blob we made by morphing several border-radius instances: too many elements for a simple blob. Luckily, we can take advantage of the background property to create multiple shapes and morph them together using SVG filters, all in a single element. Since we are keeping it to one element, we will go back to just one empty .blob div:

<div class="blob"></div>

To recap, the background shorthand can set all background properties and also set multiple backgrounds at once. Of all the properties, we only care about the background-imagebackground-position and background-size.

First, we will use background-image along with radial-gradient() to create a circle inside the element:

body {
  background: radial-gradient(farthest-side, var(--blob-color) 100%, #0000);
  background-repeat: no-repeat; /* Important! */
}

Here is what each parameter does:

  • farthest-side: Confines the shape to the element’s box farthest from its center. This way, it is kept as a circle.
  • var(--blob-color) 100%: Fills the background shape from 0 to 100% with the same color, so it ends up as a solid color.
  • #0000: After the shape is done, it makes a full stop to transparency, so the color ends.

The next part is moving and resizing the circle using the background-position and background-size properties. Luckily, both can be set on background after the gradient, separated from each other by a forward slash (/).

body {
  background: radial-gradient(...) 20% 30% / 30% 40%;
  background-repeat: no-repeat; /* Important! */
}

The first pair of percentages sets the shape’s horizontal and vertical position (taking as a reference the top-left corner), while the second pair sets the shape’s width and height (taking as a reference the element’s size).

As I mentioned, we can stack up different backgrounds together, which means we can create as many circles/ellipses as we want! For example, we can create three ellipses on the same element:

.blob {
  background:
    radial-gradient(farthest-side, var(--blob-color) 100%, #0000) 20% 30% / 30% 40%, 
    radial-gradient(farthest-side, var(--blob-color) 100%, #0000) 80% 50% / 40% 60%, 
    radial-gradient(farthest-side, var(--blob-color) 100%, #0000) 50% 70% / 50% 50%;
  background-repeat: no-repeat;
}

What’s even better is that SVG filters don’t care whether shapes are made of elements or backgrounds, so we can also morph them together using the last url(#blob) filter!

While this method may be a little too much for blobs, it unlocks squishing, stretching, dividing, and merging blobs in seamless animations.

Again, all these tricks are awesome, but not enough for what we want! We accomplished reducing the blob to a single element, but we still can’t use gradients, borders, or shadows on them, and also, they are tedious to design and model. Then, that brings us to the ultimate blob approach…

Using the shape() function

Fortunately, there is a new way to make blobs that just dropped to CSS: the shape() function!

I’ll explain shape()‘s syntax briefly, but for an in-depth explanation, you’ll want to check out both this explainer from the CSS-Tricks Almanac as well as Temani Afif‘s three-part series on the shape() function, as well as his recent article about blobs.

First off, the CSS shape() function is used alongside the clip-path property to cut elements into any shape we want. More specifically, it uses a verbal version of SVG’s path syntax. The syntax has lots of commands for lots of types of lines, but when blobbing with shape(), we’ll define curves using the curve command:

.blob {
  clip-path: shape(
    from X0 Y0, 
    curve to X1 Y1 with Xc1 Yc1, 
    curve to X2 Y2 with Xc21 Yc21 / Xc22 Yc22
    /* ... */
  );
}

Let’s break down each parameter:

  • X0 Y0 defines the starting point of the shape.
  • curve starts the curve where X1 Y1 is the next point of the shape, while Xc1 Yc1 defines a control point used in Bézier curves.
  • The next parameter is similar, but we used Xc21 Yc21 / Xc22 Yc22 instead to define two control points on the Bézier curve.

I honestly don’t understand Bézier curves and control points completely, but luckily, we don’t need them to use shape() and blobs! Again, shape() uses a verbal version of SVG’s path syntax, so it can draw any shape an SVG can, which means that we can translate the SVG blobs we generated earlier… and CSS-ify them. To do so, we’ll grab the d attribute (which defines the path) from our SVG and paste it into Temani’s SVG to shape() generator.

This is the exact code the tool generated for me:

.blob {
  aspect-ratio: 0.925; /* Generated too! */

  clip-path: shape(
    from 91.52% 26.2%,
    curve to 93.52% 78.28% with 101.76% 42.67%/103.09% 63.87%,
    curve to 44.11% 99.97% with 83.95% 92.76%/63.47% 100.58%,
    curve to 1.45% 78.42% with 24.74% 99.42%/6.42% 90.43%,
    curve to 14.06% 35.46% with -3.45% 66.41%/4.93% 51.38%,
    curve to 47.59% 0.33% with 23.18% 19.54%/33.13% 2.8%,
    curve to 91.52% 26.2% with 62.14% -2.14%/81.28% 9.66%
  );
}

As you might have guessed, it returns our beautiful blob:

Let’s check if it passes our requirements:

  1. Yes, they can be made of a single element.
  2. Yes, they can also be created in a generator and then translated into CSS.
  3. Yes, we can use gradient backgrounds, but due to the nature of clip-path(), borders and shadows get cut out.

Two out of three? Maybe two and a half of three? That’s a big improvement over the other approaches, even if it’s not perfect.

Conclusion

So, alas, we failed to find what I believe is the perfect CSS approach to blobs. I am, however, amazed how something so trivial designing blobs can teach us about so many tricks and new CSS features, many of which I didn’t know myself.


CSS Blob Recipes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

KelpUI

by: Geoff Graham
Thu, 26 Jun 2025 16:42:32 +0000


KelpUI is new library that Chris Ferdinandi is developing, designed to leverage newer CSS features and Web Components. I’ve enjoyed following Chris as he’s published an ongoing series of articles detailing his thought process behind the library, getting deep into his approach. You really get a clear picture of his strategy and I love it.

He outlined his principles up front in a post back in April:

I’m imagining a system that includes…

  • Base styles for all of the common HTML elements.
  • Loads of utility classes for nudging and tweaking things.
  • Group classes for styling more complex UI elements without a million little classes.
  • Easy customization with CSS variables.
  • Web Components to progressively add interactivity to functional HTML.
  • All of the Web Component HTML lives in the light DOM, so its easy to style and reason about.

I’m imagining something that can be loaded directly from a CDN, downloaded locally, or imported if you want to roll your own build.

And that’s what I’ve seen so far. The Cascade is openly embraced and logically structured with Cascade Layers. Plenty of utility classes are included, with extra care put into how they are named. Selectors are kept simple and specificity is nice and low, where needed. Layouts are flexible with good constraints. Color palettes are accessible and sport semantic naming.

Chris has even put a ton of thought into how KelpUI is licensed.

KelpUI is still evolving, and that’s part of the beauty of looking at it now and following Chris’s blog as he openly chronicles his approach. There’s always going to be some opinionated directions in a library like this, but I love that the guiding philosophy is so clear and is being used as a yardstick to drive decisions. As I write this, Chris is openly questioning the way he optimizes the library, demonstrating the tensions between things like performance and a good developer experience.

Looks like it’ll be a good system, but even more than that, it’s a wonderful learning journey that’s worth following.


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

by: Daniel Schwarz
Wed, 25 Jun 2025 14:33:45 +0000


Chrome 137 shipped the if() CSS function, so it’s totally possible we’ll see other browsers implement it, though it’s tough to know exactly when. Whatever the case, if() enables us to use values conditionally, which we can already do with queries and other functions (e.g., media queries and the light-dark() function), so I’m sure you’re wondering: What exactly does if() do?

Sunkanmi gave us a nice overview of the function yesterday, poking at the syntax at a high level. I’d like to poke at it a little harder in this article, getting into some possible real-world usage.

To recap, if() conditionally assigns a value to a property based on the value of a CSS variable. For example, we could assign different values to the color and background properties based on the value of --theme:

  • --theme: "Shamrock"
    • color: ‌hsl(146 50% 3%)
    • background: hsl(146 50% 40%)
  • --theme: Anything else
    • color: hsl(43 74% 3%)
    • background: hsl(43 74% 64%)
:root {
  /* Change to fall back to the ‘else’ values */
  --theme: "Shamrock";

  body {
    color: if(style(--theme: "Shamrock"): hsl(146 50% 3%); else: hsl(43 74% 3%));
    background: if(style(--theme: "Shamrock"): hsl(146 50% 40%); else: hsl(43 74% 64%));
  }
}

I don’t love the syntax (too many colons, brackets, and so on), but we can format it like this (which I think is a bit clearer):

color: if(
  style(--theme: "Shamrock"): hsl(146 50% 3%);
  else: hsl(43 74% 3%)
);

We should be able to do a crazy number of things with if(), and I hope that becomes the case eventually, but I did some testing and learned that the syntax above is the only one that works. We can’t base the condition on the value of an ordinary CSS property (instead of a custom property), HTML attribute (using attr()), or any other value. For now, at least, the condition must be based on the value of a custom property (CSS variable).

Exploring what we can do with if()

Judging from that first example, it’s clear that we can use if() for theming (and design systems overall). While we could utilize the light-dark() function for this, what if the themes aren’t strictly light and dark, or what if we want to have more than two themes or light and dark modes for each theme? Well, that’s what if() can be used for.

First, let’s create more themes/more conditions:

:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Saffron"; /* ...I choose you! */

  body {
    color: if(
      style(--theme: "Shamrock"): hsl(146 50% 3%);
      style(--theme: "Saffron"): hsl(43 74% 3%);
      style(--theme: "Amethyst"): hsl(282 47% 3%)
    );
    background: if(
      style(--theme: "Shamrock"): hsl(146 50% 40%);
      style(--theme: "Saffron"): hsl(43 74% 64%);
      style(--theme: "Amethyst"): hsl(282 47% 56%)
    );
    transition: 300ms;
  }
}

Pretty simple really, but there are a few easy-to-miss things. Firstly, there’s no “else condition” this time, which means that if the theme isn’t Shamrock, Saffron, or Amethyst, the default browser styles are used. Otherwise, the if() function resolves to the value of the first true statement, which is the Saffron theme in this case. Secondly, transitions work right out of the box; in the demo below, I’ve added a user interface for toggling the --theme, and for the transition, literally just transition: 300ms alongside the if() functions:

Note: if theme-swapping is user-controlled, such as selecting an option, you don’t actually need if() at all. You can just use the logic that I’ve used at the beginning of the demo (:root:has(#shamrock:checked) { /* Styles */ }). Amit Sheen has an excellent demonstration over at Smashing Magazine.

To make the code more maintainable though, we can slide the colors into CSS variables as well, then use them in the if() functions, then slide the if() functions themselves into CSS variables:

/* Setup */
:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Shamrock"; /* ...I choose you! */

  /* Base colors */
  --shamrock: hsl(146 50% 40%);
  --saffron: hsl(43 74% 64%);
  --amethyst: hsl(282 47% 56%);

  /* Base colors, but at 3% lightness */
  --shamrock-complementary: hsl(from var(--shamrock) h s 3%);
  --saffron-complementary: hsl(from var(--saffron) h s 3%);
  --amethyst-complementary: hsl(from var(--amethyst) h s 3%);

  --background: if(
    style(--theme: "Shamrock"): var(--shamrock);
    style(--theme: "Saffron"): var(--saffron);
    style(--theme: "Amethyst"): var(--amethyst)
  );

  --color: if(
    style(--theme: "Shamrock"): var(--shamrock-complementary);
    style(--theme: "Saffron"): var(--saffron-complementary);
    style(--theme: "Amethyst"): var(--amethyst-complementary)
  );

  /* Usage */
  body {
    /* One variable, all ifs! */
    background: var(--background);
    color: var(--color);
    accent-color: var(--color);

    /* Can’t forget this! */
    transition: 300ms;
  }
}

As well as using CSS variables within the if() function, we can also nest other functions. In the example below, I’ve thrown light-dark() in there, which basically inverts the colors for dark mode:

--background: if(
  style(--theme: "Shamrock"): light-dark(var(--shamrock), var(--shamrock-complementary));
  style(--theme: "Saffron"): light-dark(var(--saffron), var(--saffron-complementary));
  style(--theme: "Amethyst"): light-dark(var(--amethyst), var(--amethyst-complementary))
);

if() vs. Container style queries

If you haven’t used container style queries before, they basically check if a container has a certain CSS variable (much like the if() function). Here’s the exact same example/demo but with container style queries instead of the if() function:

:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Shamrock"; /* ...I choose you! */

  --shamrock: hsl(146 50% 40%);
  --saffron: hsl(43 74% 64%);
  --amethyst: hsl(282 47% 56%);

  --shamrock-complementary: hsl(from var(--shamrock) h s 3%);
  --saffron-complementary: hsl(from var(--saffron) h s 3%);
  --amethyst-complementary: hsl(from var(--amethyst) h s 3%);

  body {
    /* Container has chosen Shamrock! */
    @container style(--theme: "Shamrock") {
      --background: light-dark(var(--shamrock), var(--shamrock-complementary));
      --color: light-dark(var(--shamrock-complementary), var(--shamrock));
    }

    @container style(--theme: "Saffron") {
      --background: light-dark(var(--saffron), var(--saffron-complementary));
      --color: light-dark(var(--saffron-complementary), var(--saffron));
    }

    @container style(--theme: "Amethyst") {
      --background: light-dark(var(--amethyst), var(--amethyst-complementary));
      --color: light-dark(var(--amethyst-complementary), var(--amethyst));
    }

    background: var(--background);
    color: var(--color);
    accent-color: var(--color);
    transition: 300ms;
  }
}

As you can see, where if() facilitates conditional values, container style queries facilitate conditional properties and values. Other than that, it really is just a different syntax.

Additional things you can do with if() (but might not realize)

Check if a CSS variable exists:

/* Hide icons if variable isn’t set */
.icon {
  display: if(
    style(--icon-family): inline-block;
    else: none
  );
}

Create more-complex conditional statements:

h1 {
  font-size: if(
    style(--largerHeadings: true): xxx-large;
    style(--theme: "themeWithLargerHeadings"): xxx-large
  );
}

Check if two CSS variables match:

/* If #s2 has the same background as #s1, add a border */
#s2 {
  border-top: if(
    style(--s2-background: var(--s1-background)): thin solid red
  );
}

if() and calc(): When the math isn’t mathing

This won’t work (maybe someone can help me pinpoint why):

div {
  /* 3/3 = 1 */
  --calc: calc(3/3);
  /* Blue, because if() won’t calculate --calc */
  background: if(style(--calc: 1): red; else: blue);
}

To make if() calculate --calc, we’ll need to register the CSS variable using @property first, like this:

@property --calc {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

Closing thoughts

Although I’m not keen on the syntax and how unreadable it can sometimes look (especially if it’s formatted on one line), I’m mega excited to see how if() evolves. I’d love to be able to use it with ordinary properties (e.g., color: if(style(background: white): black; style(background: black): white);) to avoid having to set CSS variables where possible.

It’d also be awesome if calc() calculations could be calculated on the fly without having to register the variable.

That being said, I’m still super happy with what if() does currently, and can’t wait to build even simpler design systems.


Poking at the CSS if() Function a Little More: Conditional Color Theming originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Daniel Schwarz
Wed, 25 Jun 2025 14:33:45 +0000


Chrome 137 shipped the if() CSS function, so it’s totally possible we’ll see other browsers implement it, though it’s tough to know exactly when. Whatever the case, if() enables us to use values conditionally, which we can already do with queries and other functions (e.g., media queries and the light-dark() function), so I’m sure you’re wondering: What exactly does if() do?

Sunkanmi gave us a nice overview of the function yesterday, poking at the syntax at a high level. I’d like to poke at it a little harder in this article, getting into some possible real-world usage.

To recap, if() conditionally assigns a value to a property based on the value of a CSS variable. For example, we could assign different values to the color and background properties based on the value of --theme:

  • --theme: "Shamrock"
    • color: ‌hsl(146 50% 3%)
    • background: hsl(146 50% 40%)
  • --theme: Anything else
    • color: hsl(43 74% 3%)
    • background: hsl(43 74% 64%)
:root {
  /* Change to fall back to the ‘else’ values */
  --theme: "Shamrock";

  body {
    color: if(style(--theme: "Shamrock"): hsl(146 50% 3%); else: hsl(43 74% 3%));
    background: if(style(--theme: "Shamrock"): hsl(146 50% 40%); else: hsl(43 74% 64%));
  }
}

I don’t love the syntax (too many colons, brackets, and so on), but we can format it like this (which I think is a bit clearer):

color: if(
  style(--theme: "Shamrock"): hsl(146 50% 3%);
  else: hsl(43 74% 3%)
);

We should be able to do a crazy number of things with if(), and I hope that becomes the case eventually, but I did some testing and learned that the syntax above is the only one that works. We can’t base the condition on the value of an ordinary CSS property (instead of a custom property), HTML attribute (using attr()), or any other value. For now, at least, the condition must be based on the value of a custom property (CSS variable).

Exploring what we can do with if()

Judging from that first example, it’s clear that we can use if() for theming (and design systems overall). While we could utilize the light-dark() function for this, what if the themes aren’t strictly light and dark, or what if we want to have more than two themes or light and dark modes for each theme? Well, that’s what if() can be used for.

First, let’s create more themes/more conditions:

:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Saffron"; /* ...I choose you! */

  body {
    color: if(
      style(--theme: "Shamrock"): hsl(146 50% 3%);
      style(--theme: "Saffron"): hsl(43 74% 3%);
      style(--theme: "Amethyst"): hsl(282 47% 3%)
    );
    background: if(
      style(--theme: "Shamrock"): hsl(146 50% 40%);
      style(--theme: "Saffron"): hsl(43 74% 64%);
      style(--theme: "Amethyst"): hsl(282 47% 56%)
    );
    transition: 300ms;
  }
}

Pretty simple really, but there are a few easy-to-miss things. Firstly, there’s no “else condition” this time, which means that if the theme isn’t Shamrock, Saffron, or Amethyst, the default browser styles are used. Otherwise, the if() function resolves to the value of the first true statement, which is the Saffron theme in this case. Secondly, transitions work right out of the box; in the demo below, I’ve added a user interface for toggling the --theme, and for the transition, literally just transition: 300ms alongside the if() functions:

Note: if theme-swapping is user-controlled, such as selecting an option, you don’t actually need if() at all. You can just use the logic that I’ve used at the beginning of the demo (:root:has(#shamrock:checked) { /* Styles */ }). Amit Sheen has an excellent demonstration over at Smashing Magazine.

To make the code more maintainable though, we can slide the colors into CSS variables as well, then use them in the if() functions, then slide the if() functions themselves into CSS variables:

/* Setup */
:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Shamrock"; /* ...I choose you! */

  /* Base colors */
  --shamrock: hsl(146 50% 40%);
  --saffron: hsl(43 74% 64%);
  --amethyst: hsl(282 47% 56%);

  /* Base colors, but at 3% lightness */
  --shamrock-complementary: hsl(from var(--shamrock) h s 3%);
  --saffron-complementary: hsl(from var(--saffron) h s 3%);
  --amethyst-complementary: hsl(from var(--amethyst) h s 3%);

  --background: if(
    style(--theme: "Shamrock"): var(--shamrock);
    style(--theme: "Saffron"): var(--saffron);
    style(--theme: "Amethyst"): var(--amethyst)
  );

  --color: if(
    style(--theme: "Shamrock"): var(--shamrock-complementary);
    style(--theme: "Saffron"): var(--saffron-complementary);
    style(--theme: "Amethyst"): var(--amethyst-complementary)
  );

  /* Usage */
  body {
    /* One variable, all ifs! */
    background: var(--background);
    color: var(--color);
    accent-color: var(--color);

    /* Can’t forget this! */
    transition: 300ms;
  }
}

As well as using CSS variables within the if() function, we can also nest other functions. In the example below, I’ve thrown light-dark() in there, which basically inverts the colors for dark mode:

--background: if(
  style(--theme: "Shamrock"): light-dark(var(--shamrock), var(--shamrock-complementary));
  style(--theme: "Saffron"): light-dark(var(--saffron), var(--saffron-complementary));
  style(--theme: "Amethyst"): light-dark(var(--amethyst), var(--amethyst-complementary))
);

if() vs. Container style queries

If you haven’t used container style queries before, they basically check if a container has a certain CSS variable (much like the if() function). Here’s the exact same example/demo but with container style queries instead of the if() function:

:root {
  /* Shamrock | Saffron | Amethyst */
  --theme: "Shamrock"; /* ...I choose you! */

  --shamrock: hsl(146 50% 40%);
  --saffron: hsl(43 74% 64%);
  --amethyst: hsl(282 47% 56%);

  --shamrock-complementary: hsl(from var(--shamrock) h s 3%);
  --saffron-complementary: hsl(from var(--saffron) h s 3%);
  --amethyst-complementary: hsl(from var(--amethyst) h s 3%);

  body {
    /* Container has chosen Shamrock! */
    @container style(--theme: "Shamrock") {
      --background: light-dark(var(--shamrock), var(--shamrock-complementary));
      --color: light-dark(var(--shamrock-complementary), var(--shamrock));
    }

    @container style(--theme: "Saffron") {
      --background: light-dark(var(--saffron), var(--saffron-complementary));
      --color: light-dark(var(--saffron-complementary), var(--saffron));
    }

    @container style(--theme: "Amethyst") {
      --background: light-dark(var(--amethyst), var(--amethyst-complementary));
      --color: light-dark(var(--amethyst-complementary), var(--amethyst));
    }

    background: var(--background);
    color: var(--color);
    accent-color: var(--color);
    transition: 300ms;
  }
}

As you can see, where if() facilitates conditional values, container style queries facilitate conditional properties and values. Other than that, it really is just a different syntax.

Additional things you can do with if() (but might not realize)

Check if a CSS variable exists:

/* Hide icons if variable isn’t set */
.icon {
  display: if(
    style(--icon-family): inline-block;
    else: none
  );
}

Create more-complex conditional statements:

h1 {
  font-size: if(
    style(--largerHeadings: true): xxx-large;
    style(--theme: "themeWithLargerHeadings"): xxx-large
  );
}

Check if two CSS variables match:

/* If #s2 has the same background as #s1, add a border */
#s2 {
  border-top: if(
    style(--s2-background: var(--s1-background)): thin solid red
  );
}

if() and calc(): When the math isn’t mathing

This won’t work (maybe someone can help me pinpoint why):

div {
  /* 3/3 = 1 */
  --calc: calc(3/3);
  /* Blue, because if() won’t calculate --calc */
  background: if(style(--calc: 1): red; else: blue);
}

To make if() calculate --calc, we’ll need to register the CSS variable using @property first, like this:

@property --calc {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

Closing thoughts

Although I’m not keen on the syntax and how unreadable it can sometimes look (especially if it’s formatted on one line), I’m mega excited to see how if() evolves. I’d love to be able to use it with ordinary properties (e.g., color: if(style(background: white): black; style(background: black): white);) to avoid having to set CSS variables where possible.

It’d also be awesome if calc() calculations could be calculated on the fly without having to register the variable.

That being said, I’m still super happy with what if() does currently, and can’t wait to build even simpler design systems.


Poking at the CSS if() Function a Little More: Conditional Color Theming originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Sunkanmi Fafowora
Tue, 24 Jun 2025 15:17:10 +0000


We’ve known it for a few weeks now, but the CSS if() function officially shipped in Chrome 137 version. It’s really fast development for a feature that the CSSWG resolved to add less than a year ago. We can typically expect this sort of thing — especially one that is unlike anything we currently have in CSS — to develop over a number of years before we can get our dirty hands on it. But here we are!

I’m not here to debate whether if() in CSS should exist, nor do I want to answer whether CSS is a programming language; Chris already did that and definitely explained how exhausting that fun little argument can be.

What I am here to do is poke at if() in these early days of support and explore what we know about it today at a pretty high level to get a feel for its syntax. We’ll poke a little harder at it in another upcoming post where we’ll look at a more heady real-world example.

Yes, it’s already here!

Conditional statements exist everywhere in CSS. From at-rules to the parsing and matching of every statement to the DOM, CSS has always had conditionals. And, as Lea Verou put it, every selector is essentially a conditional! What we haven’t had, however, is a way to style an element against multiple conditions in one line, and then have it return a result conditionally.

The if() function is a more advanced level of conditionals, where you can manipulate and have all your conditional statements assigned to a single property.

.element {
  color: if(style(--theme: dark): oklch(52% 0.18 140); else: oklch(65% 0.05 220));
}

How does if() work?

Well before Chrome implemented the feature, back in 2021 when it was first proposed, the early syntax was like this:

<if()> = if( <container-query>, [<declaration-value>]{1, 2} )

Now we’re looking at this instead:

<if()> = if(
  [<if-statement>: <result>]*;
  <if-statement>: <result> ;?
)

Where…

  • The first <if-statement> represents conditions inside either style()media(), or supports() wrapper functions. This allows us to write multiple if statements, as many as we may desire. Yes, you read that right. As many as we want!
  • The final <if-statement> condition (else) is the default value when all other if statements fail.

That’s the “easy” way to read the syntax. This is what’s in the spec:

<if()> = if( [ <if-branch> ; ]* <if-branch> ;? )
<if-branch> = <if-condition> : <declaration-value>?
<if-condition> = <boolean-expr[ <if-test> ]> | else
<if-test> =
  supports( [ <ident> : <declaration-value> ] | <supports-condition> )
  media( <media-feature> | <media-condition> ) |
  style( <style-query> )

A little wordy, right? So, let’s look at an example to wrap our heads around it. Say we want to change an element’s padding depending on a given active color scheme. We would set an if() statement with a style() function inside, and that would compare a given value with something like a custom variable to output a result. All this talk sounds so complicated, so let’s jump into code:

.element {
  padding: if(style(--theme: dark): 2rem; else: 3rem);
}

The example above sets the padding to 2rem… if the --theme variable is set to dark. If not, it defaults to 3rem. I know, not exactly the sort of thing you might actually use the function for, but it’s merely to illustrate the basic idea.

Make the syntax clean!

One thing I noticed, though, is that things can get convoluted very very fast. Imagine you have three if() statements like this:

:root {
  --height: 12.5rem;
  --width: 4rem;
  --weight: 2rem;
}

.element {
  height: if(
    style(--height: 3rem): 14.5rem; style(--width: 7rem): 10rem; style(--weight: 100rem): 2rem; else: var(--height)
  );
}

We’re only working with three statements and, I’ll be honest, it makes my eyes hurt with complexity. So, I’m anticipating if() style patterns to be developed soon or prettier versions to adopt a formatting style for this.

For example, if I were to break things out to be more readable, I would likely do something like this:

:root {
  --height: 12.5rem;
  --width: 4rem;
  --weight: 2rem;
}

/* This is much cleaner, don't you think? */
.element {
  height: if(
    style(--height: 3rem): 14.5rem; 
    style(--width: 7rem): 10rem; 
    style(--weight: 100rem): 2rem; 
    else: var(--height)
  );
}

Much better, right? Now, you can definitely understand what is going on at a glance. That’s just me, though. Maybe you have different ideas… and if you do, I’d love to see them in the comments.

Here’s a quick demo showing multiple conditionals in CSS for this animated ball to work. The width of the ball changes based on some custom variable values set. Gentle reminder that this is only supported in Chrome 137+ at the time I’m writing this:

The supports() and media() statements

Think of supports() the same way you would use the @supports at-rule. In fact, they work about the same, at least conceptually:

/* formal syntax for @supports */
@supports <supports-condition> {
  <rule-list>
}

/* formal syntax for supports() */
supports( [ <ident> : <declaration-value> ] | <supports-condition> )

The only difference here is that supports() returns a value instead of matching a block of code. But, how does this work in real code?

The <ident>: <declaration-value> you see here is, in this case, the property name: property value e.g. display: flex.

Let’s say you want to check for support for the backdrop-filter property, particularly the blur() function. Typically, you can do this with @supports:

/* Fallback in case the browser doesn't support backdrop-filter */
.card {
  backdrop-filter: unset;
  background-color: oklch(20% 50% 40% / 0.8);
}

@supports (backdrop-filter: blur(10px)) {
  .card {
    backdrop-filter: blur(10px);
    background-color: oklch(20% 50% 40% / 0.8);
  }
}

But, with CSS if(), we can also do this:

.card {
  backdrop-filter: if(
    supports(backdrop-filter: blur(10px)): blur(10px);
    else: unset
  );
}

Note: Think of unset here as a possible fallback for graceful degradation.

That looks awesome, right? Multiple conditions can be checked as well for supports() and any of the supported functions. For example:

.card {
  backdrop-filter: if(
    supports(backdrop-filter: blur(10px)): blur(10px);
    supports(backdrop-filter: invert(50%)): invert(50%);
    supports(backdrop-filter: hue-rotate(230deg)): hue-rotate(230deg);;
    else: unset
  );
}

Now, take a look at the @media at-rule. You can compare and check for a bunch of stuff, but I’d like to keep it simple and check for whether or not a screen size is a certain width and apply styles based on that:

h1 {
  font-size: 2rem;
}

@media (min-width: 768px) {
  h1 {
    font-size: 2.5rem;
  }
}

@media (min-width: 1200px) {
  h1 {
    font-size: 3rem;
  }
}

The media() wrapper works almost the same way as its at-rule counterpart. Note its syntax from the spec:

/* formal syntax for @media */
@media <media-query-list> {
  <rule-list>
}

/* formal syntax for media() */
media( <media-feature> | <media-condition> )

Notice how at the end of the day, the formal syntax (<media-query>) is the same as the syntax for the media() function. And instead of returning a block of code in @media, you’d have something like this in the CSS inline if():

h1 {
  font-size: if(
    media(width >= 1200px): 3rem;
    media(width >= 768px): 2.5rem;
    else: 2rem
  );
}

Again, these are early days

As of the time of this writing, only the latest update of Chrome supports if()). I’m guessing other browsers will follow suit once usage and interest come in. I have no idea when that will happen. Until then, I think it’s fun to experiment with this stuff, just as others have been doing:

Experimenting with early features is how we help CSS evolve. If you’re trying things out, consider adding your feedback to the CSSWG and Chromium. The more use cases, the better, and that will certain help make future implementations better as well.

Now that we have a high-level feel for the if()syntax, we’ll poke a little harder at the function in another article where we put it up against a real-world use case. We’ll link that up when it publishes tomorrow.


Lightly Poking at the CSS if() Function in Chrome 137 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 23 Jun 2025 15:47:48 +0000


I like the term “content aware components” like Eric Bailey uses in the Piccalilli article Making content-aware components using CSS :has(), grid, and quantity queries. Does a card have a photo? Yes, do one thing, no, do another. That sort of thing. Eric has some good examples where a UI component has a bunch more “tags” than another, so the layout adjusts to accommodate them better.

Menu items with descriptions and pricing, featuring a Chicken Fajita Omelette and Poached Eggs, along with badge labels indicating their popularity and other attributes.

Thanks to :has(), the idea of “quantity queries” (e.g. style an element if there are, say, 4 or more of an element) have gotten a lot easier. The way I figure it, we could do like:

.card:has(.tag:nth-child(4)) {
  /* apply styles to the card because there are at least 4 tags */
}

Admittedly, the logic gets a little bit more complicated if you want like “3 or less tags”, but that’s exactly what Eric covers in the article, linking up the original ideas and resources, so you’re in luck.

At CSS Day the other week, I listened to Ahmad Shadeed have a wonderful idea on stage. Imagine a layout with one item, you show it. Two items? Side by side? Three items? Maybe two on top and one on the bottom. Four? Two by Two. Five? Well! That’s getting to be a lot of items. We could break them into a carousel. All entirely in CSS. That’s wild. I’d love to build that one day. Maybe I’ll try to stream it one of these days. (Of course, as I write this Kevin Powell put out a video that verges on the idea, and it’s very clever.)

Speaking of Ahmad, he’s got a great article introducing the big improvements that the attr() function has gotten. I absolutely love how we can pluck attribute values out of HTML and actually have them be useful now. I think we’ll realize the power of that more and more. But it occurs to me here that it could factor into this quantity query situation. Say you trust your server to know and output stuff like this. So like:

<div class="card-grid" data-cards="13">
  ...
</div>

You can get your hands on 13, as an actual number not a string, in CSS now like:

attr(data-cards type(<number>), 2)

The number 2 above is a fallback. With a number like that, it makes me think that maybe-just-maybe, we could combine it with the newfangled if() commands in CSS (See Una’s great video) to write the same kind of “quantity query” logic.

Ya know how Sass has @mixin to repeat blocks of CSS? Native CSS doesn’t have that yet, but style queries are pretty close. I snagged this screenshot out of Kevin’s video (in CodePen, naturally):

See how he just flips on a --custom-property then the style query matches when that custom property is set and outputs a block of styles? That feels an awful lot like a mixin to me. Miriam has a nice homebase page for native mixins, which links to some very active discussion on it. At the pace CSS is moving I imagine we’ll have it before we know it. Just @applying a @mixin seems like a more straightforward approach than the style query approach, and more flexible as it’s likely they’ll take parameters, and possibly even slots.

CSS carousels amount to a pretty hefty amount of CSS. Wouldn’t it be cool to make that into a hefty @mixin that takes parameters on what features you want? Ship it.


In other news, gap decorations are starting to be a thing and that’s wonderful. Hoping they’ll move right onto styling a grid area without needing an HTML element there, that would be just as wonderful. I’m still hesitant on making entire columns of article content into grids, but that’s a me-problem, I see others are coming around on the idea.

by: Zell Liew
Mon, 23 Jun 2025 13:41:34 +0000


In a previous article, I showed you how to refactor the Resize Observer API into something way simpler to use:

// From this
const observer = new ResizeObserver(observerFn)

function observerFn (entries) {
  for (let entry of entries) {
    // Do something with each entry
  }
}

const element = document.querySelector('#some-element')
observer.observe(element);
// To this 
const node = document.querySelector('#some-element')
const obs = resizeObserver(node, {
  callback({ entry }) {
    // Do something with each entry
  }
})

Today, we’re going to do the same for MutationObserver and IntersectionObserver.

Refactoring Mutation Observer

MutationObserver has almost the same API as that of ResizeObserver. So we can practically copy-paste the entire chunk of code we wrote for resizeObserver to mutationObserver.

export function mutationObserver(node, options = {}) {
  const observer = new MutationObserver(observerFn)
  const { callback, ...opts } = options
  observer.observe(node, opts)

  function observerFn(entries) {
    for (const entry of entries) {
      // Callback pattern
      if (options.callback) options.callback({ entry, entries, observer })
      // Event listener pattern
      else {
        node.dispatchEvent(
          new CustomEvent('mutate', {
            detail: { entry, entries, observer },
          })
        )
      }
    }
  }
}

You can now use mutationObserver with the callback pattern or event listener pattern.

const node = document.querySelector('.some-element')

// Callback pattern 
const obs = mutationObserver(node, {
  callback ({ entry, entries }) {
    // Do what you want with each entry
  }
})

// Event listener pattern
node.addEventListener('mutate', event => {
  const { entry } = event.detail
  // Do what you want with each entry
})

Much easier!

Disconnecting the observer

Unlike ResizeObserver who has two methods to stop observing elements, MutationObserver only has one, the disconnect method.

export function mutationObserver(node, options = {}) {
  // ... 
  return {
    disconnect() {
      observer.disconnect()
    }
  }
}

But, MutationObserver has a takeRecords method that lets you get unprocessed records before you disconnect. Since we should takeRecords before we disconnect, let’s use it inside disconnect.

To create a complete API, we can return this method as well.

export function mutationObserver(node, options = {}) {
  // ... 
  return {
    // ...
    disconnect() {
      const records = observer.takeRecords()
      observer.disconnect()
      if (records.length > 0) observerFn(records)
    }
  }
}

Now we can disconnect our mutation observer easily with disconnect.

const node = document.querySelector('.some-element')
const obs = mutationObserver(/* ... */)

obs.disconnect()

MutationObserver’s observe options

In case you were wondering, MutationObserver’s observe method can take in 7 options. Each one of them determines what to observe, and they all default to false.

  • subtree: Monitors the entire subtree of nodes
  • childList: Monitors for addition or removal children elements. If subtree is true, this monitors all descendant elements.
  • attributes: Monitors for a change of attributes
  • attributeFilter: Array of specific attributes to monitor
  • attributeOldValue: Whether to record the previous attribute value if it was changed
  • characterData: Monitors for change in character data
  • characterDataOldValue: Whether to record the previous character data value

Refactoring Intersection Observer

The API for IntersectionObserver is similar to other observers. Again, you have to:

  1. Create a new observer: with the new keyword. This observer takes in an observer function to execute.
  2. Do something with the observed changes: This is done via the observer function that is passed into the observer.
  3. Observe a specific element: By using the observe method.
  4. (Optionally) unobserve the element: By using the unobserve or disconnect method (depending on which Observer you’re using).

But IntersectionObserver requires you to pass the options in Step 1 (instead of Step 3). So here’s the code to use the IntersectionObserver API.

// Step 1: Create a new observer and pass in relevant options
const options = {/*...*/}
const observer = new IntersectionObserver(observerFn, options)

// Step 2: Do something with the observed changes
function observerFn (entries) {
  for (const entry of entries) {
    // Do something with entry
  }
}

// Step 3: Observe the element
const element = document.querySelector('#some-element')
observer.observe(element)

// Step 4 (optional): Disconnect the observer when we're done using it
observer.disconnect(element)

Since the code is similar, we can also copy-paste the code we wrote for mutationObserver into intersectionObserver. When doing so, we have to remember to pass the options into IntersectionObserver and not the observe method.

export function mutationObserver(node, options = {}) {
  const { callback, ...opts } = options
  const observer = new MutationObserver(observerFn, opts)
  observer.observe(node)

  function observerFn(entries) {
    for (const entry of entries) {
      // Callback pattern
      if (options.callback) options.callback({ entry, entries, observer })
      // Event listener pattern
      else {
        node.dispatchEvent(
          new CustomEvent('intersect', {
            detail: { entry, entries, observer },
          })
        )
      }
    }
  }
}

Now we can use intersectionObserver with the same easy-to-use API:

const node = document.querySelector('.some-element')

// Callback pattern 
const obs = intersectionObserver(node, {
  callback ({ entry, entries }) {
    // Do what you want with each entry
  }
})

// Event listener pattern
node.addEventListener('intersect', event => {
  const { entry } = event.detail
  // Do what you want with each entry
})

Disconnecting the Intersection Observer

IntersectionObserver‘s methods are a union of both resizeObserver and mutationObserver. It has four methods:

  • observe: observe an element
  • unobserve: stops observing one element
  • disconnect: stops observing all elements
  • takeRecords: gets unprocessed records

So, we can combine the methods we’ve written in resizeObserver and mutationObserver for this one:

export function intersectionObserver(node, options = {}) {
  // ...
  return {
    unobserve(node) {
      observer.unobserve(node)
    },

    disconnect() {
      // Take records before disconnecting.
      const records = observer.takeRecords()
      observer.disconnect()
      if (records.length > 0) observerFn(records)
    },
    
    takeRecords() {
      return observer.takeRecords()
    },
  }
}

Now we can stop observing with the unobserve or disconnect method.

const node = document.querySelector('.some-element')
const obs = intersectionObserver(node, /*...*/)

// Disconnect the observer
obs.disconnect()

IntersectionObserver options

In case you were wondering, IntersectionObserver takes in three options:

  • root: The element used to check if observed elements are visible
  • rootMargin: Lets you specify an offset amount from the edges of the root
  • threshold: Determines when to log an observer entry

Here’s an article to help you understand IntersectionObserver options.

Using this in practice via Splendid Labz

Splendid Labz has a utils library that contains resizeObserver, mutationObserver and IntersectionObserver.

You can use them if you don’t want to copy-paste the above snippets into every project.

import { 
  resizeObserver, 
  intersectionObserver, 
  mutationObserver 
} from 'splendidlabz/utils/dom'

const mode = document.querySelector(‘some-element’)

const resizeObs = resizeObserver(node, /* ... */)
const intersectObs = intersectionObserver(node, /* ... */)
const mutateObs = mutationObserver(node, /* ... */)

Aside from the code we’ve written together above (and in the previous article), each observer method in Splendid Labz is capable of letting you observe and stop observing multiple elements at once (except mutationObserver because it doesn’t have a unobserve method)

const items = document.querySelectorAll('.elements')
const obs = resizeObserver(items, {
  callback ({ entry, entries }) {
    /* Do what you want here */
  }
})

// Unobserves two items at once
const subset = [items[0], items[1]]
obs.unobserve(subset)

So it might be just a tad easier to use the functions I’ve already created for you. 😉

Shameless Plug: Splendid Labz contains a ton of useful utilities — for CSS, JavaScript, Astro, and Svelte — that I have created over the last few years.

I’ve parked them all in into Splendid Labz, so I no longer need to scour the internet for useful functions for most of my web projects. If you take a look, you might just enjoy what I’ve complied!

(I’m still making the docs at the time of writing so it can seem relatively empty. Check back every now and then!)

Learning to refactor stuff

If you love the way I explained how to refactor the observer APIs, you may find how I teach JavaScript interesting.

In my JavaScript course, you’ll learn to build 20 real life components. We’ll start off simple, add features, and refactor along the way.

Refactoring is such an important skill to learn — and in here, I make sure you got cement it into your brain.

That’s it! Hope you had fun reading this piece!


A Better API for the Intersection and Mutation Observers originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Juan Diego Rodríguez
Fri, 20 Jun 2025 14:04:12 +0000


I have had the opportunity to edit over a lot of the new color entries coming to the CSS-Tricks Almanac. We’ve already published several with more on the way, including a complete guide on color functions:

And I must admit: I didn’t know a lot about color in CSS (I still used rgb(), which apparently isn’t what cool people do anymore), so it has been a fun learning experience. One of the things I noticed while trying to keep up with all this new information was how long the glossary of color goes, especially the “color” concepts. There are “color spaces,” “color models,” “color gamuts,” and basically a “color” something for everything.

They are all somewhat related, and it can get confusing as you dig into using color in CSS, especially the new color functions that have been shipped lately, like contrast-color() and color-mix(). Hence, I wanted to make the glossary I wish I had when I was hearing for the first time about each concept, and that anyone can check whenever they forget what a specific “color” thing is.

As a disclaimer, I am not trying to explain color, or specifically, color reproduction, in this post; that would probably be impossible for a mortal like me. Instead, I want to give you a big enough picture for some technicalities behind color in CSS, such that you feel confident using functions like lab() or oklch() while also understanding what makes them special.

What’s a color?

Let’s slow down first. In order to understand everything in color, we first need to understand the color in everything.

While it’s useful to think about an object being a certain color (watch out for the red car, or cut the white cable!), color isn’t a physical property of objects, or even a tangible thing. Yes, we can characterize light as the main cause of color1, but it isn’t until visible light enters our eyes and is interpreted by our brains that we perceive a color. As said by Elle Stone:

Light waves are out there in the world, but color happens in the interaction between light waves and the eye, brain, and mind.

Even if color isn’t a physical thing, we still want to replicate it as reliably as possible, especially in the digital era. If we take a photo of a beautiful bouquet of lilies (like the one on my desk) and then display it on a screen, we expect to see the same colors in both the image and reality. However, “reality” here is a misleading term since, once again, the reality of color depends on the viewer. To solve this, we need to understand how light wavelengths (something measurable and replicable) create different color responses in viewers (something not so measurable).

Luckily, this task was already carried out 95 years ago by the International Commission on Illumination (CIE, by its French name). I wish I could get into the details of the experiment, but we haven’t gotten into our first color thingie yet. What’s important is that from these measurements, the CIE was able to map all the colors visible to the average human (in the experiment) to light wavelengths and describe them with only three values.

Initially, those three primary values corresponded to the red, green, and blue wavelengths used in the experiment, and they made up the CIERGB Color Space, but researchers noticed that some colors required a negative wavelength2 to represent a visible color. To avoid that, a series of transformations were performed on the original CIERGB and the resulting color space was called CIEXYZ.

This new color space also has three values, X and Z represent the chromaticity of a color, while Y represents its luminance. Since it has three axes, it makes a 3D shape, but if we slice it such that its luminance is the same, we get all the visible colors for a given luminance in a figure you have probably seen before.

xy chromaticitydiagram showing all visible colors

This is called the xy chromaticity diagram and holds all the colors visible by the average human eye (based on the average viewer in the CIE 1931 experiment). Colors inside the shape are considered real, while those outside are deemed imaginary.

Color Spaces

The purpose of the last explanation was to reach the CIEXYZ Color Space concept, but what exactly is a “color space”? And why is the CIEXYZ Color Space so important?

The CIEXYZ Color Space is a mapping from all the colors visible by the average human eye into a 3D coordinate system, so we only need three values to define a color. Then, a color space can be thought of as a general mapping of color, with no need to include every visible color, and it is usually defined through three values as well.

RGB Color Spaces

The most well-known color spaces are the RGB color spaces (note the plural). As you may guess from the name, here we only need the amount of red, green, and blue to describe a color. And to describe an RGB color space, we only need to define its “reddest”, “greenest”, and “bluest” values3. If we use coordinates going from 0 to 1 to define a color in the RGB color space, then:

  • (1, 0, 0) means the reddest color.
  • (0, 1, 0) means the greenest color.
  • (0, 0, 1) means the bluest color.

However, “reddest”, “bluest”, and “greenest” are only arbitrary descriptions of color. What makes a color the “bluest” is up to each person. For example, which of the following colors do you think is the bluest?

Different shades of blue

As you can guess, something like “bluest” is an appalling description. Luckily, we just have to look back at the CIEXYZ color space — it’s pretty useful! Here, we can define what we consider the reddest, greenest, and bluest colors just as coordinates inside the xy chromaticity diagram. That’s all it takes to create an RGB color space, and why there are so many!

sRGV and WideGamutRGB in the xy chromaticity diagram
Credit: Elle Stone

In CSS, the most used color space is the standard RGB (sRGB) color space, which, as you can see in the last image, leaves a lot of colors out. However, in CSS, we can use modern RGB color spaces with a lot more colors through the color() function, such as display-p3prophoto-rgb, and rec2020.

Comparing the shapes of various color spaces, including sRGB, Display P3, A98-RGB, Rec2020, and ProPhoto.
Credit: Chrome Developer Team

Notice how the ProPhoto RGB color space goes out of the visible color. This is okay. Colors outside are clamped; they aren’t new or invisible colors.

In CSS, besides sRGB, we have two more color spaces: the CIELAB color space and the Oklab color space. Luckily, once we understood what the CIEXYZ color space is, then these two should be simpler to understand. Let’s dig into that next.

CIELAB and Oklab Color Spaces

As we saw before, the sRGB color space lacks many of the colors visible by the average human eye. And as modern screens got better at displaying more colors, CSS needed to adopt newer color spaces to fully take advantage of those newer displays. That wasn’t the only problem with sRGB — it also lacks perceptual uniformity, meaning that changes in the color’s chromaticity also change its perceived lightness. Check, for example, this demo by Adam Argyle:

Created in 1976 by the CIE, CIELAB, derived from CIEXYZ, also encompasses all the colors visible by the human eye. It works with three coordinates: L for perceptual lightness, a for the amount of red-green, and b* for the amount of yellow-blue in the color.

A diagram in the shape of a sphere containing a range of colors that go from white to black vertically and green to blue to yellow to red horizontally.
Credit: Linshang Technology

It has a way better perceptual uniformity than sRGB, but it still isn’t completely uniform, especially in gradients involving blue. For example, in the following white-to-blue gradient, CIELAB shifts towards purple.

Comparing blue gradients in Oklahoma and CIELAB. The CIELAB gradients contains shades of purple.
Image Credits to Björn Ottosson

As a final improvement, Björn Ottosson came up with the Oklab color space, which also holds all colors visible by the human eye while keeping a better perceptual uniformity. Oklab also uses the three L*a*b* coordinates. Thanks to all these improvements, it is the color space I try to use the most lately.

Color Models

When I was learning about these concepts, my biggest challenge after understanding color spaces was not getting them confused with color models and color gamuts. These two concepts, while complementary and closely related to color spaces, aren’t the same, so they are a common pitfall when learning about color.

A color model refers to the mathematical description of color through tuples of numbers, usually involving three numbers, but these values don’t give us an exact color until we pair them with a color space. For example, you know that in the RGB color model, we define color through three values: red, green, and blue. However, it isn’t until we match it to an RGB color space (e.g., sRGB with display-p3) that we have a color. In this sense, a color space can have several color models, like sRGB, which uses RGB, HSL, and HWB. At the same time, a color model can be used in several color spaces.

I found plenty of articles and tutorials where “color spaces” and “color models” were used interchangeably. And some places were they had a different definition of color spaces and models than the one provided here. For example, Chrome’s High definition CSS color guide defines CSS’s RGB and HSL as different color spaces, while MDN’s Color Space entry does define RGB and HSL as part of the sRGB color space.

Personally, in CSS, I find it easier to understand the idea of RGB, HSL and HWB as different models to access the sRGB color space.

Color Gamuts

A color gamut is more straightforward to explain. You may have noticed how we have talked about a color space having more colors than another, but it would be more correct to say it has a “wider” gamut, since a color gamut is the range of colors available in a color space. However, a color gamut isn’t only restricted by color space boundaries, but also by physical limitations. For example, an older screen may decrease the color gamut since it isn’t able to display each color available in a given color space. In this case where a color can’t be represented (due to physical limitation or being outside the color space itself), it’s said to be “out of gamut”.

Color Functions

In CSS, the only color space available used to be sRGB. Nowadays, we can work with a lot of modern color spaces through their respective color functions. As a quick reference, each of the color spaces in CSS uses the following functions:

  • sRGB: We can work in sRGB using the ol’ hexadecimal notation, named colors, and the rgb()rgba()hsl()hsla() and hwb() functions.
  • CIELAB: Here we have the lab() for Cartesian coordinates and lch() for polar coordinates.
  • Oklab: Similar to CIELAB, we have oklab() for Cartesian coordinates and oklch() for polar coordinates.
  • More through the color() and color-mix(). Outside these three color spaces, we can use many more using the color() and color-mix() functions. Specifically, we can use the RGB color spaces: rgb-lineardisplay-p3a98-rgbprophoto-rgbrec2020 and the XYZ color space: xyzxyz-d50, or xyz-d65.

TL;DR

  1. Color spaces are a mapping between available colors and a coordinate system. In CSS, we have three main color spaces: sRGB, CIELAB, and Oklab, but many more are accessible through the color() function.
  2. Color models define color with tuples of numbers, but they don’t give us information about the actual color until we pair them with a color space. For example, the RGB model doesn’t mean anything until we assign it an RGB color space.
  3. Most of the time, we want to talk about how many colors a color space holds, so we use the term color gamut for the task. However, a color gamut is also tied to the physical limitations of a camera/display. A color may be out-of-gamut, meaning it can’t be represented in a given color space.
  4. In CSS, we can access all these color spaces through color functions, of which there are many.
  5. The CIEXYZ color space is extremely useful to define other color spaces, describe their gamuts, and convert between them.

References

Footnotes

1 Light is the main cause of color, but color can be created by things other than light. For example, rubbing your closed eyes mechanically stimulates your retina, creating color in what’s called phosphene. ⤴️

2 If negative light also makes you scratch your head, and for more info on how the CIEXYZ color space was created, I highly recommend Douglas A. Kerr The CIE XYZ and xyY Color Spaces paper. ⤴️

3 We also need to define the darkest dark color (“black”) and the lightest light color (“white”). However, for well-behaved color spaces, these two can be abstracted from the reddest, blues, and greenest colors. ⤴️


Color Everything in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Sunkanmi Fafowora
Thu, 19 Jun 2025 15:01:18 +0000


If you asked me a few months ago, “What does it take for a website to stand out?” I may have said fancy animations, creative layouts, cool interactions, and maybe just the general aesthetics, without pointing out something in particular. If you ask me now, after working on color for the better part of the year, I can confidently say it’s all color. Among all the aspects that make a design, a good color system will make it as beautiful as possible.

However, color in CSS can be a bit hard to fully understand since there are many ways to set the same color, and sometimes they even look the same, but underneath are completely different technologies. That’s why, in this guide, we will walk through all the ways you can set up colors in CSS and all the color-related properties out there!


Colors are in everything

They are in your phone, in what your eye sees, and on any screen you look at; they essentially capture everything. Design-wise, I see the amazing use of colors on sites listed over at awwwards.com, and I’m always in awe.

Not all color is the same. In fact, similar colors can live in different worlds, known as color spaces. Take for example, sRGB, the color space used on the web for the better part of its existence and hence the most known. While it’s the most used, there are many colors that are simply missing in sRGB that new color spaces like CIELAB and Oklab bring, and they cover a wider range of colors sRGB could only dream of, but don’t let me get ahead of myself.


What’s a color space?

A color space is the way we arrange and represent colors that exist within a device, like printers and monitors. We have different types of color spaces that exist in media (Rec2020, Adobe RGB, etc), but not all of them are covered in CSS. Luckily, the ones we have are sufficient to produce all the awesome and beautiful colors we need. In this guide, we will be diving into the three main color spaces available in CSS: sRGB, CIELAB, and OkLab.


The sRGB Color Space

The sRGB is one of the first color spaces we learn. Inside, there are three color functions, which are essentially notations to define a color: rgb()hsl(), and hwb().

sRGB has been a standard color space for the web since 1996. However, it’s closer to how old computers represented color, rather than how humans understand it, so it had some problems like not being able to capture the full gamut of modern screens. Still, many modern applications and websites use sRGB, so even though it is the “old way” of doing things, it is still widely accepted and used today.

The rgb() function

Diagram of the rgb function showing arguments for red, green, blue, and alpha.

rgb() uses three values, rg, and b which specifies the redness, greenness, and blueness of the color you want.

All three values are non-negative, and they go from 0 to 255.

.element {
  color: rgb(245 123 151);
}

It also has an optional value (the alpha value) preceded by a forward slash. It determines the level of opacity for the color, which goes from 0 (or 0%) for a completely transparent color, to 1 (or 100%) for a fully opaque one.

.element {
  color: rgb(245 123 151 / 20%);
}

There are two ways you can write inside rgb(). Either using the legacy syntax that separates the three values with commas or the modern syntax that separates each with spaces.

You want to combine the two syntax formats, yes? That’s a no-no. It won’t even work.

/* This would not work */
.element {
  color: rgb(225, 245, 200 / 0.5);
}

/* Neither will this */
.element {
  color: rgb(225 245 200, 0.5);
}

/* Or this */
.element {
  color: rgb(225, 245 200 / 0.5);
}

But, following one consistent format will do the trick, so do that instead. Either you’re so used to the old syntax and it’s hard for you to move on, continue to use the legacy syntax, or you’re one who’s willing to try and stick to something new, use the modern syntax.

/* Valid (Modern syntax)  */
.element {
  color: rgb(245 245 255 / 0.5);
}

/* Valid (Legacy syntax)  */
.element {
  color: rgb(245, 245, 255, 0.5);
}

The rgba() function

rgba() is essentially the same as rgb() with an extra alpha value used for transparency.

In terms of syntax, the rgba() function can be written in two ways:

  • Comma-separated and without percentages
  • Space-separated, with the alpha value written after a forward slash (/)
.element {
  color: rgba(100, 50, 0, 0.5);
}

.element {
  color: rgba(100 50 0 / 0.5);
}

So, what’s the difference between rgba() and rgb()?

Breaking news! There is no difference. Initially, only rgba() could set the alpha value for opacity, but in recent years, rgb() now supports transparency using the forward slash (/) before the alpha value.

rgb() also supports legacy syntax (commas) and modern syntax (spaces), so there’s practically no reason to use rgba() anymore; it’s even noted as a CSS mistake by folks at W3C.

In a nutshell, rgb() and rgba() are the same, so just use rgb().

/* This works */
.element-1 {
    color: rgba(250 30 45 / 0.8);
}

/* And this works too, so why not just use this? */
.element-2 {
    color: rgb(250 30 45 / 0.8);
}

The hexadecimal notation

Diagram of the hex color notation showing how #3dFa4C corresponds to the #RRGGBB color channels.

The hexadecimal CSS color code is a 3, 4, 6, or 8 (being the maximum) digit code for colors in sRGB. It’s basically a shorter way of writing rgb(). The hexadecimal color (or hex color) begins with a hash token (#) and then a hexadecimal number, which means it goes from 0 to 9 and then skips to letters a to f (a being 10, b being 11, and so on, up to f for 15).

In the hexadecimal color system, the 6-digit style is done in pairs. Each pair represents red (RR), blue (BB), and green (GG).

Each value in the pair can go from 00 to FF, which it’s equivalent to 255 in rgb().

Notice how I used caps for the letters (F) and not lowercase letters like I did previously? Well, that’s because hexadecimals are not case-sensitive in CSS, so you don’t have to worry about uppercase or lowercase letters when dealing with hexadecimal colors.

  • 3-digit hexadecimal. The 3-digit hexadecimal system is a shorter way of writing the 6-digit hexadecimal system, where each value represents the color’s redness, greenness, and blueness, respectively
.element {
  color: #abc;
}

In reality, each value in the 3-digit system is duplicated and then translated to a visible color

.element {
  color: #abc; /* Equals #AABBCC  */
}

BUT, this severely limits the colors you can set. What if I want to target the color 213 in the red space, or how would I get a blue of value 103? It’s impossible. That’s why you can only get a total number of 4,096 colors here as opposed to the 17 million in the 6-digit notation. Still, if you want a fast way of getting a certain color in hexadecimal without having to worry about the millions of other colors, use the 3-digit notation.

  • 4-digit hexadecimal. This is similar to the 3-digit hexadecimal notation except it includes the optional alpha value for opacity. It’s a shorter way of writing the 8-digit hexadecimal which also means that all values here are repeated once during color translation.
.element {
  color: #ABCD2;
}

For the alpha value, 0 represents 00 (a fully transparent color) and F represents FF (a fully opaque color).

.element {
  color: #abcd; /* Same as #AABBCCDD */
}
  • 6-digit hexadecimal. The 6-digit hexadecimal system just specifies a hexadecimal color’s redness, blueness, and greenness without its alpha value for color opacity.
.element {
  color: #abcdef;
}
  • 8-digit hexadecimal. This 8-digit hexadecimal system specifies hexadecimal color’s redness, blueness, greenness, and its alpha value for color opacity. Basically, it is complete for color control in sRGB.
.element {
  color: #faded101;
}

The hsl() function

Diagram of the hsl function showing arguments for hue, saturation, lightness, and alpha.

Both hsl() and rgb() live in the sRGB space, but they access colors differently. And while the consensus is that hsl() is far more intuitive than rgb(), it all boils down to your preference.

hsl() takes three values: hs, and l, which set its hue, saturation, and lightness, respectively.

  • The hue sets the base color and represents a direction in the color wheel, so it’s written in angles from 0deg to 360deg.
  • The saturation sets how much of the base color is present and goes from 0 (or 0%) to 100 (or 100%).
  • The lightness represents how close to white or black the color gets.

One cool thing: the hue angle goes from (0deg360deg), but we might as well use negative angles or angles above 360deg, and they will circle back to the right hue. Especially useful for infinite color animation. Pretty neat, right?

Plus, you can easily get a complementary color from the opposite angle (i.e., adding 180deg to the current hue) on the color wheel.

/* Current color */
.element {
  color: hsl(120deg 40 60 / 0.8);
}

/* Complementary color */
.element {
  color: hsl(300deg 40 60 / 0.8);
}

You want to combine the two syntax formats like in rgb(), yes? That’s also a no-no. It won’t work.

/* This would not work */
.element {
  color: hsl(130deg, 50, 20 / 0.5);
}

/* Neither will this */
.element {
  color: hsl(130deg 50 20, 0.5);
}

/* Or this */
.element {
  color: hsl(130deg 50, 20 / 0.5);
}

Instead, stick to one of the syntaxes, like in rgb():

/* Valid (Modern syntax)  */ 
.element {
  color: hsl(130deg 50 20 / 0.5);
}

/* Valid (Modern syntax)  */ 
.element {
  color: hsl(130deg, 50, 20, 0.5);
}

The hsla() function

hsla() is essentially the same with hsl(). It uses three values to represent its color’s hue (h), saturation (s), and lightness (l), and yes (again), an alpha value for transparency (a). We can write hsla() in two different ways:

  • Comma separated
  • Space separated, with the alpha value written after a forward slash (/)
.element {
  color: hsla(120deg, 100%, 50%, 0.5);
}

.element {
  color: hsla(120deg 100% 50% / 0.5);
}

So, what’s the difference between hsla() and hsl()?

Breaking news (again)! They’re the same. hsl() and hsla() both:

  • Support legacy and modern syntax
  • Have the power to increase or reduce color opacity

So, why does hsla() still exist? Well, apart from being one of the mistakes of CSS, many applications on the web still use hsla() since there wasn’t a way to set opacity with hsl() when it was first conceived.

My advice: just use hsl(). It’s the same as hsla() but less to write.

/* This works */
.element-1 {
    color: hsla(120deg 80 90 / 0.8);
}

/* And this works too, so why not just use this? */
.element-2 {
    color: hsl(120deg 80 90 / 0.8);
}

The hwb() function

Diagram of the hwb color function showing arguments for hue, whiteness, blackness, and alpha.

hwb() also uses hue for its first value, but instead takes two values for whiteness and blackness to determine how your colors will come out (and yes, it also does have an optional transparency value, a, just like rgb() and hsl()).

.element {
  color: hwb(80deg 20 50 / 0.5);
}
  • The first value h is the same as the hue angle in hsl(), which represents the color position in the color wheel from 0 (or 0deg) to 360 (or 360deg).
  • The second value, w, represents the whiteness in the color. It ranges from 0/0% (no white) to 100/100% (full white if b is 0).
  • The third value, b, represents the blackness in the color. It ranges from 0/0% (no black) to 100/100% (fully black if w is 0).
  • The final (optional) value is the alpha value, a, for the color’s opacity, preceded by a forward slash The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

Although this color function is barely used, it’s completely valid to use, so it’s up to personal preference.

Named colors

CSS named colors are hardcoded keywords representing predefined colors in sRGB. You are probably used to the basic: whiteblueblackred, but there are a lot more, totaling 147 in all, that are defined in the Color Modules Level 4 specification.

Named colors are often discouraged because their names do not always match what color you would expect.


The CIELAB Color Space

The CIELAB color space is a relatively new color space on the web that represents a wider color gamut, closer to what the human eye can see, so it holds a lot more color than the sRGB space.

The lab() function

Diagram of the lab color function showing arguments for lightness, greenness to redness, blueness to yellowness, and the alpha transparency.

For this color function, we have three axes in a space-separated list to determine how the color is set.

.element {
    color: lab(50 20 20 / 0.9);
}
  • The first value l represents the degree of whiteness to blackness of the color. Its range being 0/(or 0%) (black) to 100 (or 100%) (white).
  • The second value a represents the degree of greenness to redness of the color. Its range being from -125/0% (green) to125 (or 100%) (red).
  • The third value b represents the degree of blueness to yellowness of the color. Its range is also from -125 (or 0%) (blue) to 125 (or 100%) (red).
  • The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

This is useful when you’re trying to obtain new colors and provide support for screens that do support them. Actually, most screens and all major browsers now support lab(), so you should be good.

The CSS lab() color function’s a and b values are actually unbounded. Meaning they don’t technically have an upper or lower limit. But, at practice, those are their limits according to the spec.

The lch() function

Diagram of the lch color function chowing arguments for whiteness to blackness, chroma, hue, and the alpha transparency.

The CSS lch() color function is said to be better and more intuitive than lab().

.element {
    color: lch(10 30 300deg);
}

They both use the same color space, but instead of having l, a, and b, lch uses lightness, chroma, and hue.

  • The first value l represents the degree of whiteness to blackness of the color. Its range being 0 (or 0%) (black) to 100 (or 100%) (white).
  • The second value c represents the color’s chroma (which is like saturation). Its range being from 0 (or 100%) to 150 or (or 100%).
  • The third value h represents the color hue. The value’s range is also from 0 (or 0deg) to 360 (or 360deg).
  • The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

The CSS lch() color function’s chroma (c) value is actually unbounded. Meaning it doesn’t technically have an upper or lower limit. But, in practice, the chroma values above are the limits according to the spec.


The OkLab Color Space

Björn Ottosson created this color space as an “OK” and even better version of the lab color space. It was created to solve the limitations of CIELAB and CIELAB color space like image processing in lab(), such as making an image grayscale, and perceptual uniformity. The two color functions in CSS that correspond to this color space are oklab() and oklch().

Perceptual uniformity occurs when there’s a smooth change in the direction of a gradient color from one point to another. If you notice stark contrasts like the example below for rgb() when transitioning from one hue to another, that is referred to as a non-uniform perceptual colormap.

Notice how the change from one color to another is the same in oklab() without any stark contrasts as opposed to rgb()? Yeah, OKLab color space solves the stark contrasts present and gives you access to many more colors not present in sRGB.

OKlab actually provides a better saturation of colors while still maintaining the hue and lightness present in colors in CIELAB (and even a smoother tranisition between colors!).

The oklab() function

Diagram of the oklahoma color function syntax showing arguments for whiteness to blackness, green-ness to redness, blueness to yellowness, and the alpha transparency.

The oklab() color function, just like lab(), generates colors according to their lightness, red/green axis, blue/yellow axis, and an alpha value for color opacity. Also, the values for oklab() are different from that of lab() so please watch out for that.

.element {
  color: oklab(30% 20% 10% / 0.9);
}
  • The first value l represents the degree of whiteness to blackness of the color. Its range being 0 (or 0%) (black) to 0.1 (or 100%) (white).
  • The second value a represents the degree of greenness to redness of the color. Its range being from -0.4 (or -100%) (green) to 0.4 (or 100%) (red).
  • The third value b represents the degree of blueness to yellowness of the color. The value’s range is also from -0.4 (or 0%) (blue) to 0.4 (or -100%) (red).
  • The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

Again, this solves one of the issues in lab which is perceptual uniformity so if you’re looking to use a better alternative to lab, use oklab().

The CSS oklab() color function’s a and b values are actually unbounded. Meaning they don’t technically have an upper or lower limit. But, theoretically, those are the limits for the values according to the spec.

The oklch() function

Diagram of the oklch function showing the arguments for whiteness, chroma, hue, and alpha.

The oklch() color function, just like lch(), generates colors according to their lightness, chroma, hue, and an alpha value for color opacity. The main difference here is that it solves the issues present in lab() and lch().

.element {
  color: oklch(40% 20% 100deg / 0.7);
}
  • The first value l represents the degree of whiteness to blackness of the color. Its range being 0.0 (or 0%) (black) to 1.0 (or 100%) (white).
  • The second value c represents the color’s chroma. Its range being from 0 (or 0%) to 0.4 (or 100%) (it theoretically doesn’t exceed 0.5).
  • The third value h represents the color hue. The value’s range is also from 0 (or 0deg) to 360 (or 360deg).
  • The fourth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

The CSS oklch() color function’s chroma (c) value is actually unbounded. Meaning it doesn’t technically have an upper or lower limit. But, theoretically, the chroma values above are the limits according to the spec.


The color() function

Example of the color function syntax showing the arguments for the colorspace, c1, c2, and c3, and the alpha transparency channel.

The color() function allows access to colors in nine different color spaces, as opposed to the previous color functions mentioned, which only allow access to one.

To use this function, you must simply be aware of these 6 parameters:

  • The first value specifies the color space you want to access colors from. They can either be srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, xyz, xyz-d50, or xyz-d65
  • The next three values (c1, c2, and c3) specifies the coordinates in the color space for the color ranging from 0.0 – 1.0.
  • The sixth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%).

The color-mix() function

Example of the color mix function syntax labelling the in keyword, the the color space, and the inputs followed by the alpha transparency.

The color-mix() function mixes two colors of any type in a given color space. Basically, you can create an endless number of colors with this method and explore more options than you normally would with any other color function. A pretty powerful CSS function, I would say.

.element{
  color-mix(in oklab, hsl(40 20 60) 80%, red 20%);
}

You’re basically mixing two colors of any type in a color space. Do take note, the accepted color spaces here are different from the color spaces accepted in the color() function.

To use this function, you must be aware of these three values:

  • The first value in colorspace specifies the interpolation method used to mix the colors, and these can be any of these 15 color spaces: srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, lab, oklab, xyz, xyz-d50, xyz-d65, hsl, hwb, lch, and oklch.
  • The second and third values specifies an accepted color value and a percentage from 0% to 100%.

The Relative Color Syntax

Example of the relative color syntax labelling the color function, the mandatory keyword, the origin color, the color channel inputs, and the alpha value.

Here’s how it works. We have:

.element{
  color-function(from origin-color c1 c2 c3 / alpha)
}
  • The first value from is a mandatory keyword you must set to extract the color values from origin-color.
  • The second value, origin-color, represents a color function or value or even another relative color that you want to get color from.
  • The next three values, c1, c2, and c3 represent the current color function’s color channels and they correspond with the color function’s valid color values.
  • The sixth and final value is its alpha value for color’s opacity. The value’s range is from 0.0 (or 0%) to 1.0 (or 100%) which either set from the origin-color or set manually,

Let’s take an example, say, converting a color from rgb() to lab():

.element {
  color: lab(from rgb(255 210 01 / 0.5) l a b / a);
}

All the values above will be translated to the corresponding colors in rgb(). Now, let’s take a look at another example where we convert a color from rgb() to oklch():

.element {
    color: oklch(from rgb(255 210 01 / 0.5) 50% 20% h / a);
}

Although, the l and c values were changed, the h and a would be taken from the original color, which in this case is a light yellowish color in rgb().

You can even be wacky and use math functions:

All CSS color functions support the relative color syntax. The relative color syntax, simply put, is a way to access other colors in another color function or value, then translating it to the values of the current color function. It goes “from <color>” to another.

.element {
  color: oklch(from rgb(255 210 01 / 0.5) calc(50% + var(--a)) calc(20% + var(--b)) h / a);
}

The relative color syntax is, however, different than the color() function in that you have to include the color space name and then fully write out the channels, like this:

.element {
  color: color(from origin-color colorspace c1 c2 c3 / alpha);
}

Remember, the color-mix() function is not a part of this. You can have relative color functions inside the color functions you want to mix, yes, but the relative color syntax is not available in color-mix() directly.


Color gradients

Gradient transitioning from a rich magenta color to bright orange at a 145 degree angle.

CSS is totally capable of transitioning from one color to another. See the “CSS Gradients Guide” for a full run-down, including of the different types of gradients with examples.


Properties that support color values

There are a lot of properties that support the use of color. Just so you know, this list does not contain deprecated properties.

accent-color

This CSS property sets the accent color for UI controls like checkboxes and radio buttons, and any other form element

progress {
  accent-color: lightgreen;
}

Accent colors are a way to style unique elements in respect to the chosen color scheme.

background-color

Applies solid colors as background on an element.

.element {
  background-color: #ff7a18;
}
border-color

Shorthand for setting the color of all four borders.

/* Sets all border colors */
.element {
    border-color: lch(50 50 20);
}

/* Sets top, right, bottom, left border colors */
.element {
  border-color: black green red blue;
}
box-shadow

Adds shadows to element for creating the illusion of depth. The property accepts a number of arguments, one of which sets the shadow color.

.element {
  box-shadow: 0 3px 10px rgb(0 0 0 / 0.2);
}
caret-color

Specifies the color of the text input cursor (caret).

.element {
    caret-color: lch(30 40 40);
}
color

Sets the foreground color of text and text decorations.

.element {
  color: lch(80 10 20);
}
color-rule-color

Sets the color of a line between columns in a multi-column layout. This property can’t act alone, so you need to set the columns and column-rule-style property first before using this.

.element {
  column: 3;
  column-rule-style: solid;
  column-rule-color: lch(20 40 40); /* highlight */
}
fill

Sets the color of the SVG shape

.element {
  fill: lch(40 20 10);
}
flood-color

Specifies the flood color to use for <feFlood> and <feDropShadow> elements inside the <filter> element for <svg>. This should not be confused with the flood-color CSS attribute, as this is a CSS property and that’s an HTML attribute (even though they basically do the same thing). If this property is specified, it overrides the CSS flood-color attribute

.element {
  flood-color: lch(20 40 40);
}
lighting-color

Specifies the color of the lighting source to use for <feDiffuseLighting> and <feSpecularLighting> elements inside the <filter> element for <svg>.

.element {
  lighting-color: lch(40 10 20);
}
outline-color

Sets the color of an element’s outline.

.element {
  outline-color: lch(20 40 40);
}
stop-color

Specifies the color of gradient stops for the <stop> tags for <svg>.

.element {
  stop-color: lch(20 40 40);
}
stroke

Defines the color of the outline of <svg>.

.element {
  stroke: lch(20 40 40);
}
text-decoration-color

Sets the color of text decoration lines like underlines.

.element {
  text-decoration-color: lch(20 40 40);
}
text-emphasis-color

Specifies the color of emphasis marks on text.

.element {
  text-emphasis-color: lch(70 20 40);
}
text-shadow

Applies shadow effects to text, including color.

.element {
  text-shadow: 1px 1px 1px lch(50 10 30);
}

Almanac references

Color functions
Color properties


CSS Color Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Sacha Greif
Tue, 17 Jun 2025 13:13:15 +0000


How do you keep up with new CSS features?

Let’s say for example that, hypothetically speaking, you run a popular web development survey focused on CSS, and need to figure out what to include in this year’s edition. (In a total coincidence the aforementioned State of CSS survey for this year is actually open right now — go take it to see what’s new in CSS!)

You might think you can just type “new CSS features 2025” in Google and be done with it. But while this does give us a few promising leads, it also unearths a lot of cookie-cutter content that proclaims CSS Grid as the “next big thing”, despite the fact it’s been well-supported for over eight years now. 

We need a better approach. 

I’ll focus on CSS in this article, but all the resources linked here cover all web platform features, including JavaScript and HTML.

Web.dev

New to the web platform in April

A good general starting point is Google’s web.dev blog, and more specifically Rachel Andrew‘s monthly web platform recaps. Here’s a small sample of those:

CSS-Tricks (and others)

I’d be remiss to not mention that CSS-Tricks is also a great source for up-to-date CSS knowledge, including an ever-growing almanac of CSS features. But you probably already know that since you’re reading this.

And let’s not discount other fine publications that cover CSS. Here are just a few:

Web Platform Features Explorer

Web Platform Features Explorer

If you need something a bit more structured to help you figure out what’s new, Web Platform Features Explorer is great way to look up features based on their Baseline status.

Web Platform Status

Web Platform Status

A similar tool is the Web Platform Status dashboard. This one features more fine-grained filtering tools, letting you narrow down features by Baseline year or even show features mentioned as Top CSS Interop in the latest State of CSS survey!

Another very cool feature is the ability to view a feature’s adoption rate, as measured in terms of percentage of Chrome page views where that feature was used, such as here for the popover HTML attribute:

Showing browser usage statistics of a feature in Chrome.

An important caveat: since sites like Facebook and Google account for a very large percentage of all measured page views, this metric can become skewed once one of these platforms adopts a new feature.

The Web Platform Status’s stats section also features the “chart of shame” (according to Lea Verou), which highlights how certain browsers might be slightly lagging behind their peers in terms of new feature adoption.

Chrome Platform Status

Chrome Platform Status webpage showing usage statistics for CSS subgrid.

That same adoption data can also be found on the Chrome Platform Status dashboard, which gives you even more details, such as usage among top sites, as well as sample URLs of sites that are using a feature. 

Polypane Experimental Chromium Features Dashboard

Polypane webpage showing a search bar for Experimental Chromium Web Platform Features.

Polypane is a great developer-focused browser that provides a ton of useful tools like contrast checkers, multi-viewport views, and more. 

They also provide an experimental Chromium features explorer that breaks new features down by Chromium version, for those of you who want to be at the absolute top of the cutting edge. 

Kevin Powell’s YouTube Channel

A still frame of Kevin Powell in a video on YouTube.

As YouTube’s de facto CSS expert, Kevin Powell often puts up great video recaps of new features. You should definitely be following him, but statistically speaking you probably already are! It’s also worth mentioning that Kevin runs a site that publishes weekly HTML and CSS tips.

CSS Working Group

Of course, you can always also go straight to the source and look at what the CSS Working Group itself has been working on! They have a mailing list you can subscribe to keep tabs on things straight from your inbox, as well as an RSS feed.

Browser release notes

Most browsers publish a set of release notes any time a new version ships. For the most part, you can get a good pulse on when new CSS features are released by following the three big names in browsers:

ChatGPT

Another way to catch up with CSS is to just ask ChatGPT! This sample prompt worked well enough for me:

What are the latest CSS features that have either become supported by browsers in the past year, or will soon become supported? 

Other resources

If you really want to get in the weeds, Igalia’s BCD Watch displays changes to MDN’s browser-compat-data repo, which itself tracks which features are supported in which browsers. 

Also, the latest editions of the HTTP Archive Web Almanac do not seem to include a CSS section specifically, but past editions did feature one, which was a great way to catch up with CSS once a year. 

There’s also caniuse has a news section which does not seem to be frequently updated at the moment, but could potentially become a great resource for up-to-date new feature info in the future.

The IntentToShip bot (available on Bluesky, Mastodon, Twitter) posts whenever a browser vendor ships or changes a feature. You can’t get more cutting-edge than that!

And lastly, there’s a ton of folks on social media who are frequently discussing new CSS features and sharing their own thoughts and experiments with them. If you’re on Bluesky, there’s a starter pack of CSS-Tricks authors that’s a good spot to find a deep community of people.

Wrapping up

Of course, another great way to make sure no new features are slipping through the cracks is to take the State of CSS survey once a year. I use all the resources mentioned above to try and make sure each survey includes every new important feature. What’s more, you can bookmark features by adding them to your “reading list” as you take the survey to get a nice recap at the end.

So go take this year’s State of CSS survey and then let me know on Bluesky how many new features you learned about!


How to Keep Up With New CSS Features originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 16 Jun 2025 16:23:56 +0000


First a quick heads up about… me. I have a weird itch to do “streaming”, so I’m letting myself just be a hardcore beginner and giving it a shot. The plan is just hang out with whoever shows up and make stuff and talk about front end web development and design. So:

Seems like those two platforms make the most sense for that, so here we go.

I made this super sick banner for Twitch, which you can’t even see because it’s covered by UI stuff lol.

Welp.

I suppose you knew that there’s no way I’m letting “liquid glass” slide by this week. Or should I say:

Amazing.

Marie actually beat me to it doing a whole Spark issue on it last week. Obviously CodePen users are all over this design trend, as it’s an absolutely magnetic challenge in CSS. Kevin Powell did a video which happened to drop at the perfect time. Kevin is so good at these things I’m like sick with jealousy about it. Maybe my stream will level up my video teaching skills.

It’s not like CodePen is only now starting to have these glass-like effects. People have been doing it for ages. It had a particular boon when backdrop-filter: blur(2px); became a thing — that’s more like “frosted” glass — but still, Apple is doing that, too. Maybe -webkit-box-reflect will get new life on the web also? Feels related.

Sebastiaan de With fortold it nearly perfectly well. 👏👏👏. Little touches like the reflective progress bar are so cool.


I don’t know if Apple is actually doing this particular detail, I don’t have the new OS yet, but Sebastiaan’s idea is awesome.

Apple is actually quite serious about this, and released a video of the whole idea. Honestly I think it’s kinda awesome looking.

But I did kinda 😬 about the accessibility of it.

No chance the text “Nao” above is passing any contrast test. Nao way amiright?

Feels like text/background contrast has taken a hit. I haven’t seen a full throated takedown of it yet (there are some mentions though), but I imagine that’s coming. There are already settings in there to tone the effects down, I hear.

I thought out loud the other month: literally everything ships inaccessibly. And since having that thought I’ve seen a half dozen things ship that way. Certainly we’re not immune to it, but it’s good motivation to get some more accessibility testing done (we’ve done a good bit already!) on our new editor before it goes out.

Random thing before I sign off. The Oatmeal on Erasers is lovely.

by: Zell Liew
Mon, 16 Jun 2025 12:47:51 +0000


Resize Observer, Mutation Observer, and Intersection Observers are all good APIs that are more performant than their older counterparts:

The API for these three observers are quite similar (but they have their differences which we will go into later). To use an observer, you have to follow the steps below:

  1. Create a new observer with the new keyword: This observer takes in an observer function to execute.
  2. Do something with the observed changes: This is done via the observer function that is passed into the observer.
  3. Observe a specific element: By using the observe method.
  4. (Optionally) unobserve the element: By using the unobserve or disconnect method. (depending on which observer you’re using).

In practice, the above steps looks like this with the ResizeObserver.

// Step 1: Create a new observer
const observer = new ResizeObserver(observerFn)

// Step 2: Do something with the observed changes
function observerFn (entries) {
  for (let entry of entries) {
    // Do something with entry
  }
}

// Step 3: Observe an element
const element = document.querySelector('#some-element')
observer.observe(element);

// Step 4 (optional): Disconnect the observer
observer.disconnect(element)

This looks clear (and understandable) after the steps have been made clear. But it can look like a mess without the comments:

const observer = new ResizeObserver(observerFn)

function observerFn (entries) {
  for (let entry of entries) {
    // Do something with entry
  }
}

const element = document.querySelector('#some-element')
observer.observe(element);

The good news is: I think we can improve the observer APIs and make them easier to use.

The Resize Observer

Let’s start with the ResizeObserver since it’s the simplest of them all. We’ll begin by writing a function that encapsulates the resizeObserver that we create.

function resizeObserver () {
  // ... Do something
}

The easiest way to begin refactoring the ResizeObserver code is to put everything we’ve created into our resizeObserver first.

function resizeObserver () {
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) {
    for (let entry of entries) {
      // Do something with entry
    }
  }

  const node = document.querySelector('#some-element')
  observer.observe(node);
}

Next, we can pass the element into the function to make it simpler. When we do this, we can eliminate the document.querySelector line.

function resizeObserver (element) {
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) {
    for (let entry of entries) {
      // Do something with entry
    }
  }

  observer.observe(node);
}

This makes the function more versatile since we can now pass any element into it.

// Usage of the resizeObserver function
const node = document.querySelector('#some-element')
const obs = resizeObserver(node)

This is already much easier than writing all of the ResizeObserver code from scratch whenever you wish to use it.

Next, it’s quite obvious that we have to pass in an observer function to the callback. So, we can potentially do this:

// Not great
function resizeObserver (node, observerFn) {
  const observer = new ResizeObserver(observerFn)
  observer.observe(node);
}

Since observerFn is always the same — it loops through the entries and acts on every entry — we could keep the observerFn and pass in a callback to perform tasks when the element is resized.

// Better 
function resizeObserver (node, callback) {
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) {
    for (let entry of entries) {
      callback(entry)
    }
  }

  observer.observe(node);
}

To use this, we can pass callback into the resizeObserver — this makes resizeObserver operate somewhat like an event listener which we are already familiar with.

// Usage of the resizeObserver function
const node = document.querySelector('#some-element')
const obs = resizeObserver(node, entry => {
  // Do something with each entry
})

We can make the callback slightly better by providing both entry and entries. There’s no performance hit for passing an additional variable so there’s no harm providing more flexibility here.

function resizeObserver (element, callback) {
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) {
    for (let entry of entries) {
      callback({ entry, entries })
    }
  }

  observer.observe(element);
}

Then we can grab entries in the callback if we need to.

// Usage of the resizeObserver function
// ...
const obs = resizeObserver(node, ({ entry, entries }) => {
  // ...
})

Next, it makes sense to pass the callback as an option parameter instead of a variable. This will make resizeObserver more consistent with the mutationObserver and intersectionObserver functions that we will create in the next article.

function resizeObserver (element, options = {}) {
  const { callback } = options
  const observer = new ResizeObserver(observerFn)

  function observerFn (entries) {
    for (let entry of entries) {
        callback({ entry, entries })
      }
  }

  observer.observe(element);
}

Then we can use resizeObserver like this.

const obs = resizeObserver(node, {
  callback ({ entry, entries }) {
    // Do something ...
  }
})

The observer can take in an option too

ResizeObserver‘s observe method can take in an options object that contains one property, box. This determines whether the observer will observe changes to content-box, border-box or device-pixel-content-box.

So, we need to extract these options from the options object and pass them to observe.

function resizeObserver (element, options = {}) {
  const { callback, ...opts } = options
  // ...
  observer.observe(element, opts);
}

Optional: Event listener pattern

I prefer using callback because it’s quite straightforward. But if you want to use a standard event listener pattern, we can do that, too. The trick here is to emit an event. We’ll call it resize-obs since resize is already taken.

function resizeObserver (element, options = {}) {
  // ...
  function observerFn (entries) {
    for (let entry of entries) {
      if (callback) callback({ entry, entries })
      else {
        node.dispatchEvent(
          new CustomEvent('resize-obs', {
            detail: { entry, entries },
          }),
        )
      }
    }
  }

  // ...
}

Then we can listen to the resize-obs event, like this:

const obs = resizeObserver(node)
node.addEventListener('resize-obs', event => {
  const { entry, entries } = event.detail
})

Again, this is optional.

Unobserving the element

One final step is to allow the user to stop observing the element(s) when observation is no longer required. To do this, we can return two of the observer methods:

  • unobserve: Stops observing one Element
  • disconnect: Stops observing all Elements
function resizeObserver (node, options = {}) {
  // ...
  return {
    unobserve(node) {
      observer.unobserve(node)
    },
    
    disconnect() {
      observer.disconnet()
    }
  }
}

Both methods do the same thing for what we have built so far since we only allowed resizeObserver to observe one element. So, pick whatever method you prefer to stop observing the element.

const obs = resizeObserver(node, {
  callback ({ entry, entries }) {
    // Do something ...
  }
})

// Stops observing all elements 
obs.disconect()

With this, we’ve completed the creation of a better API for the ResizeObserver — the resizeObserver function.

Code snippet

Here’s the code we’ve wrote for resizeObserver

export function resizeObserver(node, options = {}) {
  const observer = new ResizeObserver(observerFn)
  const { callback, ...opts } = options

  function observerFn(entries) {
    for (const entry of entries) {
      // Callback pattern
      if (callback) callback({ entry, entries, observer })
      // Event listener pattern
      else {
        node.dispatchEvent(
          new CustomEvent('resize-obs', {
            detail: { entry, entries, observer },
          })
        )
      }
    }
  }
 
  observer.observe(node)

  return {
    unobserve(node) {
      observer.unobserve(node)
    },
    
    disconnect() {
      observer.disconnect()
    }
  }
}

Using this in practice via Splendid Labz

Splendid Labz has a utils library that contains an enhanced version of the resizeObserver we made above. You can use it if you wanna use a enhanced observer, or if you don’t want to copy-paste the observer code into your projects.

import { resizeObserver } from '@splendidlabz/utils/dom'

const node = document.querySelector('.some-element')
const obs = resizeObserver(node, {
  callback ({ entry, entries }) {
    /* Do what you want here */
  }
})

Bonus: The Splendid Labz resizeObserver is capable of observing multiple elements at once. It can also unobserve multiple elements at once.

const items = document.querySelectorAll('.elements')
const obs = resizeObserver(items, {
  callback ({ entry, entries }) {
    /* Do what you want here */
  }
})

// Unobserves two items at once
const subset = [items[0], items[1]]
obs.unobserve(subset) 

Found this refactoring helpful?

Refactoring is ultra useful (and important) because its a process that lets us create code that’s easy to use or maintain.

If you found this refactoring exercise useful, you might just love how I teach JavaScript to budding developers in my Learn JavaScript course.

In this course, you’ll learn to build 20 real-world components. For each component, we start off simple. Then we add features and you’ll learn to refactor along the way.

That’s it!

Hope you enjoyed this piece and see you in the next one.


A Better API for the Resize Observer originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Sladjana Stojanovic
Thu, 12 Jun 2025 13:58:38 +0000


For years, I believed that drag-and-drop games — especially those involving rotation, spatial logic, and puzzle solving — were the exclusive domain of JavaScript. Until one day, I asked AI:

“Is it possible to build a fully interactive Tangram puzzle game using only CSS?”

The answer: “No — not really. You’ll need JavaScript.” That was all the motivation I needed to prove otherwise.

But first, let’s ask the obvious question: Why would anyone do this?

Well…

  • To know how far CSS can be pushed in creating interactive UIs.
  • To get better at my CSS skills.
  • And it’s fun!

Fair enough?

Now, here’s the unsurprising truth: CSS isn’t exactly made for this. It’s not a logic language, and let’s be honest, it’s not particularly dynamic either. (Sure, we have CSS variables and some handy built-in functions now, hooray!)

In JavaScript, we naturally think in terms of functions, loops, conditions, objects, comparisons. We write logic, abstract things into methods, and eventually ship a bundle that the browser understands. And once it’s shipped? We rarely look at that final JavaScript bundle — we just focus on keeping it lean.

Now ask yourself: isn’t that exactly what Sass does for CSS?

Why should we hand-write endless lines of repetitive CSS when we can use mixins and functions to generate it — cleanly, efficiently, and without caring how many lines it takes, as long as the output is optimized?

So, we put it to the test and it turns out Sass can replace JavaScript, at least when it comes to low-level logic and puzzle behavior. With nothing but maps, mixins, functions, and a whole lot of math, we managed to bring our Tangram puzzle to life, no JavaScript required.

Let the (CSS-only) games begin! 🎉

The game

The game consists of seven pieces: the classic Tangram set. Naturally, these pieces can be arranged into a perfect square (and many other shapes, too). But we need a bit more than just static pieces.

So here’s what I am building:

  • A puzzle goal, which is the target shape the player has to recreate.
  • A start button that shuffles all the pieces into a staging area.
  • Each piece is clickable and interactive.
  • The puzzle should let the user know when they get a piece wrong and also celebrate when they finish the puzzle.

The HTML structure

I started by setting up the HTML structure, which is no small task, considering the number of elements involved.

  • Each shape was given seven radio buttons. I chose radios over checkboxes to take advantage of their built-in exclusivity. Only one can be selected within the same group. This made it much easier to track which shape and state were currently active.
  • The start button? Also a radio input. A checkbox could’ve worked too, but for the sake of consistency, I stuck with radios across the board.
  • The puzzle map itself is just a plain old <div>, simple and effective.
  • For rotation, we added eight radio buttons, each representing a 45-degree increment: 45°, 90°, 135°, all the way to 360°. These simulate rotation controls entirely in CSS.
  • Every potential shadow position got its own radio button too. (Yes, it’s a lot, I know.)
  • And to wrap it all up, I included a classic reset button inside a <form> using <button type="reset">, so players can easily start over at any point.

Given the sheer number of elements required, I used Pug to generate the HTML more efficiently. It was purely a convenience choice. It doesn’t affect the logic or behavior of the puzzle in any way.

Below is a sample of the compiled HTML. It might look overwhelming at first glance (and this is just a portion of it!), but it illustrates the structural complexity involved. This section is collapsed to not nuke your screen, but it can be expanded if you’d like to explore it.

Open HTML Code
<div class="wrapper">
  <div class="tanagram-box"></div>
  <div class="tanagram-box"></div>
  <form class="container">
    <input class="hide_input start" type="checkbox" id="start" autofocus />
    <button class="start-button" type="reset" id="restart">Restart</button>
    <label class="start-button" for="start">Start </label>
    <div class="shadow">
      <input class="hide_input" type="radio" id="blueTriangle-tan" name="tan-active" />
      <input class="hide_input" type="radio" id="yellowTriangle-tan" name="tan-active" />
      <!-- Inputs for others tans -->
      <input class="hide_input" type="radio" id="rotation-reset" name="tan-active" />
      <input class="hide_input" type="radio" id="rotation-45" name="tan-rotation" />
      <input class="hide_input" type="radio" id="rotation-90" name="tan-rotation" />
      <!--radios for 90, 225, 315, 360 -->

      <input class="hide_input" type="checkbox" id="yellowTriangle-tan-1-135" name="tan-rotation" />
      <input class="hide_input" type="checkbox" id="yellowTriangle-tan-1-225" name="tan-rotation" />
      <!-- radio for every possible shape shadows-->

      <label class="rotation rot" for="rotation-45" id="rot45">⟲</label>
      <label class="rotation rot" for="rotation-90" id="rot90">⟲</label>
      <!--radios for 90, 225, 315, 360 -->
      <label class="rotation" for="rotation-reset" id="rotReset">✘</label>

      <label class="blueTriangle tans" for="blueTriangle-tan" id="tanblueTrianglelab"></label>
      <div class="tans tan_blocked" id="tanblueTrianglelabRes"></div>
      <!-- labels for every tan and disabled div -->

      <label class="blueTriangle tans" for="blueTriangle-tan-1-90" id="tanblueTrianglelab-1-90"></label>
      <label class="blueTriangle tans" for="blueTriangle-tan-1-225" id="tanblueTrianglelab-1-225"></label>
      <!-- labels radio for every possible shape shadows-->
      <div class="shape"></div>
    </div>
  </form>
  <div class="tanagram-box"></div>
  <div class="tanagram-box"></div>
  <div class="tanagram-box"></div>
  <div class="tanagram-box"></div>
  <div class="tanagram-box"></div>
</div>

Creating maps for shape data

Now that HTML skeleton is ready, it’s time to inject it with some real power. That’s where our Sass maps come in, and here’s where the puzzle logic starts to shine.

Note: Maps in Sass hold pairs of keys and values, and make it easy to look up a value by its corresponding key. Like objects in JavaScript, dictionaries in Python and, well, maps in C++.

I’m mapping out all the core data needed to control each tangram piece (tan): its color, shape, position, and even interaction logic. These maps contain:

  • the background-color for each tan,
  • the clip-path coordinates that define their shapes,
  • the initial position for each tan,
  • the position of the blocking div (which disables interaction when a tan is selected),
  • the shadow positions (coordinates for the tan’s silhouette displayed on the task board),
  • the grid information, and
  • the winning combinations — the exact target coordinates for each tan, marking the correct solution.
$colors: ( blue-color: #53a0e0, yellow-color: #f7db4f, /* Colors for each tan */ );
$nth-child-grid: ( 1: (2, 3, 1, 2, ), 2: ( 3, 4, 1, 2, ), 4: ( 1, 2, 2, 3, ), /* More entries to be added */);
$bluePosiblePositions: ( 45: none, 90: ( (6.7, 11.2), ), 135: none, 180: none, /* Positions defined up to 360 degrees */);
/* Other tans */

/* Data defined for each tan */
$tansShapes: (
  blueTriangle: (
    color: map.get($colors, blue-color),
    clip-path: ( 0 0, 50 50, 0 100, ),
    rot-btn-position: ( -20, -25, ),
    exit-mode-btn-position: ( -20, -33, ),
    tan-position: ( -6, -37, ),
    diable-lab-position: ( -12, -38, ),
    poss-positions: $bluePosiblePositions,
    correct-position: ((4.7, 13.5), (18.8, 13.3), ),
    transform-origin: ( 4.17, 12.5,),
  ),
);

/* Remaining 7 combinations */
$winningCombinations: (
  combo1: (
    (blueTriangle, 1, 360),
    (yellowTriangle, 1, 225),
    (pinkTriangle, 1, 180),
    (redTriangle, 4, 360),
    (purpleTriangle, 2, 225),
    (square, 1, 90),
    (polygon, 4, 90),
  ),
);

You can see this in action on CodePen, where these maps drive the actual look and behavior of each puzzle piece. At this point, there’s no visible change in the preview. We’ve simply prepared and stored the data for later use.

Using mixins to read from maps

The main idea is to create reusable mixins that will read data from the maps and apply it to the corresponding CSS rules when needed.

But before that, we’ve elevated things to a higher level by making one key decision: We never hard-coded units directly inside the maps. Instead, we built a reusable utility function that dynamically adds the desired unit (e.g., vminpx, etc.) to any numeric value when it’s being used. This way, when can use our maps however we please.

@function get-coordinates($data, $key, $separator, $unit) {
  $coordinates: null;

  // Check if the first argument is a map
  @if meta.type-of($data) == "map" {
    // If the map contains the specified key
    @if map.has-key($data, $key) {
      // Get the value associated with the key (expected to be a list of coordinates)
      $coordinates: map.get($data, $key);
    }

  //  If the first argument is a list
  } @else if meta.type-of($data) == "list" {
    // Ensure the key is a valid index (1-based) within the list
    @if meta.type-of($key) == "number" and $key > 0 and $key <= list.length($data) {
      // Retrieve the item at the specified index
      $coordinates: list.nth($data, $key);
    }

  //  If neither map nor list, throw an error
  } @else {
    @error "Invalid input: First argument must be a map or a list.";
  }

  // If no valid coordinates were found, return null
  @if $coordinates == null {
    @return null;
  }

  //  Extract x and y values from the list
  $x: list.nth($coordinates, 1);
  $y: list.nth($coordinates, -1); // -1 gets the last item (y)

  //  Return the combined x and y values with units and separator
  @return #{$x}#{$unit}#{$separator}#{$y}#{$unit};
}

Sure, nothing’s showing up in the preview yet, but the real magic starts now.

Now we move on to writing mixins. I’ll explain the approach in detail for the first mixin, and the rest will be described through comments.

The first mixin dynamically applies grid-column and grid-row placement rules to child elements based on values stored in a map. Each entry in the map corresponds to an element index (1 through 8) and contains a list of four values: [start-col, end-col, start-row, end-row].

@mixin tanagram-grid-positioning($nth-child-grid) {
  // Loop through numbers 1 to 8, corresponding to the tanam pieces
  @for $i from 1 through 8 {

    // Check if the map contains a key for the current piece (1-8)
    @if map.has-key($nth-child-grid, $i) {

      // Get the grid values for this piece: [start-column, end-column, start-row, end-row]
      $values: map.get($nth-child-grid, $i);

      // Target the nth child (piece) and set its grid positions
      &:nth-child(#{$i}) {
        // Set grid-column: start and end values based on the first two items in the list
        grid-column: #{list.nth($values, 1)} / #{list.nth($values, 2)};

        // Set grid-row: start and end values based on the last two items in the list
        grid-row: #{list.nth($values, 3)} / #{list.nth($values, 4)};
      }
    }
  }
}

We can expect the following CSS to be generated:

.tanagram-box:nth-child(1) {
  grid-column: 2 / 3;
  grid-row: 1 / 2;
}

.tanagram-box:nth-child(2) {
  grid-column: 3 / 4;
  grid-row: 1 / 2;
}

In this mixin, my goal was actually to create all the shapes (tans). I am using clip-path. There were ideas to use fancy SVG images, but this test project is more about testing the logic rather than focusing on beautiful design. For this reason, the simplest solution was to cut the elements according to dimensions while they are still in the square (the initial position of all the tans).

So, in this case, through a static calculation, the $tansShapes map was updated with the clip-path property:

clip-path: (0 0, 50 50, 0 100);

This contains the clip points for all the tans. In essence, this mixin shapes and colors each tan accordingly.

@mixin set-tan-clip-path($tanName, $values) {
  //  Initialize an empty list to hold the final clip-path points
  $clip-path-points: ();

  // Extract the 'clip-path' data from the map, which contains coordinate pairs
  $clip-path-key: map.get($values, clip-path);

  // Get the number of coordinate pairs to loop through
  $count: list.length($clip-path-key);

  //  Loop through each coordinate point
  @for $i from 1 through $count {
    //  Convert each pair of numbers into a formatted coordinate string with units
    $current-point: get-coordinates($clip-path-key, $i, " ", "%");

    //  Add the formatted coordinate to the list, separating each point with a comma
    $clip-path-points: list.append($clip-path-points, #{$current-point}, comma);
  }

  //  Style for the preview element (lab version), using the configured background color
  #tan#{$tanName}lab {
    background: map.get($values, color);
    clip-path: polygon(#{$clip-path-points}); // Apply the full list of clip-path points
  }

  //  Apply the same clip-path to the actual tan element
  .#{$tanName} {
    clip-path: polygon(#{$clip-path-points});
  }
}

and output in CSS should be:

.blueTriangle {
  clip-path: polygon(0% 0%, 50% 50%, 0% 100%);
}
/* other tans */

Start logic

Alright, now I’d like to clarify what should happen first when the game loads.

First, with a click on the Start button, all the tans “go to their positions.” In reality, we assign them a transform: translate() with specific coordinates and a rotation.

.start:checked ~ .shadow #tanblueTrianglelab {
  transform-origin: 4.17vmin 12.5vmin;
  transform: translate(-6vmin,-37vmin) rotate(360deg);
  cursor: pointer;
}

So, we still maintain this pattern. We use transform and simply change the positions or angles (in the maps) of both the tans and their shadows on the task board.

When any tan is clicked, the rotation button appears. By clicking on it, the tan should rotate around its center, and this continues with each subsequent click. There are actually eight radio buttons, and with each click, one disappears and the next one appears. When we reach the last one, clicking it makes it disappear and the first one reappears. This way, we get the impression of clicking the same button (they are, of course, styled the same) and being able to click (rotate the tan) infinitely. This is exactly what the following mixin enables.

@mixin set-tan-rotation-states($tanName, $values, $angles, $color) {
  // This mixin dynamically applies rotation UI styles based on a tan's configuration.
  // It controls the positioning and appearance of rotation buttons and visual feedback when a rotation state is active.
  @each $angle in $angles{
    & ~ #rot#{$angle}{ transform: translate(get-coordinates($values,rot-btn-position,',',vmin )); background: $color;}
    & ~ #rotation-#{$angle}:checked{
      @each $key in map.keys($tansShapes){
        & ~ #tan#{$key}labRes{ visibility: visible; background:rgba(0,0,0,0.4); }
        & ~ #tan#{$key}lab{ opacity:.3; }
        & ~ #rotReset{ visibility: visible; }
      } 
    }
  }
}

And the generated CSS should be:

#blueTriangle-tan:checked ~ #rotation-45:checked ~ #tanblueTrianglelab {
  transform: translate(-6vmin,-37vmin) rotate(45deg);
}

#blueTriangle-tan:checked ~ #rotation-45:checked ~ #tanblueTrianglelabRes {
  visibility: hidden;
}

OK, the following mixins use the set-clip-path and set-rotation mixins. They contain all the information about the tans and their behavior in relation to which tan is clicked and which rotation is selected, as well as their positions (as defined in the second mixin).

@mixin generate-tan-shapes-and-interactions($tansShapes) {
// Applies styling logic and UI interactions for each individual tan shape from the $tansShapes map.
  @each $tanName, $values in $tansShapes{
    $color: color.scale(map.get($values, color), $lightness: 10%); 
    $angles: (45, 90, 135, 180, 225, 270, 315, 360); 
    @include set-tan-clip-path($tanName, $values);

    ##{$tanName}-tan:checked{
      & ~ #tan#{$tanName}Res{ visibility:hidden; }
      & ~ #tan#{$tanName}lab{opacity: 1 !important;background: #{$color};cursor:auto;}
      @each $key in map.keys($tansShapes){
          & ~ #tan#{$tanName}Res:checked ~ #tan#{$key}labRes{visibility: visible;}
      }
      & ~  #rot45{display: flex;visibility: visible;}
      & ~ #rotReset{ transform: translate(get-coordinates($values, exit-mode-btn-position,',', vmin)); }
      @include set-tan-rotation-states($tanName, $values, $angles, $color);
    }  
  }
}
@mixin set-initial-tan-position($tansShapes) {
// This mixin sets the initial position and transformation for both the interactive (`lab`) and shadow (`labRes`) versions
// of each tan shape, based on coordinates provided in the $tansShapes map.
 @each $tanName, $values in $tansShapes{
    & ~ .shadow #tan#{$tanName}lab{
      transform-origin: get-coordinates($values, transform-origin,' ' ,vmin);
      transform: translate( get-coordinates($values,tan-position,',', vmin)) rotate(360deg) ;
      cursor: pointer;
    }
    & ~ .shadow #tan#{$tanName}labRes{
      visibility:hidden;
      transform: translate(get-coordinates($values,diable-lab-position,',',vmin)); 
    }
  }
}

As mentioned earlier, when a tan is clicked, one of the things that becomes visible is its shadow — a silhouette that appears on the task board.

These shadow positions (coordinates) are currently defined statically. Each shadow has a specific place on the map, and a mixin reads this data and applies it to the shadow using transform: translate().

When the clicked tan is rotated, the number of visible shadows on the task board can change, as well as their angles, which is expected.

Of course, special care was taken with naming conventions. Each shadow element gets a unique ID, made from the name (inherited from its parent tan) and a number that represents its sequence position for the given angle.

Pretty cool, right? That way, we avoid complicated naming patterns entirely!

@mixin render-possible-tan-positions( $name, $angle, $possiblePositions, $visibility, $color, $id, $transformOrigin ) {
    // This mixin generates styles for possible positions of a tan shape based on its name, rotation angle, and configuration map.
    // It handles both squares and polygons, normalizing their rotation angles accordingly and applying transform styles if positions exist.}
  @if $name == 'square' {
    $angle: normalize-angle($angle); // Normalizujemo ugao ako je u pitanju square
  } @else if $name == 'polygon'{
    $angle: normalize-polygon-angle($angle);
  }
  @if map.has-key($possiblePositions, $angle) {
    $values: map.get($possiblePositions, $angle);

    @if $values != none {
      $count: list.length($values);

      @for $i from 1 through $count {
        $position: get-coordinates($values, $i, ',', vmin);
        & ~ #tan#{$name}lab-#{$i}-#{$angle} { 
          @if $visibility == visible {
            visibility: visible;
            background-color: $color;
            opacity: .2;
            z-index: 2;
            transform-origin: #{$transformOrigin};
            transform: translate(#{$position}) rotate(#{$angle}deg);
          } @else if $visibility == hidden { visibility: hidden; }
          &:hover{ opacity: 0.5; cursor: pointer; }
        }
      }
    }
  }
}

The generated CSS:

#blueTriangle-tan:checked ~ #tanblueTrianglelab-1-360 {
  visibility: visible;
  background-color: #53a0e0;
  opacity: 0.2;
  z-index: 2;
  transform-origin: 4.17vmin 12.5vmin;
  transform: translate(4.7vmin,13.5vmin) rotate(360deg);
}

This next mixin is tied to the previous one and manages when and how the tan shadows appear while their parent tan is being rotated using the button. It listens for the current rotation angle and checks whether there are any shadow positions defined for that specific angle. If there are, it displays them; if not — no shadows!

@mixin render-possible-positions-by-rotation {
   // This mixin applies rotation to each tan shape. It loops through each tan, calculates its possible positions for each angle, and handles visibility and transformation.
   // It ensures that rotation is applied correctly, including handling the transitions between various tan positions and visibility states.
 @each $tanName, $values in $tansShapes{
    $possiblePositions: map.get($values, poss-positions);
    $possibleTansColor: map.get($values, color);
    $validPosition: get-coordinates($values, correct-position,',' ,vmin);
    $transformOrigin: get-coordinates($values,transform-origin,' ' ,vmin); 
    $rotResPosition: get-coordinates($values,exit-mode-btn-position ,',' ,vmin );
    $angle: 0;
    @for $i from 1 through 8{
      $angle: $i * 45;
      $nextAngle: if($angle + 45 > 360, 45, $angle + 45);
      @include render-position-feedback-on-task($tanName,$angle, $possiblePositions,$possibleTansColor, #{$tanName}-tan, $validPosition,$transformOrigin, $rotResPosition);   
        ##{$tanName}-tan{
        @include render-possible-tan-positions($tanName,$angle, $possiblePositions,hidden, $possibleTansColor, #{$tanName}-tan,$transformOrigin)
      }
        ##{$tanName}-tan:checked{
          @include render-possible-tan-positions($tanName,360, $possiblePositions,visible, $possibleTansColor, #{$tanName}-tan,$transformOrigin);
          & ~ #rotation-#{$angle}:checked {
            @include render-possible-tan-positions($tanName,360, $possiblePositions,hidden, $possibleTansColor, #{$tanName}-tan,$transformOrigin);
            & ~ #tan#{$tanName}lab{transform:translate( get-coordinates($values,tan-position,',', vmin))  rotate(#{$angle}deg) ;}
            & ~ #tan#{$tanName}labRes{ visibility: hidden; }
            & ~ #rot#{$angle}{ visibility: hidden; }
            & ~ #rot#{$nextAngle}{ visibility: visible } 
            @include render-possible-tan-positions($tanName,$angle, $possiblePositions,visible, $possibleTansColor, #{$tanName}-tan,$transformOrigin);
        }
      }
    }
  }
}

When a tan’s shadow is clicked, the corresponding tan should move to that shadow’s position. The next mixin then checks whether this new position is the correct one for solving the puzzle. If it is correct, the tan gets a brief blinking effect and becomes unclickable, signaling it’s been placed correctly. If it’s not correct, the tan simply stays at the shadow’s location. There’s no effect and it remains draggable/clickable.

Of course, there’s a list of all the correct positions for each tan. Since some tans share the same size — and some can even combine to form larger, existing shapes — we have multiple valid combinations. For this Camel task, all of them were taken into account. A dedicated map with these combinations was created, along with a mixin that reads and applies them.

At the end of the game, when all tans are placed in their correct positions, we trigger a “merging” effect — and the silhouette of the camel turns yellow. At that point, the only remaining action is to click the Restart button.

Well, that was long, but that’s what you get when you pick the fun (albeit hard and lengthy) path. All as an ode to CSS-only magic!


Breaking Boundaries: Building a Tangram Puzzle With (S)CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 09 Jun 2025 16:37:09 +0000


Somebody with good taste could’ve made my website, but then it wouldn’t be mine.

My website is ugly because I made it — Taylor Troesh

I love weird design ideas. Probably because so much of what we need to do as web designers is, appropriately, somewhat serious. We want things to be simple, clear, professional, so that people understand them and in many cases pay for them. So when the constraints relax, so can we. It’s unlikely that Taylor’s homepage would “perform well” in any sort of UX testing, but who cares? It’s not impossible to use, it’s just unusual. And crucially, it’s fun and memorable, which is likely a leg up on the last “dashboard” you saw.

It’s cool of Blackle Mori to have documented The Lost CSS Tricks of Cohost.org, a social network ultimately too cool for this world. I sort of suspect a lot of this trickery is available in advanced email clients too, where you definitely don’t have JavaScript, but do have access to more-modern-than-you’d-think HTML and CSS.

And high on the tranfixingly-weird scale is Amit Sheen’s CSS Spotlight Effect. Don’t write it off as a black page with a transparent circle moving with the mouse. I mean, it kinda is, but the filtering and scaling affects that come along for the ride are extremely cool. I actually just got to hang with Amit a bit at CSS Day in Amsterdam this past week. His talk was about building logic gates in CSS was pretty wild, and the whole thing end with him just showing off random amazing Pens of his on stage.

Sometimes design can feel impressive because of the extreme constraints of where you’re seeing it. I’m at an airport lounge right now where I’ve seen an exhibit of sculptures carved into the lead tips of pencils. It’s that same kind of feeling I get when I see art happen in the terminal, a place usually not regarded for it’s beauty. Like seeing a daisy grow from the cracks of a busted up sidewalk.

I like serious design as well. Certainly there is more money in it. I’m allowed to like them both, just like I enjoy both fine dining and fast food. I’ll just hit you with some quicker links though as I bet you’re tired of my going on.

  • Chris Nager weighs in on Design Engineering from his experience with that title at Carta. “The most important skill design engineers possess is the ability to communicate with both designers and frontend engineers. They’re able to give feedback to both sides, and can act as translators between the two worlds through prototypes.” Emphasis mine, naturally.
  • Lea Verou looks critically at a design change at GitHub in Minimalist Affordances: Making the right tradeoffs. Not only was it interesting, it showcases the power of blogging and making coherent points: GitHub noticed, talked with her, and improved the design.
  • Grant Slatton on How to write a good design document. “Think of a design document like a proof in mathematics. The goal of a proof is to convince the reader that the theorem is true. The goal of a design document is to convince the reader the design is optimal given the situation.”
by: Preethi
Mon, 09 Jun 2025 12:58:37 +0000


The HTML popover attribute transforms elements into top-layer elements that can be opened and closed with a button or JavaScript. Most popovers can be light-dismissed, closing when the user clicks or taps outside the popup. Currently, HTML popover lacks built-in auto-close functionality, but it’s easy to add. Auto closing popups are useful for user interfaces like banner notifications — the new-message alerts in phones, for instance.

A picture demo, is worth a thousand words, right? Click on the “Add to my bookmarks” button in the following example. It triggers a notification that dismisses itself after a set amount of time.

Let’s start with the popover

The HTML popover attribute is remarkably trivial to use. Slap it on a div, specify the type of popover you need, and you’re done.

<div popover="manual" id="pop">Bookmarked!</div>

A manual popover simply means it cannot be light-dismissed by clicking outside the element. As a result, we have to hide, show, or toggle the popover’s visibility ourselves explicitly with either buttons or JavaScript. Let’s use a semantic HTML button.

<button popovertarget="pop" popovertargetaction="show">
  Add to my bookmarks
</button>
<div popover="manual" id="pop">Bookmarked!</div>

The popovertarget and popovertargetaction attributes are the final two ingredients, where popovertarget links the button to the popover element and popovertargetaction ensures that the popover is show-n when the button is clicked.

Hiding the popover with a CSS transition

OK, so the challenge is that we have a popover that is shown when a certain button is clicked, but it cannot be dismissed. The button is only wired up to show the popover, but it does not hide or toggle the popover (since we are not explicitly declaring it). We want the popover to show when the button is clicked, then dismiss itself after a certain amount of time.

The HTML popover can’t be closed with CSS, but it can be hidden from the page. Adding animation to that creates a visual effect. In our example, we will hide the popover by eliminating its CSS height property. You’ll learn in a moment why we’re using height, and that there are other ways you can go about it.

We can indeed select the popover attribute using an attribute selector:

[popover] {
  height: 0;
  transition: height cubic-bezier(0.6, -0.28, 0.735, 0.045) .3s .6s;

  @starting-style { 
    height: 1lh;
  }
}

When the popover is triggered by the button, its height value is the one declared in the @starting-style ruleset (1lh). After the transition-delay (which is .6s in the example), the height goes from 1lh to 0 in .3s, effectively hiding the popover.

Once again, this is only hiding the popover, not closing it properly. That’s the next challenge and we’ll need JavaScript for that level of interaction.

Closing the popover with JavaScript

We can start by setting a variable that selects the popover:

const POPOVER = document.querySelector('[popover]');

Next, we can establish a ResizeObserver that monitors the popover’s size:

const POPOVER = document.querySelector('[popover]');
const OBSERVER = 
  new ResizeObserver((entries) => {
    if(entries[0].contentBoxSize[0].blockSize == 0) 
      OBSERVER.unobserve((POPOVER.hidePopover(), POPOVER));
  });

And we can fire that off starting when the button to show the popover is clicked:

const POPOVER = document.querySelector('[popover]');
const OBSERVER = 
  new ResizeObserver((entries) => {
    if(entries[0].contentBoxSize[0].blockSize == 0) 
      OBSERVER.unobserve((POPOVER.hidePopover(), POPOVER));
  });
document.querySelector('button').onclick = () => OBSERVER.observe(POPOVER);

The observer will know when the popover’s CSS height reaches zero at the end of the transition, and, at that point, the popover is closed with hidePopover(). From there, the observer is stopped with unobserve().

In our example, height and ResizeObserver are used to auto-close the notification. You can try any other CSS property and JavaScript observer combination that might work with your preference. Learning about ResizeObserver and MutationObserver can help you find some options.

Setting an HTML fallback

When JavaScript is disabled in the browser, if the popover type is set to any of the light-dismissible types, it acts as a fallback. Keep the popover visible by overriding the style rules that hide it. The user can dismiss it by clicking or tapping anywhere outside the element.

If the popover needs to be light-dismissible only when JavaScript is disabled, then include that popover inside a <noscript> element before the manual popover. It’s the same process as before, where you override CSS styles as needed.

<noscript>
  <div popover="auto" id="pop">Bookmarked!</div>
</noscript>

<div popover="manual" id="pop">Bookmarked!</div>

<!-- goes where <head> element's descendants go -->
<noscript>
  <style>
    [popover] {
      transition: none;
      height: 1lh;
    }
  </style>
</noscript>

When to use this method?

Another way to implement all of this would be to use setTimeout() to create a delay before closing the popover in JavaScript when the button is clicked, then adding a class to the popover element to trigger the transition effect. That way, no observer is needed.

With the method covered in this post, the delay can be set and triggered in CSS itself, thanks to @starting-style and transition-delay — no extra class required! If you prefer to implement the delay through CSS itself, then this method works best. The JavaScript will catch up to the change CSS makes at the time CSS defines, not the other way around.


Creating an Auto-Closing Notification With an HTML Popover originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Temani Afif
Fri, 06 Jun 2025 13:52:42 +0000


If you’re following along, this is the third post in a series about the new CSS shape() function. We’ve learned how to draw lines and arcs and, in this third part, I will introduce the curve command — the missing command you need to know to have full control over the shape() function. In reality, there are more commands, but you will rarely need them and you can easily learn about them later by checking the documentation.

Better CSS Shapes Using shape()

  1. Lines and Arcs
  2. More on Arcs
  3. Curves (you are here!)

The curve command

This command adds a Bézier curve between two points by specifying control points. We can either have one control point and create a Quadratic curve or two control points and create a Cubic curve.

Bézier, Quadratic, Cubic, control points? What?!

For many of you, that definition is simply unclear, or even useless! You can spend a few minutes reading about Bézier curves but is it really worth it? Probably not, unless your job is to create shapes all the day and you have a solid background in geometry.

We already have cubic-bezier() as an easing function for animations but, honestly, who really understands how it works? We either rely on a generator to get the code or we read a “boring” explanation that we forget in two minutes. (I have one right here by the way!)

Don’t worry, this article will not be boring as I will mostly focus on practical examples and more precisely the use case of rounding the corners of irregular shapes. Here is a figure to illustrate a few examples of Bézier curves.

Comparing two curved lines, one with one control point and one with two control points.

The blue dots are the starting and ending points (let’s call them A and B) and the black dots are the control points. And notice how the curve is tangent to the dashed lines illustrated in red.

In this article, I will consider only one control point. The syntax will follow this pattern:

clip-path: shape(
  from Xa Ya, 
  curve to Xb Yb with Xc Yc
);

arc command vs. curve command

We already saw in Part 1 and Part 2 that the arc command is useful establishing rounded edges and corners, but it will not cover all the cases. That’s why you will need the curve command. The tricky part is to know when to use each one and the answer is “it depends.” There is no generic rule but my advice is to first see if it’s possible (and easy) using arc. If not, then you have to use curve.

For some shapes, we can have the same result using both commands and this is a good starting point for us to understand the curve command and compare it with arc.

Take the following example:

This is the code for the first shape:

.shape {
  clip-path: shape(from 0 0,
    arc to 100% 100% of 100% cw,
    line to 0 100%)
}

And for the second one, we have this:

.shape {
  clip-path: shape(from 0 0,
    curve to 100% 100% with 100% 0,
    line to 0 100%)
}

The arc command needs a radius (100% in this case), but the curve command needs a control point (which is 100% 0 in this example).

Two rounded shapes that appear to have similar curves, one using an arc and another using a curve.

Now, if you look closely, you will notice that both results aren’t exactly the same. The first shape using the arc command is creating a quarter of a circle, whereas the shape using the curve command is slightly different. If you place both of them above each other, you can clearly see the difference.

This is interesting because it means we can round some corners using either an arc or a curve, but with slightly different results. Which one is better, you ask? I would say it depends on your visual preference and the shape you are creating.

In Part 1, we created rounded tabs using the arc command, but we can also create them with curve.

Can you spot the difference? It’s barely visible but it’s there.

Notice how I am using the by directive the same way I am doing with arc, but this time we have the control point, which is also relative. This part can be confusing, so pay close attention to this next bit.

Consider the following:

shape(from Xa Ya, curve by Xb Yb with Xc Yc)

It means that both (Xb,Yb) and (Xc,Yc) are relative coordinates calculated from the coordinate of the starting point. The equivalent of the above using a to directive is this:

shape(from Xa Ya, curve to (Xa + Xb) (Ya + Yb) with (Xa + Xc) (Yb + Yc))

We can change the reference of the control point by adding a from directive. We can either use start (the default value), end, or origin.

shape(from Xa Ya, curve by Xb Yb with Xc Yc from end)

The above means that the control point will now consider the ending point instead of the starting point. The result is similar to:

shape(from Xa Ya, curve to (Xa + Xb) (Ya + Yb) with (Xa + Xb + Xc) (Ya + Yb + Yc))

If you use origin, the reference will be the origin, hence the coordinate of the control point becomes absolute instead of relative.

The from directive may add some complexity to the code and the calculation, so don’t bother yourself with it. Simply know it exists in case you face it, but keep using the default value.

I think it’s time for your first homework! Similar to the rounded tab exercise, try to create the inverted radius shape we covered in the Part 1 using curve instead of arc. Here are both versions for you to reference, but try to do it without peeking first, if you can.

Let’s draw more shapes!

Now that we have a good overview of the curve command, let’s consider more complex shapes where arc won’t help us round the corners and the only solution is to draw curves instead. Considering that each shape is unique, so I will focus on the technique rather than the code itself.

Slanted edge

Let’s start with a rectangular shape with a slanted edge.

A slanted rectangle shape in two stages, first with sharp edges, then with curved edges.

Getting the shape on the left is quite simple, but the shape on the right is a bit tricky. We can round two corners with a simple border-radius, but for the slanted edge, we will use shape() and two curve commands.

The first step is to write the code of the shape without rounded corners (the left one) which is pretty straightforward since we’re only working with the line command:

.shape {
  --s: 90px;  /* slant size */

  clip-path: 
    shape(from 0 0,
    line to calc(100% - var(--s)) 0,
    line to 100% 100%,
    line to 0 100%
    );
}

Then we take each corner and try to round it by modifying the code. Here is a figure to illustrate the technique I am going to use for each corner.

Diagrammiong a rounded rectangular shape in three stages, first with sharp edges, then with points indicating where the curve control points are, then the completed shape.

We define a distance, R, that controls the radius. From each side of the corner point, I move by that distance to create two new points, which are illustrated above in red. Then, I draw my curve using the new points as starting and ending points. The corner point will be the control point.

The code becomes:

.shape {
  --s: 90px;  /* slant size */

  clip-path: 
    shape(from 0 0,
    Line  to Xa Ya,
    curve to Xb Yb with calc(100% - var(--s)) 0,
    line to 100% 100%,
    line to 0 100%
    );
}

Notice how the curve is using the coordinates of the corner point in the with directive, and we have two new points, A and B.

Until now, the technique is not that complex. For each corner point, you replace the line command with line + curve commands where the curve command reuses the old point in its with directive.

If we apply the same logic to the other corner, we get the following:

.shape {
  --s: 90px;  /* slant size */

  clip-path: 
    shape(from 0 0,
    line  to Xa Ya, 
    curve to Xb Yb with calc(100% - var(--s)) 0,
    line  to Xc Yc,
    curve to Xd Yd with 100% 100%,
    line to 0 100%
    );
}

Now we need to calculate the coordinates of the new points. And here comes the tricky part because it’s not always simple and it may require some complex calculation. Even if I detail this case, the logic won’t be the same for the other shapes we’re making, so I will skip the math part and give you the final code:

.box {
  --h: 200px; /* element height */
  --s: 90px;  /* slant size */
  --r: 20px;  /* radius */
  
  height: var(--h);
  border-radius: var(--r) 0 0 var(--r);
  --_a: atan2(var(--s), var(--h));
  clip-path: 
    shape(from 0 0,
    line  to calc(100% - var(--s) - var(--r)) 0,
    curve by calc(var(--r) * (1 + sin(var(--_a)))) 
              calc(var(--r) * cos(var(--_a)))
    with var(--r) 0,
    line  to calc(100% - var(--r) * sin(var(--_a))) 
              calc(100% - var(--r) * cos(var(--_a))),
    curve to calc(100% - var(--r)) 100%  with 100% 100%,
    line to 0 100%
    );
}

I know the code looks a bit scary, but the good news is that the code is also really easy to control using CSS variables. So, even if the math is not easy to grasp, you don’t have to deal with it. It should be noted that I need to know the height to be able to calculate the coordinates which means the solution isn’t perfect because the height is a fixed value.

Arrow-shaped box

Here’s a similar shape, but this time we have three corners to round using the curve command.

The final code is still complex but I followed the same steps. I started with this:

.shape {
  --s: 90px; 

  clip-path: 
    shape(from 0 0,
    /* corner #1 */
    line to calc(100% - var(--s)) 0,
    /* corner #2 */
    line to 100% 50%,
    /* corner #3 */
    line to calc(100% - var(--s)) 100%,

    line to 0 100%
    );
}

Then, I modified it into this:

.shape {
  --s: 90px; 

  clip-path: 
    shape(from 0 0,
    /* corner #1 */
    line  to Xa Ya
    curve to Xb Yb with calc(100% - var(--s)) 0,
    /* corner #2 */
    line  to Xa Ya
    curve to Xb Yb with 100% 50%,
    /* corner #3 */
    line  to Xa Yb
    curve to Xb Yb with calc(100% - var(--s)) 100%,

    line to 0 100%
    );
}

Lastly, I use a pen and paper to do all the calculations.

You might think this technique is useless if you are not good with math and geometry, right? Not really, because you can still grab the code and use it easily since it’s optimized using CSS variables. Plus, you aren’t obligated to be super accurate and precise. You can rely on the above technique and use trial and error to approximate the coordinates. It will probably take you less time than doing all the math.

Rounded polygons

I know you are waiting for this, right? Thanks to the new shape() and the curve command, we can now have rounded polygon shapes!

Three rounded polygon shapes, first a pentagon, second a triangle, and third an octagon.

Here is my implementation using Sass where you can control the radius, number of sides and the rotation of the shape:

If we omit the complex geometry part, the loop is quite simple as it relies on the same technique with a line + curve per corner.

$n: 9; /* number of sides*/
$r: .2; /* control the radius [0 1] */
$a: 15deg; /* control the rotation */

.poly {
  aspect-ratio: 1;
  $m: ();
  @for $i from 0 through ($n - 1) {
    $m: append($m, line  to Xai Yai, comma);
    $m: append($m, curve to Xbi Ybi with Xci Yci, comma);
  } 
  clip-path: shape(#{$m});
}

Here is another implementation where I define the variables in CSS instead of Sass:

Having the variables in CSS is pretty handy especially if you want to have some animations. Here is an example of a cool hover effect applied to hexagon shapes:

I have also updated my online generator to add the radius parameter. If you are not familiar with Sass, you can easily copy the CSS code from there. You will also find the border-only and cut-out versions!

A rounded shape with six sides next to a cutout of a rounded shape with six edges.

Conclusion

Are we done with the curve command? Probably not, but we have a good overview of its potential and all the complex shapes we can build with it. As for the code, I know that we have reached a level that is not easy for everyone. I could have extended the explanation by explicitly breaking down the math, but then this article would be overly complex and make it seem like using shape() is harder than it is.

This said, most of the shapes I code are available within my online collection that I constantly update and optimize so you can easily grab the code of any shape!

If you want a good follow-up to this article, I wrote an article for Frontend Masters where you can create blob shapes using the curve command.

Three blob shapes in a single row, each colored with a gradient that goes left to right from dark orange to light orange.

Better CSS Shapes Using shape()

  1. Lines and Arcs
  2. More on Arcs
  3. Curves (you are here!)

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

by: Daniel Schwarz
Thu, 05 Jun 2025 13:45:56 +0000


In many countries, web accessibility is a human right and the law, and there can be heavy fines for non-compliance. Naturally, this means that text and icons and such must have optimal color contrast in accordance with the benchmarks set by the Web Content Accessibility Guidelines (WCAG). Now, there are quite a few color contrast checkers out there (Figma even has one built-in now), but the upcoming contrast-color() function doesn’t check color contrast, it outright resolves to either black or white (whichever one contrasts the most with your chosen color).

Right off the bat, you should know that we’ve sorta looked at this feature before. Back then, however, it was called color-contrast() instead of contrast-color() and had a much more convoluted way of going about things. It was only released in Safari Technology Preview 122 back in 2021, and that’s still the case at the time I’m writing this (now at version 220).

You’d use it like this:

button {
  --background-color: darkblue;
  background-color: var(--background-color);
  color: contrast-color(var(--background-color));
}

Here, contrast-color() has determined that white contrasts with darkblue better than black does, which is why contrast-color() resolves to white. Pretty simple, really, but there are a few shortcomings, which includes a lack of browser support (again, it’s only in Safari Technology Preview at the moment).

We can use contrast-color() conditionally, though:

@supports (color: contrast-color(red)) {
  /* contrast-color() supported */
}

@supports not (color: contrast-color(red)) {
  /* contrast-color() not supported */
}

The shortcomings of contrast-color()

First, let me just say that improvements are already being considered, so here I’ll explain the shortcomings as well as any improvements that I’ve heard about.

Undoubtedly, the number one shortcoming is that contrast-color() only resolves to either black or white. If you don’t want black or white, well… that sucks. However, the draft spec itself alludes to more control over the resolved color in the future.

But there’s one other thing that’s surprisingly easy to overlook. What happens when neither black nor white is actually accessible against the chosen color? That’s right, it’s possible for contrast-color() to just… not provide a contrasting color. Ideally, I think we’d want contrast-color() to resolve to the closest accessible variant of a preferred color. Until then, contrast-color() isn’t really usable.

Another shortcoming of contrast-color() is that it only accepts arguments of the <color> data type, so it’s just not going to work with images or anything like that. I did, however, manage to make it “work” with a gradient (basically, two instances of contrast-color() for two color stops/one linear gradient):

<button>
  <span>A button</span>
</button>
button {
  background: linear-gradient(to right, red, blue);

  span {
    background: linear-gradient(to right, contrast-color(red), contrast-color(blue));
    color: transparent;
    background-clip: text;
  }
}

The reason this looks so horrid is that, as mentioned before, contrast-color() only resolves to black or white, so in the middle of the gradient we essentially have 50% grey on purple. This problem would also get solved by contrast-color() resolving to a wider spectrum of colors.

But what about the font size? As you might know already, the criteria for color contrast depends on the font size, so how does that work? Well, at the moment it doesn’t, but I think it’s safe to assume that it’ll eventually take the font-size into account when determining the resolved color. Which brings us to APCA.

APCA (Accessible Perceptual Contrast Algorithm) is a new algorithm for measuring color contrast reliably. Andrew Somers, creator of APCA, conducted studies (alongside many other independent studies) and learned that 23% of WCAG 2 “Fails” are actually accessible. In addition, an insane 47% of “Passes” are inaccessible.

Not only should APCA do a better job, but the APCA Readability Criterion (ARC) is far more nuanced, taking into account a much wider spectrum of font sizes and weights (hooray for me, as I’m very partial to 600 as a standard font weight). While the criterion is expectedly complex and unnecessarily confusing, the APCA Contrast Calculator does a decent-enough job of explaining how it all works visually, for now.

contrast-color() doesn’t use APCA, but the draft spec does allude to offering more algorithms in the future. This wording is odd as it suggests that we’ll be able to choose between the APCA and WCAG algorithms. Then again, we have to remember that the laws of some countries will require WCAG 2 compliance while others require WCAG 3 compliance (when it becomes a standard).

That’s right, we’re a long way off of APCA becoming a part of WCAG 3, let alone contrast-color(). In fact, it might not even be a part of it initially (or at all), and there are many more hurdles after that, but hopefully this sheds some light on the whole thing. For now, contrast-color() is using WCAG 2 only.

Using contrast-color()

Here’s a simple example (the same one from earlier) of a darkblue-colored button with accessibly-colored text chosen by contrast-color(). I’ve put this darkblue color into a CSS variable so that we can define it once but reference it as many times as is necessary (which is just twice for now).

button {
  --background-color: darkblue;
  background-color: var(--background-color);
  /* Resolves to white */
  color: contrast-color(var(--background-color));
}

And the same thing but with lightblue:

button {
  --background-color: lightblue;
  background-color: var(--background-color);
  /* Resolves to black */
  color: contrast-color(var(--background-color));
}

First of all, we can absolutely switch this up and use contrast-color() on the background-color property instead (or in-place of any <color>, in fact, like on a border):

button {
  --color: darkblue;
  color: var(--color);
  /* Resolves to white */
  background-color: contrast-color(var(--color));
}

Any valid <color> will work (named, HEX, RGB, HSL, HWB, etc.):

button {
  /* HSL this time */
  --background-color: hsl(0 0% 0%);
  background-color: var(--background-color);
  /* Resolves to white */
  color: contrast-color(var(--background-color));
}

Need to change the base color on the fly (e.g., on hover)? Easy:

button {
  --background-color: hsl(0 0% 0%);
  background-color: var(--background-color);
  /* Starts off white, becomes black on hover */
  color: contrast-color(var(--background-color));

  &:hover {
    /* 50% lighter */
    --background-color: hsl(0 0% 50%);
  }
}

Similarly, we could use contrast-color() with the light-dark() function to ensure accessible color contrast across light and dark modes:

:root {
  /* Dark mode if checked */
  &:has(input[type="checkbox"]:checked) {
    color-scheme: dark;
  }

  /* Light mode if not checked */
  &:not(:has(input[type="checkbox"]:checked)) {
    color-scheme: light;
  }

  body {
    /* Different background for each mode */
    background: light-dark(hsl(0 0% 50%), hsl(0 0% 0%));
    /* Different contrasted color for each mode */
    color: light-dark(contrast-color(hsl(0 0% 50%)), contrast-color(hsl(0 0% 0%));
  }
}

The interesting thing about APCA is that it accounts for the discrepancies between light mode and dark mode contrast, whereas the current WCAG algorithm often evaluates dark mode contrast inaccurately. This one nuance of many is why we need not only a new color contrast algorithm but also the contrast-color() CSS function to handle all of these nuances (font size, font weight, etc.) for us.

This doesn’t mean that contrast-color() has to ensure accessibility at the expense of our “designed” colors, though. Instead, we can use contrast-color() within the prefers-contrast: more media query only:

button {
  --background-color: hsl(270 100% 50%);
  background-color: var(--background-color);
  /* Almost white (WCAG AA: Fail) */
  color: hsl(270 100% 90%);

  @media (prefers-contrast: more) {
    /* Resolves to white (WCAG AA: Pass) */
    color: contrast-color(var(--background-color));
  }
}

Personally, I’m not keen on prefers-contrast: more as a progressive enhancement. Great color contrast benefits everyone, and besides, we can’t be sure that those who need more contrast are actually set up for it. Perhaps they’re using a brand new computer, or they just don’t know how to customize accessibility settings.

Closing thoughts

So, contrast-color() obviously isn’t useful in its current form as it only resolves to black or white, which might not be accessible. However, if it were improved to resolve to a wider spectrum of colors, that’d be awesome. Even better, if it were to upgrade colors to a certain standard (e.g., WCAG AA) if they don’t already meet it, but let them be if they do. Sort of like a failsafe approach? This means that web browsers would have to take the font size, font weight, element, and so on into account.

To throw another option out there, there’s also the approach that Windows takes for its High Contrast Mode. This mode triggers web browsers to overwrite colors using the forced-colors: active media query, which we can also use to make further customizations. However, this effect is quite extreme (even though we can opt out of it using the forced-colors-adjust CSS property and use our own colors instead) and macOS’s version of the feature doesn’t extend to the web.

I think that forced colors is an incredible idea as long as users can set their contrast preferences when they set up their computer or browser (the browser would be more enforceable), and there are a wider range of contrast options. And then if you, as a designer or developer, don’t like the enforced colors, then you have the option to meet accessibility standards so that they don’t get enforced. In my opinion, this approach is the most user-friendly and the most developer-friendly (assuming that you care about accessibility). For complete flexibility, there could be a CSS property for opting out, or something. Just color contrast by default, but you can keep the colors you’ve chosen as long as they’re accessible.

What do you think? Is contrast-color() the right approach, or should the user agent bear some or all of the responsibility? Or perhaps you’re happy for color contrast to be considered manually?


Exploring the CSS contrast-color() Function… a Second Time originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Juan Diego Rodríguez
Thu, 05 Jun 2025 11:13:00 +0000


The State of CSS 2025 Survey dropped a few days ago, and besides waiting for the results, it’s exciting to see a lot of the new things shipped to CSS over the past year reflected in the questions. To be specific, the next survey covers the following features:

Again, a lot!

However, I think the most important questions (regarding CSS) are asked at the end of each section. I am talking about the “What are your top CSS pain points related to ______?” questions. These sections are optional, but help user agents and the CSS Working Group know what they should focus on next.

By nature of comments, those respondents with strong opinions are most likely to fill them in, skewing data towards issues that maybe the majority doesn’t have. So, even if you don’t have a hard-set view on a CSS pain point, I encourage you to fill them — even with your mild annoyances.


The State of CSS 2025 Survey is out! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Andy Clarke
Tue, 03 Jun 2025 14:39:04 +0000


Like ’em or loath ’em, whether you’re showing an alert, a message, or a newsletter signup, dialogue boxes draw attention to a particular piece of content without sending someone to a different page. In the past, dialogues relied on a mix of divisions, ARIA, and JavaScript. But the HTML dialog element has made them more accessible and style-able in countless ways.

So, how can you take dialogue box design beyond the generic look of frameworks and templates? How can you style them to reflect a brand’s visual identity and help to tell its stories? Here’s how I do it in CSS using ::backdrop, backdrop-filter, and animations.

Homepage design for Mike Worth website, showing a cartoon gorilla in a dark cave in an Indiana Jones style.
Design by Andy Clarke, Stuff & Nonsense. Mike Worth’s website will launch in June 2025, but you can see examples from this article on CodePen.

I mentioned before that Emmy-award-winning game composer Mike Worth hired me to create a highly graphical design. Mike loves ’90s animation, and he challenged me to find ways to incorporate its retro style without making a pastiche. However, I also needed to achieve that retro feel while maintaining accessibility, performance, responsiveness, and semantics.

A brief overview of dialog and ::backdrop

Let’s run through a quick refresher.

Note: While I mostly refer to “dialogue boxes” throughout, the HTML element is spelt dialog.

dialog is an HTML element designed for implementing modal and non-modal dialogue boxes in products and website interfaces. It comes with built-in functionality, including closing a box using the keyboard Esc key, focus trapping to keep it inside the box, show and hide methods, and a ::backdrop pseudo-element for styling a box’s overlay.

The HTML markup is just what you might expect:

<dialog>
  <h2>Keep me informed</h2>
  <!-- ... -->
  <button>Close</button>
</dialog>

This type of dialogue box is hidden by default, but adding the open attribute makes it visible when the page loads:

<dialog open>
  <h2>Keep me informed</h2>
  <!-- ... -->
  <button>Close</button>
</dialog>

I can’t imagine too many applications for non-modals which are open by default, so ordinarily I need a button which opens a dialogue box:

<dialog>
  <!-- ... -->
</dialog>
<button>Keep me informed</button>

Plus a little bit of JavaScript, which opens the modal:

const dialog = document.querySelector("dialog");
const showButton = document.querySelector("dialog + button");
showButton.addEventListener("click", () => {
  dialog.showModal();
});

Closing a dialogue box also requires JavaScript:

const closeButton = document.querySelector("dialog button");
closeButton.addEventListener("click", () => {
  dialog.close();
});

Unless the box contains a form using method="dialog", which allows it to close automatically on submit without JavaScript:

<dialog>
  <form method="dialog">
    <button>Submit</button>
  </form>
</dialog>

The dialog element was developed to be accessible out of the box. It traps focus, supports the Esc key, and behaves like a proper modal. But to help screen readers announce dialogue boxes properly, you’ll want to add an aria-labelledby attribute. This tells assistive technology where to find the dialogue box’s title so it can be read aloud when the modal opens.

<dialog aria-labelledby="dialog-title">
  <h2 id="dialog-title">Keep me informed</h2>
  <!-- ... -->
</dialog>

Most tutorials I’ve seen include very little styling for dialog and ::backdrop, which might explain why so many dialogue boxes have little more than border radii and a box-shadow applied.

Two examples of plain-looking dialogs with white backgrounds and box shadows.
Out-of-the-box dialogue designs

I believe that every element in a design — no matter how small or infrequently seen — is an opportunity to present a brand and tell a story about its products or services. I know there are moments during someone’s journey through a design where paying special attention to design can make their experience more memorable.

Dialogue boxes are just one of those moments, and Mike Worth’s design offers plenty of opportunities to reflect his brand or connect directly to someone’s place in Mike’s story. That might be by styling a newsletter sign-up dialogue to match the scrolls in his news section.

Dialog in the design is an open scroll with script lettering and an email signup form.
Mike Worth concept design, designed by Andy Clarke, Stuff & Nonsense.

Or making the form modal on his error pages look like a comic-book speech balloon.

Example of a dialog in the shape of a shat bubble with an email signup form inside.
Mike Worth concept design, designed by Andy Clarke, Stuff & Nonsense.

dialog in action

Mike’s drop-down navigation menu looks like an ancient stone tablet.

A menu of links set against a cartoon stone tablet illustration.
Mike Worth, designed by Andy Clarke, Stuff & Nonsense.

I wanted to extend this look to his dialogue boxes with a three-dimensional tablet and a jungle leaf-filled backdrop.

A dialog against a cartoon stone tablet illustration with an email signup for inside.
Mike Worth, designed by Andy Clarke, Stuff & Nonsense.

This dialog contains a newsletter sign-up form with an email input and a submit button:

<dialog>
  <h2>Keep me informed</h2>
  <form>
    <label for="email" data-visibility="hidden">Email address</label>
    <input type="email" id="email" required>
    <button>Submit</button>
  </form>
  <button>x</button>
</dialog>

I started by applying dimensions to the dialog and adding the SVG stone tablet background image:

dialog {
  width: 420px;
  height: 480px;
  background-color: transparent;
  background-image: url("dialog.svg");
  background-repeat: no-repeat;
  background-size: contain;
}

Then, I added the leafy green background image to the dialogue box’s generated backdrop using the ::backdrop pseudo element selector:

dialog::backdrop {
  background-image: url("backdrop.svg");
  background-size: cover;
}
Dialog sitting on top of a safari jungle pattern as the backdrop. The dialog is styled with a cartoon stone tablet illustration with an email signup form inside.
Mike Worth, designed by Andy Clarke, Stuff & Nonsense.

I needed to make it clear to anyone filling in Mike’s form that their email address is in a valid format. So I combined :has and :valid CSS pseudo-class selectors to change the color of the submit button from grey to green:

dialog:has(input:valid) button {
  background-color: #7e8943;
  color: #fff;
}

I also wanted this interaction to reflect Mike’s fun personality. So, I also changed the dialog background image and applied a rubberband animation to the box when someone inputs a valid email address:

dialog:has(input:valid) {
  background-image: url("dialog-valid.svg");
  animation: rubberBand 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}

@keyframes rubberBand {
  from { transform: scale3d(1, 1, 1); }
  30% { transform: scale3d(1.25, 0.75, 1); }
  40% { transform: scale3d(0.75, 1.25, 1); }
  50% { transform: scale3d(1.15, 0.85, 1); }
  65% { transform: scale3d(0.95, 1.05, 1); }
  75% { transform: scale3d(1.05, 0.95, 1); }
  to { transform: scale3d(1, 1, 1); }
}

Tip: Daniel Eden’s Animate.css library is a fabulous source of “Just-add-water CSS animations” like the rubberband I used for this dialogue box.

Changing how an element looks when it contains a valid input is a fabulous way to add interactions that are, at the same time, fun and valuable for the user.

Dialog sitting on top of a safari jungle pattern as the backdrop. The dialog is styled with a cartoon stone tablet illustration with an email signup form inside.
Mike Worth, designed by Andy Clarke, Stuff & Nonsense.

That combination of :has and :valid selectors can even be extended to the ::backdrop pseudo-class, to change the backdrop’s background image:

dialog:has(input:valid)::backdrop {
  background-image: url("backdrop-valid.svg");
}

Try it for yourself:

Conclusion

We often think of dialogue boxes as functional elements, as necessary interruptions, but nothing more. But when you treat them as opportunities for expression, even the smallest parts of a design can help shape a product or website’s personality.

The HTML dialog element, with its built-in behaviours and styling potential, opens up opportunities for branding and creative storytelling. There’s no reason a dialogue box can’t be as distinctive as the rest of your design.


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.


Getting Creative With HTML Dialog originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

by: Chris Coyier
Mon, 02 Jun 2025 17:01:09 +0000


Let’s do typography stuff!

  • Video: “A live demo by me of early font editors on a real Macintosh Plus” by Mark Simonson
  • Font: “Is this font easy for you to read? Good—that’s the idea.” Hyperlegible is the name of the font, designed for people with low vision.
  • Technique: “Fluid typography means thinking in terms of type scales and flexible spacing across your defined design space.” Richard Rutter goes retrofitting a new type sizing technique into an old layout. Miriam is also thinking about all this. Richard also has a good one on avoiding faux bold, which has afflicted me many times. My hot take is that browsers shouldn’t do faux nuthin.
  • Font: “To help young readers of all skill levels, we’re introducing Kermit, a child-friendly typeface created by the type design studio Underware.” It’s fairly pricey but I can imagine it being perfect for some projects. Like Comic Sans but cooler. P.S. you should really try Comic Code on CodePen, it’s awesome.
  • Fonts: “UNCUT.wtf is a free typeface catalogue, focusing on somewhat contemporary type.” A lot of them are super similar which makes me wonder if many of them come from students taking a design class or something.
  • Journey: “One day, I saw what felt like Gorton on a ferry traversing the waters Bay Area. A few weeks later, I spotted it on a sign in a national park. Then on an intercom. On a street lighting access cover. In an elevator. At my dentist’s office. In an alley.” Marcin Wichary on Manhattan’s hardest working typeface.
  • Performance: “… file sizes of web fonts? I personally don’t have a gut feeling how much is too much and how much is to be expected.” Stoyan Stefanov reckons 20k is fair.
  • Behind the Scenes: “This meant that on web we could simply start our font stacks with Verdana, pick a couple of reasonable fallbacks, and get IKEA branding effectively for free.” IKEA didn’t end up using Verdana, but I wish they did, I kinda love it at small sizes. This is a great look at a major typographic choice at a major brand from Robin Whittleton.
  • Technique: “…we added text-wrap: balance on WordPress.org, and quickly got community feedback that it led to awkward, unexpected breaks in Japanese and Korean.” Kelly Choyce-Dwan whips out stuff like word-break: auto-phrase; for the win.
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.

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.