Skip to main content Accessibility Feedback

An advanced way to use CSS variables

Yesterday, we learned about CSS variables. Today, I wanted to show you an advanced approach to working with them that I often use with client projects.

Let’s dig in!

Globals for system or theme defaults

I like to scope design system or theme defaults to the :root element. This makes them accessible to every element and class in the design system.

:root {

	/* Colors */
	--color-primary: #0088cc;
	--color-secondary: rebeccapurple;
	--color-black: #272727;
	--color-white: #ffffff;

	/* Sizes */
	--size-default: 1rem;
	--size-small: 0.875rem;
	--size-large: 1.25rem;

	/* Typefaces */
	--font-sans: "PT Sans", sans;
	--font-serif: "PT Serif", serif;
	--font-mono: Menlo, Monaco, "Courier New", monospace;

}

I typically have variables for --color-*, --size-*, and --font-*, as well as ones to define the max width of containers and how much --spacing to use between paragraphs and various elements.

Styling elements

Let’s look at styles for a button element.

button {
	background-color: var(--color-primary);
	border: 0.125rem solid var(--color-primary);
	border-radius: 0.25em;
	color: var(--color-white);
	display: inline-block;
	font-size: var(--font-default);
	font-weight: normal;
	line-height: 1.2;
	padding: 0.5rem 0.6875rem;
}

button:hover {
	background-color: var(--color-secondary);
	border-color: var(--color-secondary);
}

Let’s say we want to add a secondary button style: the .btn-secondary class.

<button>Primary Button</button>
<button class="btn-secondary">Secondary Button</button>

Using only globals, we might write the CSS like this.

.btn-secondary {
	background-color: var(--color-secondary);
	border-color: var(--color-secondary);
}

.btn-secondary:hover {
	background-color: var(--color-primary);
	border-color: var(--color-primary);
}

It totally works, and we can easily update our global colors later and have them automatically update the button styles.

But there’s another way we could approach this that I think works a little bit better.

CSS variables scoped to the element

While global variables scoped to the :root let me define system-wide defaults, I also like to scope variables for styles that change with utility classes to the element itself.

CSS variables scoped to an element can use other CSS variables as their value. But scoping them to the element provides an easy way to modify them.

Looking at our button again, I’ll often do something like this…

button {
	--bg-color: var(--color-primary);
	--color: var(--color-white);
	--size: var(--font-default);
	--padding-x: 0.6875rem;
	--padding-y: 0.5rem;

	background-color: var(--bg-color);
	border: 0.125rem solid var(--bg-color);
	border-radius: 0.25em;
	color: var(--color);
	display: inline-block;
	font-size: var(--size);
	font-weight: normal;
	line-height: 1.2;
	padding: var(--padding-y) var(--padding-x);
}

Now, to change the button:hover style, I only need to update the --bg-color variable, which controls both the background-color and border-color properties.

button:hover {
	--bg-color: var(--color-secondary);
}

Here’s a demo.

A growing system

This approach is a little bit more work up-front, but it has bigger payoffs the more you use it.

For example, our .btn-secondary class gets shorter.

.btn-secondary {
	--bg-color: var(--color-secondary);
}

.btn-secondary:hover {
	--bg-color: var(--color-primary);
}

With every utility class you add to modify your base styles, using element-scoped CSS variables makes things a bit easier.

For example, we can add .btn-large and .btn-small classes by doing this…

.btn-large {
	--size: var(--font-large);
	--padding-x: 0.875rem;
	--padding-y: 0.75rem;
}

.btn-small {
	--size: var(--font-small);
	--padding-x: 0.5rem;
	--padding-y: 0.25rem;
}

Here’s another demo.

Should you always do this?

Nope! It’s a lot more work, and sometimes results in code that’s a bit less readable at first glance.

It’s a good approach to use…

  • For properties that will change through modifier or utility classes.
  • For design systems where end-users will need to easily override certain styles in ways you can’t predict.