RSS FeedTwitterMastodonBlueskyShare IconHeart IconGithub IconArrow IconClock IconGUI Challenges IconHome IconNote IconBlog IconCSS IconJS IconHTML IconShows IconGit IconSpeaking IconTools IconShuffle IconNext IconPrevious IconCalendar IconCalendar Edit 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.7 min read

Type safe CSS design systems with @property

css

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 csswg.org/indexes/#types.

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

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

@argyleink
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: https://drafts.csswg.org/indexes/#types

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?

bigandybigandy

Crawl the CSS Webring