RSS FeedTwitterMastodonShare IconHeart IconGithub IconArrow IconClock IconGUI Challenges IconHome IconNote IconBlog IconCSS IconJS IconHTML IconMedia IconGit IconSpeaking Icon
Three custom properties are repeated in a grid and 3D tilted a bit. They are var(--party) var(--power) and var(--mix).
A series of images of an avatar doing a bunch of skateboard tricks.

10 powerful ways to use CSS variables

21 min read

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 are used atomically.

They create team alignment by naming what are otherwise "magic numbers." They often follow naming conventions.

: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(--surface-1);
  color: var(--text-1);
  padding: var(--card-padding);

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

OpenProps === tokens

2. house props #

House props are named for project reusability. They also create team alignment, jargon, and can be adaptive (see #3).

:root {
  --brand: var(--blue-1);
  --card-padding: var(--size-1);
  --surface: #eee;

  @media (prefers-color-scheme: dark) {
    --surface: #111;  

Usage of house props looks like:

.card {
  border-color: var(--brand);
  padding: var(--card-padding);
  background: var(--surface);

Open Props offers --surface-{1-4} and --text-{1,2} house props (which adapt to light & dark). They come with use of normalize.css. A light and dark card can be created with a mix of house props and tokens, no media query required as it's baked into the prop:

.card {
  background: var(--surface-2); /* surface-1 is the page bg */
  color: var(--text-1);
  border-radius: var(--radius-2);
  padding: var(--size-3);
  box-shadow: var(--shadow-2);

3. adaptive props #

Aka dynamic house props; these will change, you expect them to change. They need setup though, orchestrated and empowered with CSS conditionals.

:root {
  --text: var(--gray-0);

@media (prefers-color-scheme: dark) {
  :root {
    --text: var(--gray-10);

Usage of adaptive props looks like:

body {
  color: var(--text);

I've got a tinyyyy bit more verbose pattern for adaptive props; create static house props with names that make their usage obvious (no magic values).

:root {
  /* 2 static 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; adaptive sizing

:root {
  --adaptive-padding: .5rem;

  @media (width >= 320px)  { --adaptive-padding: 1rem }
  @media (width >= 720px)  { --adaptive-padding: 1.25rem }
  @media (width >= 1024px) { --adaptive-padding: 1.5rem }

Lastly, since media queries aren't required to make this 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);

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.

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 {
  --_radius: 10px;

Which makes them like static tokens or house props, but scoped instead of global.

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;

A tweet about this.

5. partial props #

Prop puzzle pieces. Parts of a full usable prop. Here, 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) {
  :root {
    --brand: hsl(var(--h) calc(var(--s) / 2) calc(var(--l) / 2));
    --text: hsl(var(--h) 10% 90%);
    --surface: hsl(var(--h) 20% 10%);

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 params into a greater "mixin" prop, function thing.

* {
  --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(
    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;

Snippet source

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) {
    rotate(calc((var(--active, 0) * var(--r, 0)) * 1deg))
      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.

And you can't miss Ana Tudor's post on Dry Switching with CSS Variables. This was the first place I ever saw props used as bools to pivot behavior and UI.

There's also Jane Ori who's made many games and intense systems out of Dry Switching / swappy props: CSS Sweeper, CSS Conways Game of Life, and more. They've even got this wild library called CSS Media Vars which enable responsive design with named breakpoints and props, it's very cool.

Also, there's a sweet PostCSS plugin from @CSStools called conditional values that gives some syntactic sugar to this:

.fancy-container {
    --is-fancy: true;

.block {
    color: csstools-if(--is-fancy pink else red);

/* becomes */
:root {
    --is-fancy:  ;

.fancy-container {
    --is-fancy: initial;

.block {
    --is-fancy--0: var(--is-fancy) red;
    color: var(--is-fancy--0,pink);

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! It's not real, but I'm pretty sure this could all be setup.

.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 assist browsers in knowing your animation intents.

@property --focal-size {
  syntax: '<length-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‽

Mentions #

Join the conversation on

  • Franko
  • Filmy Sperm
  • romainmenke
  • Chrome Developers
  • Dohány Tamás
  • Thomas Broyer
  • Crown 🌟🐳
  • Caleb (he/him) #BLM
  • CSS Tools
  • dave cOllison
  • Scott Kellum :typetura:
  • jfroehlich
  • Max Böck
  • Thomas Broyer
  • 🔥.codes
  • Fabrizio Calderan
  • B L I И K
  • Kyle Stoneman
  • bezier
  • Una 🇺🇦
  • Dmitry N. Medvedev 🤍💙🤍
  • Miguel Marques
  • Michael Klöpzig
  • Yannick Schall
  • Cyphr Sylph
  • Etienne🐿 van Delden-de la Haije
  • Michael Klöpzig
  • Brecht
  • Marcel dos Santos
  • Basit Bashir
  • Damian Wajer
  • Benny Powers 🇨🇦️🇮🇱️
  • Antti Mattila
  • Adam
  • Polemizante
  • oxomichael
  • Sieg
  • Chanfi Attoumani
  • lahincapie
  • alex
  • Kais
  • Jason Lengstorf ⚡️
  • John Miller
  • 全部入りHTML太郎
  • Віталій Бобров 🇺🇦💗🛡️⚔️
  • LetMeLiveFree
  • ziumek🖤
  • Alice Li
  • Marlon Alvarez
  • Y Nathan M
  • 比喩
  • Casey Robinson👨🏻‍💻
  • Thilo Maier

11. Door props to prop open your door

Baruch KatzBaruch Katz