RSS FeedTwitterMastodonBlueskyShare IconHeart IconGithub IconArrow IconClock IconGUI Challenges IconHome IconNote IconBlog IconCSS IconJS IconHTML IconMedia IconGit IconSpeaking IconTools Icon
@property --hue { syntax: '<angle>'; initial-value: 5rad; inherits: true; }
@property --surface { syntax: '<color>'; initial-value: #333; inherits: true; }
A series of images of an avatar doing a bunch of skateboard tricks.

Type safe CSS design systems with @property

8 min read

CSS types are a worthy investment into type safety in your front-end work. We're still awaiting cross browser interop, but we'll get there 🙂

In case you've never seen one, here's a typed CSS variable with @property:

@property --focal-size {
  syntax: '<length-percentage>';
  initial-value: 100%;
  inherits: false;

Used that one so I could animate a gradient mask image. Pretty sweet.

Here's a preview of what CSS type safety can do, and what I'll be explaining:

CSS type safety basics #

When learning Rust or TypeScript, a great place is to start with the type primitives. In CSS, a few of those are:

<angle> <length> <percentage> <length-percentage> <number> <integer> <color> <string> <time> <dimension> <ratio> <flex> <frequency> <resolution> <image> <position> <hue> <url> <custom-ident>
More types on MDN and a full list of grammars and types on

Peep another typed prop definition:

@property --hue {
  syntax: '<angle>';
  initial-value: .5turn;
  inherits: false;

Use it just like you always would var(--hue) and it'll be .5turn. BUT, try and set it to a value that doesn't match its type? Fails, value will still be .5turn. The custom property will not allow itself to be assigned a value that doesn't match it's type, always reverting to the last known good value.

.card {
  --hue: 90deg; /* ✅ */
  --hue: #f00;  /* ❌ */
  background: oklch(98% .01 var(--hue));

  /* background will always resolve 👍🏻 */
  /* --hue resolves 90deg *.

This is CSS type safety. It doesn't crash the page, lock a thread, and unfortunately also won't tell you in any console that there's been an attempt to set the --hue prop to a <color> and not an <angle>. But I think some better custom property tooling could help 😏.

Level 2 #

So far I defined a custom property as an <angle> and used it as a background. No property nesting.

Go a level deeper by making a custom property include another custom property. Here --_bg is an <any> kinda (because it's an untyped custom property at this point), with a nested custom property --hue:

.card {
  --_bg: oklch(98% .01 var(--hue));
  background: var(--_bg);

  @media (prefers-color-scheme: dark) {
    --_bg: oklch(15% .1 var(--hue));

You can go many levels deep, but not too deep. AND, you can type some or all of your variables. Next, we'll make some type safe 2-level deep custom properties.

Sounds like Typescript and SCSS right? Incremental adoption for tighter systems.

Design systems relevance #

Let's build a typed light and dark adaptive color scheme starter!

First, a type safe brand hue. I'll be making an <input type=text> element that will write to this value whatever we type in it. Since it's type safe, we'll see how other custom properties that depend on it, won't break if the value of --hue is set to "poots" or something.

@property --hue {
  syntax: '<angle>';
  initial-value: 5rad;
  inherits: true;

For brevity, I'll only be setting up the surfaces of an adaptive color scheme, it'll provide plenty of insights into the process of typing a design system.

Here's 3 surfaces, 1 to be the background of the page --surface, and 2 others that are intended to either be a surface on top of the page bg or under. Their initial value isn't an exciting, but we'll get there in the next part.

@property --surface {
  syntax: '<color>';
  initial-value: #333;
  inherits: true;

@property --surface-over {
  syntax: '<color>';
  initial-value: #444;
  inherits: true;

@property --surface-under {
  syntax: '<color>';
  initial-value: #222;
  inherits: true;

The important bit here is that they're a color type.

Now, we can assign more meaningful values to the surface colors. You could use @media (prefers-color-scheme) if you like, but here, since I wanted to show light and dark with a switch, I'm using :has():

@layer demo.theme {
  html:has(#light:checked) {
    color-scheme: light;
    --surface: oklch(90% .05 var(--hue));
    --surface-over: oklch(99% .02 var(--hue));
    --surface-under: oklch(85% .075 var(--hue));
  html:has(#dark:checked) {
    color-scheme: dark;
    --surface: oklch(20% .1 var(--hue));
    --surface-over: oklch(30% .1 var(--hue));
    --surface-under: oklch(15% .1 var(--hue));

That's essentially the setup and orchestration for a type safe custom property setup. All that's left is to use them. Check out the Codepen to see all the neat ways these are valuable in creating an adaptive color scheme: box-shadows, borders, and more!

The last part #

The "theme tint" text input in the demo, go ahead and start typing crap into it. None of the color system will fail due to a typo or assigned value that doesn't match the type. The browser knows exactly how to fallback and handle the errors.

You could build a very very robust and large system on @property. The same types of type safety during development that you like with Typescript, but the types actually ship to the browser and are enforced. Rad.

Firefox is almost done with their implementation, which will make @property cross browser stable 🎉

See caniuse for availability status.

Design systems are about to get a lot smarter and more stable.

Mentions #

Join the conversation on

  • Mark Malstrom
  • Rey :ghosthug:
  • Alex Riviere
  • Ryan Mulligan
  • Christian "Schepp" Schaefer
  • caleb
  • Vic Nash
  • nbeerten
  • Nicöd·e
  • Roma Komarov
  • George Francis
  • Alexander Lehner, CPACC
1 pingbacks

It would be the part of the Lighthouse system to have definitions for @prop contrast difference? Colorful letters could always be an unwanted UX path, especially on the road of light/dark theme apps world. What do you think? ????


@argyleink The “Full list on MDN” link is 404. I think you forgot an s at the end of the URL.

edit: Btw, the full, much larger list of CSS types is here:

edit 2: Actually, those are both value types and other grammar productions.

CSS Indexes
Šime VidasŠime Vidas

@simevidas ty for all this ????

1. the link got a fix about 10m ago!
2. added this link! deploying with this link to drafts now ????????
3. mentioned the added link is both grammars and types ????????

rad, hope you're well Sime <3

Adam ArgyleAdam Argyle

@argyleink @property transitioning not enabled in Firefox Nightly? Hopefully they'll solve this soon! Do you know if I need to change a flag or something?

In case someone didn’t get the memo, stop right now and update all of your browsers (yes, Safari, Chrome, Firefox, Brave, Microsoft Edge, anything that can download and process a WebP image). Then come back here… :-)

Now, back to business…

Big news coming from Chrome, via Gilberto Cocchi:
Chrome now allowing video as LCP

Chrome now allowing video as LCP
Note that this could be good for sites with fast loading videos as their LCP, but could also be bad for sites that have slow loading videos…

From the same article that Gilberto linked to above, how Chrome deals with animation within an LCP is also being improved. Good on ya, Chrome!

And then they have to go and do something like this… Oh, Googie… :-(

With all the fuss over TypeScript this past couple of weeks, seems like a good time to chat about typecasting CSS, otherwise known as @property. I was not already familiar with this, but am now, thanks!

Following in that vein, the great Stephanie Eckles explains How Custom Property Values are Computed. Starting with the basics of initial and inherit, runs through how relative units (vw, %, em, currentColor, and such) must be “absolutize[d]”, including custom properties, and how failed or invalid properties and declarations are handled. All something to be aware of as we bury more and more custom properties in our CSS files!

Both Web Bos and Josh Comeau have contributions on this topic as well, respectively:

And speaking of checking whether or not your CSS works, Bramus Van Damme helps us detect if an element can scroll or not, by using a CSS Scroll-Driven Animation… Slick!

And speaking of scroll-driven stuff, the unstoppable Jhey Tompkins sheds a killer scroll-driven animated text demo.

And still speaking of scroll-driven stuff, Shu Ding shared a CodePen showing off cool “up and down arrow” scroll indicators. Then Jhey hopped in with a slight modification. To which Bramus tossed in his own CodePen. Love this kind of riffing!

Addy Osmani points out that Chrome 118 will support @scope, including inspection in DevTools! Bramus was quick to point out his quick intro, and promise a new long post “soon(ish)”…

I have shared a lot of articles and demos related to the View Transitions API, because I think it is an amazing upgrade to the native browsing experience. Well here is one more, this time from Mojtaba Seyedi, which explains what this is, how it works, how to implement it, how to debug it, and even covers some pitfalls and workarounds. Very cool article, but remember, right now this is Chromium-only…

Zoran Jambor breaks down the new multi-keyword option for display. Initially revisiting the basics (block and inline), then expanding into flex and grid, Zoran very nicely segues into how these can be combined to affect the “outer” and “inner” contexts of an element, and finally introduces us to the new flow-root property. Wonderfully thorough, as usual.

Ever wanted to search through your fave browser’s code? Me neither. But if you ever do, Nicole Sullivan tracked down these useful resources:

And finally, Remy Sharp has always been good at finding innovative ways to do something that he wanted to do. And now he has a way to quietly make sure his mom is okay (or at least following her normal patterns). Love it, Remy, thanks.

Happy reading,



Aaron T. GroggAaron T. Grogg

Crawl the CSS Webring?

previous sitenext site
a random site