CSS custom properties are AMAZING. I'm going to attempt to name and roundup all the categories and strategies of custom props that I've come across.
be sure to comment if you know more strategies!
1. tokens #
These custom properties are generally just global values, named objectively, and used atomically.
They create team alignment by naming what are otherwise "magic numbers." In some design systems, they're seldom directly used, instead the "back of the front" team will use them to create house props.
:root {
--size-1: 1rem;
--size-2: 1.25rem;
--blue-1: hsl(200 50% 50%);
--blue-2: hsl(200 50% 60%);
--gray-0: hsl(none none 90%);
--gray-1: hsl(none none 10%);
}
Usage of tokens looks like:
.card {
background: var(--blue-10);
color: var(--blue-1);
padding: var(--size-3);
}
It's very likely these tokens will become values of other custom properties in your CSS because they are generally static and aren't overwritten later.. Think of them as globals constants.
Open Props is a good example of offering just tokens.
2. house props #
House props are named for project reusability. They create team alignment and match jargon. They often point to a global token.
:root {
--brand: var(--blue-1);
--card-padding: var(--size-1);
--surface: var(--gray-10);
@media (prefers-color-scheme: dark) {
--surface: var(--gray-1);
}
}
Usage looks like:
.card {
border-color: var(--brand);
padding: var(--card-padding);
background: var(--surface);
color: var(--text);
}
3. adaptive props #
Aka dynamic house props; these will change, you expect them to change.
:root {
--text: var(--gray-0);
@media (prefers-color-scheme: dark) {
--text: var(--gray-10);
}
}
Now the house prop --text adapts to light and dark. Any usage of it no longer needs to care about writing a media query for those conditions.
Usage of adaptive props looks like:
body {
color: var(--text);
}
I've got a tiny pattern for adaptive props. Create house props with names that make their usage obvious (no magic values), and use the house props to set the values of an adaptive prop.
:root {
/* 2 house props */
--text-dark-mode: var(--gray-0);
--text-light-mode: var(--gray-10);
/* 1 adaptive prop: defaults to light static prop */
--text: var(--text-light-mode);
@media (prefers-color-scheme: dark) {
/* adaptive prop set to dark static prop */
--text: var(--text-dark-mode);
}
}
Here's another example, doesn't use the pattern, but does define adaptive props:
:root {
--adaptive-padding: var(--size-1);
@media (width >= 320px) { --adaptive-padding: var(--size-2) }
@media (width >= 720px) { --adaptive-padding: var(--size-3) }
@media (width >= 1024px) { --adaptive-padding: var(--size-4) }
}
/* Usage */
.card {
padding: var(--adaptive-padding);
}
Lastly, since media queries aren't required to make adaptive props, here's a neat trick by Ahmad Shadeed that bakes the conditions into the math inside the prop. He's got 1 custom property that will adapt to either square corners when full screen, or rounded corners when not.
.card {
border-radius: max(0px, min(8px, calc((100vw - 4px - 100%) * 9999)));
}
Open Props offers these now too, heavily inspired by Ahmad. They look like this in Open Props, which will have a var(--radius-2) sized rounded corners when it's not full width:
/* Open Props sets them up like this */
:root {
--radius-conditional-2: clamp(0px, calc(100vw - 100%) * 1e5, var(--radius-2));
}
/* then authors get to use them like this! */
.card {
border-radius: var(--radius-conditional-2);
}
Open Props offers --surface-{1-4} and --text-{1,2} adaptive props (light & dark) in the normalize.css. A light and dark card can be created with a mix of adaptive props and global tokens, no media query required as it's baked into the props:
.card {
background: var(--surface-2); /* adaptive prop */
color: var(--text-1); /* adaptive prop */
border-radius: var(--radius-conditional-2); /* adaptive prop */
padding: var(--size-3); /* global token */
box-shadow: var(--shadow-2); /* global token */
}
Love it when a custom property disguises all the implentation details away from other authors. Reminds me of NPM and installing modules or importing functions.
Adaptive tokens are often setup by the "back of the front" team, so the "front of the front" folks can just use them.
4. pseudo-private props #
Lea Verou calls these pseudo-private custom properties. I appreciated the throw back to pseudo private properties in Javascript:
this._foo = 'please dont change this'
Then us mimmicking it in CSS:
.card {
--_foo: 10px;
}
Common usage of these pseudo-private props is to make them like static tokens or house props, but scoped instead of global. Aka, you slightly change the beginning of variable name to use --_ and you don't register them on :root.
Vanilla Extract offers scoped custom properties, they use this "pseudo-private" technique in the CSS output.
.button__1qipc2y2 {
--_1qipc2y0: #4263eb;
--_1qipc2y1: #edf2ff;
background-color: var(--_1qipc2y0);
color: var(--_1qipc2y1);
}
.button__1qipc2y2:active {
--_1qipc2y0: #3b5bdb;
--_1qipc2y1: white;
}
These are scoped to a component and signal to the rest of the team the intent to keep them localized to that component. I've seen usage where they are treated as overwritable, and I've seen usage where they are not.
5. partial props #
Prop puzzle pieces. Parts of a full usable prop. Like this, brand color "channels" as partial props:
:root {
--h: 200;
--s: 50%;
--l: 50%;
}
Usage of partial props to make adaptive props looks like:
:root {
--brand: hsl(var(--h) var(--s) var(--l));
--text: hsl(var(--h) 30% 5%);
--surface: hsl(var(--h) 25% 99%);
@media (prefers-color-scheme: dark) {
--brand: hsl(var(--h) calc(var(--s) / 2) calc(var(--l) / 2));
--text: hsl(var(--h) 10% 90%);
--surface: hsl(var(--h) 20% 10%);
}
}
A power of partial props is the ability for them to be used in a calc() like you see in the above example.
6. mixin props #
I first saw this usage of CSS vars on Smashing Magazine by Miriam Suzanne. Great distillation of it in this Codepen.
I think of basic mixin props as a collection of partial props placed on shorthands (background-image, border, border-image, mask-image, etc) . This makes the partial props like function parameters:
* {
--input-1: 1px;
--input-2: var(--blue-5);
--border-mixin: var(--input-1) solid var(--input-2);
}
Usage of basic mixin props looks like:
.card {
border: var(--border-mixin);
}
.card.variant {
--input-2: var(--purple-5);
}
Mia adds a nice hook feature by setting "required" prop parameters to initial. Then, any element that specifies this required prop, triggers the mixin to be valid, applying its effect.
* {
/* define the mixin with a required parameter */
--stripes: linear-gradient(
var(--stripes-angle),
powderblue 20%,
pink 20% 40%,
white 40% 60%,
pink 60% 80%,
powderblue 80%
);
/* reset the required parameter on each element */
--stripes-angle: initial;
/* apply the results everywhere */
/* (will only display when a valid angle is given) */
background-image: var(--stripes);
}
Usage of mixin props looks like:
.stripes {
/* providing a valid angle causes the "mixin" to work */
--stripes-angle: to bottom right;
}
7. swap props #
These props flip n' flop so other props can swap. Hehe, a simple example is a bool prop:
.house-button {
--_hover: 0;
&:hover {
--_hover: 1;
}
}
How can I use this? Ask Jhey, he's got plenty-o-demo's with bool swappin props. Like this one which includes the swap prop --active:
a:is(:hover, :focus) {
--active: 1;
}
:is(svg, .char) {
transform:
rotate(calc((var(--active, 0) * var(--r, 0)) * 1deg))
translate(
calc((var(--active, 0) * var(--x, 0)) * 1%),
calc((var(--active, 0) * var(--y, 0)) * 1%)
);
}
This one only transforms if --active is 1, because otherwise the 0 causes the math to be 0 degrees. Swappin props between 0 and 1, ugh, power.
He goes further in this light/dark theme demo switch, pivoting a color scheme and animations. Rad stuff.
The first place I ever saw this technique was via Ana Tudor's 2018 post on Dry Switching with CSS Variables. This was the first place I ever saw props used as bools to pivot behavior and UI, called it "dry switching".
A couple years later, Jane Ori who's made many games and intense systems out of "space toggles": CSS Sweeper, CSS Conways Game of Life, and more.
/* mixin scope */
.hover-mixin {
--_hover: initial;
&:hover {
--_hover: ; /* space, get it, toggle */
}
}
/* usage */
.button.hover-mixin {
background-color: var(--_hover) green;
/* when hover value is " " it's red */
}
Jane has even got this wild library called CSS Media Vars which enable responsive design with named breakpoints and props, it's very cool. A whole syntax of inline media query adaption from props. It's like Open Props, but for space toggles, it's one import and then a whole lot of new style strategies like.
.card {
width: var(--media-md) 60ch;
background: var(--media-prefers-dark) black;
}
Also, there's a sweet PostCSS plugin from @CSStools called conditional values that gives some syntactic sugar to space toggles:
.fancy-container {
--is-fancy: true;
}
.block {
color: csstools-if(--is-fancy green else red);
}
/* becomes */
:root {
--is-fancy: ;
}
.fancy-container {
--is-fancy: initial;
}
.block {
--is-fancy--0: var(--is-fancy) red;
color: var(--is-fancy--0, green);
}
8. style query props #
Container query props! Could be used as enums for theming, state machines, you name it.
Here's the gist:
button {
@container style(--vibe: primary) {
--_bg: var(--indigo-5);
--_border: var(--indigo-4);
}
@container style(--vibe: rad) {
--_bg: var(--gradient-11);
border: none;
}
@container style(--size: large) {
font-size: var(--font-size-4);
}
}
Try this snippet in Canary on Codepen!
Manuel Matuzović has been writing about it with a post on how style queries work on computed custom property values and how using @property can help with style queries.
9. meta lang props #
Make your own CSS API, in CSS! Mixins are the primary key but other strategies from this post are helpful.
Me using a quick one we'll make:
.button {
--bg: blue;
--text-color: white;
}
Setting up this means setting the actual css property color with your new name for it text.
* {
background-color: var(--bg);
color: var(--text);
}
There, you just created an interface into an interface. These custom properties pass through to the actual CSS property.
So, that was a pretty tame example. Here's one where I'm adding a new property that matches both padding and gaps.
* {
padding: var(--gapadding);
gap: var(--gapadding);
}
.card {
--gapadding: 1rem;
}
Here's a wild example!
.button {
--_type: 3d-primary;
--_accent: var(--neon-purple);
--_accent-hover: var(--neon-pink);
--_3d-depth-level: 2;
--_particles: 20;
}
10. typed props #
@property is another really cool custom property category of usage. These provide type safety and can be interpolated within their type.
@property --focal-size {
syntax: '<percentage>';
initial-value: 100%;
inherits: false;
}
That definition of --focal-size is enough to teach some browsers how to animate a gradient used into a mask. Hold alt/opt inside the codepen below, it'll transition if you are in a browser with @property.
Conclusion #
Between mixins, @property, scoping tricks, and style queries… There still a lot of unexplored territory here and tons of power 😉
Did I miss a category‽


