Custom CSS Properties
In addition to the many properties that are included in CSS (color
, font-size
, etc), we can create custom properties to use as variables. Here’s an example:
:root {
--link-color: #516cff;
}
a {
color: var(--link-color);
}
They are highly useful and flexible, if a bit verbose. Let’s dive in.
Basic Rules
For the most part, custom properties behave like normal ones. Here are their basic rules:
- Custom property names must always begin with a
--
, otherwise, they will be ignored - All of the same cascade, inheritance, and specificity rules of setting and updating regular properties apply to custom ones
- They can be assigned to any type of CSS value or function, or to another custom property
- They can only be referenced using
var()
function - To reference them in a declaration, they must be in scope. If they aren’t, that declaration will be ignored
Scoping Properties
The selector that you define a custom property in determines its scope. Let’s take another look at the example above:
:root {
--link-color: #516cff;
}
a {
color: var(--link-color);
}
Here, the property --link-color
is defined in the :root
selector. Simply put, :root
is a shortcut to the topmost scope of a CSS context (more on MDN). Defining custom properties here makes them accessible from anywhere in the stylesheet.
Doing so is a safe default choice. Defining them in a more specific scope is perfectly acceptable as well:
.thumbnail {
--thumbnail-width: 64px;
width: var(--thumbnail-width);
}
In this case, the current value of the --thumbnail-width
property at 64px
will only be accessible to elements with the .thumbnail
class, and their children. That may be useful if you have multiple thumbnails throughout your site at different sizes, or if you want to overwrite the value of a property that was defined in a parent scope.
Ultimately, where you define a custom property should depend on context.
Context-Based Variables
One of the benefits of defining custom properties at the :root
is the ability to use them in any context, and update them as context changes.
This approach allows a property to retain the same semantic meaning throughout your design system, but change its value situationally. For example, we may set a width for grid gutters globally:
:root {
--gutter: 20px;
}
.grid-item {
padding-left: var(--gutter);
padding-right: var(--gutter);
}
<div class="grid">
<div class="grid-item" />
<div class="grid-item" />
</div>
…and update it in subgrids, whose children use the same styles:
.subgrid {
--gutter: 10px;
}
<div class="grid">
<div class="grid-item" />
<div class="subgrid">
<div class="grid-item" />
<div class="grid-item" />
</div>
</div>
With this method, we’re able to take a semantic approach to styling by creating custom properties to use as variables in class styles, and updating those variables as context changes, leaving the original class styles alone.
We try to employ the same methodology in SCSS by scaffolding variables into separate assignments for values and application.
Real-Time Updates
Unlike pre-processed variables, custom properties are evaluated by the browser in real-time. That gives two benefits:
- They can have multiple values at once across contexts, thanks to scope
- They can be updated as the site responds to user interaction, media changes, etc.
A use case that illustrates this, and where custom properties really shine, is for light/dark modes. Consider a setup like so:
:root {
--c-black: #000000;
--c-white: #ffffff;
--c-fg: var(--c-black);
--c-bg: var(--c-white);
}
html {
color: var(--c-fg);
background-color: var(--c-bg);
}
.dark-mode {
--c-fg: var(--c-white);
--c-bg: var(--c-black);
}
In this case, toggling the dark-mode
class on any element will change it to dark mode. Doing so is simple enough with JavaScript, but let’s go a step further and imagine a pure CSS implementation of a dark mode switch:
:root {
--c-black: #000000;
--c-white: #ffffff;
--c-fg: var(--c-black);
--c-bg: var(--c-white);
}
main {
color: var(--c-fg);
background-color: var(--c-bg);
}
#darkModeToggle:checked + main {
--c-fg: var(--c-white);
--c-bg: var(--c-black);
}
<html>
<body>
<input type="checkbox" id="darkModeToggle" />
<main>
All site content will go here
</main>
</body>
</html>
In this example, everything we discussed comes together:
- Our
--c-black
and--c-white
variable values are scaffolded into our--c-fg
and--c-bg
(foreground and background) variable applications - We only set the
color
andbackground-color
properties once - We update the foreground and background colors based on context, leveraging scope
- Our styles change in response to user interaction
For more on what you can do with custom properties, check out these examples: