Patterns for Practical CSS Custom Properties Use

Avatar of Tyler Childs
Tyler Childs on

I’ve been playing around with CSS Custom Properties to discover their power since browser support is finally at a place where we can use them in our production code. I’ve been using them in a number different ways and I’d love for you to get as excited about them as I am. They are so useful and powerful!

I find that CSS variables usage tends to fall into categories. Of course, you’re free to use CSS variables however you like, but thinking of them in these different categories might help you understand the different ways in which they can be used.

  • Variables. The basics, such as setting, a brand color to use wherever needed.
  • Default Values. For example, a default border-radius that can be overridden later.
  • Cascading Values. Using clues based on specificity, such as user preferences.
  • Scoped Rulesets. Intentional variations on individual elements, like links and buttons.
  • Mixins. Rulesets intended to bring their values to a new context.
  • Inline Properties. Values passed in from inline styles in our HTML.

The examples we’ll look at are simplified and condensed patterns from a CSS framework I created and maintain called Cutestrap.

A quick note on browser support

There are two common lines of questions I hear when Custom Properties come up. The first is about browser support. What browsers support them? Are there fallbacks we need to use where they aren’t supported?

The global market share that support the things we’re covering in this post is 85%. Still, it’s worth cross-referencing caniuse) with your user base to determine how much and where progressive enhancement makes sense for your project.

The second question is always about how to use Custom Properties. So let’s dive into usage!

Pattern 1: Variables

The first thing we’ll tackle is setting a variable for a brand color as a Custom Property and using it on an SVG element. We’ll also use a fallback to cover users on trailing browsers.

html {
  --brand-color: hsl(230, 80%, 60%);
}

.logo {
  fill: pink; /* fallback */
  fill: var(--brand-color);
}

Here, we’ve declared a variable called --brand-color in our html ruleset. The variable is defined on an element that’s always present, so it will cascade to every element where it’s used. Long story short, we can use that variable in our .logo ruleset.

We declared a pink fallback value for trailing browsers. In the second fill declaration, we pass --brand-color into the var() function, which will return the value we set for that Custom Property.

That’s pretty much how the pattern goes: define the variable (--variable-name) and then use it on an element (var(--variable-name)).

See the Pen
Patterns for Practical Custom Properties: Example 1.0
by Tyler Childs (@tylerchilds)
on CodePen.

Pattern 2: Default values

The var() function we used in the first example can also provide default values in case the Custom Property it is trying to access is not set.

For example, say we give buttons a rounded border. We can create a variable — we’ll call it --roundness — but we won’t define it like we did before. Instead, we’ll assign a default value when putting the variable to use.

.button {
  /* --roundness: 2px; */
  border-radius: var(--roundness, 10px);
}

A use case for default values without defining the Custom Property is when your project is still in design but your feature is due today. This make it a lot easier to update the value later if the design changes.

So, you give your button a nice default, meet your deadline and when --roundness is finally set as a global Custom Property, your button will get that update for free without needing to come back to it.

See the Pen
Patterns for Practical Custom Properties: Example 2.0
by Tyler Childs (@tylerchilds)
on CodePen.

You can edit on CodePen and uncomment the code above to see what the button will look like when --roundness is set!

Pattern 3: Cascading values

Now that we’ve got the basics under our belt, let’s start building the future we owe ourselves. I really miss the personality that AIM and MySpace had by letting users express themselves with custom text and background colors on profiles.

Let’s bring that back and build a school message board where each student can set their own font, background color and text color for the messages they post.

User-based themes

What we’re basically doing is letting students create a custom theme. We’ll set the theme configurations inside data-attribute rulesets so that any descendants — a .message element in this case — that consume the themes will have access to those Custom Properties.

.message {
  background-color: var(--student-background, #fff);
  color: var(--student-color, #000);
  font-family: var(--student-font, "Times New Roman", serif);
  margin-bottom: 10px;
  padding: 10px;
}

[data-student-theme="rachel"] {
  --student-background: rgb(43, 25, 61);
  --student-color: rgb(252, 249, 249);
  --student-font: Arial, sans-serif;
}

[data-student-theme="jen"] {
  --student-background: #d55349;
  --student-color: #000;
  --student-font: Avenir, Helvetica, sans-serif;
}

[data-student-theme="tyler"] {
  --student-background: blue;
  --student-color: yellow;
  --student-font: "Comic Sans MS", "Comic Sans", cursive;
}

Here’s the markup:

<section>
  <div data-student-theme="chris">
    <p class="message">Chris: I've spoken at events and given workshops all over the world at conferences.</p>
  </div>
  <div data-student-theme="rachel">
    <p class="message">Rachel: I prefer email over other forms of communication.</p>
  </div>
  <div data-student-theme="jen">
    <p class="message">Jen: This is why I immediately set up my new team with Slack for real-time chat.</p>
  </div>
  <div data-student-theme="tyler">
    <p class="message">Tyler: I miss AIM and MySpace, but this message board is okay.</p>
  </div>
</section>

We have set all of our student themes using [data-student-theme] selectors for our student theme rulesets. The Custom Properties for background, color, and font will apply to our .message ruleset if they are set for that student because .message is a descendant of the div containing the data-attribute that, in turn, contains the Custom Property values to consume. Otherwise, the default values we provided will be used instead.

See the Pen
Patterns for Practical Custom Properties: Example 3.0
by Tyler Childs (@tylerchilds)
on CodePen.

Readable theme override

As fun and cool as it is for users to control custom styles, what users pick won’t always be accessible with considerations for contrast, color vision deficiency, or anyone that prefers their eyes to not bleed when reading. Remember the GeoCities days?

Let’s add a class that provides a cleaner look and feel and set it on the parent element (<section>) so it overrides any student theme when it’s present.

.readable-theme [data-student-theme] {
  --student-background: hsl(50, 50%, 90%);
  --student-color: hsl(200, 50%, 10%);
  --student-font: Verdana, Geneva, sans-serif;
}
<section class="readable-theme">
  ...
</section>

We’re utilizing the cascade to override the student themes by setting a higher specificity such that the background, color, and font will be in scope and will apply to every .message ruleset.

See the Pen
Patterns for Practical Custom Properties: Example 3.1
by Tyler Childs (@tylerchilds)
on CodePen.

Pattern 4: Scoped rulesets

Speaking of scope, we can scope Custom Properties and use them to streamline what is otherwise boilerplate CSS. For example, we can define variables for different link states.

a {
  --link: hsl(230, 60%, 50%);
  --link-visited: hsl(290, 60%, 50%);
  --link-hover: hsl(230, 80%, 60%);
  --link-active: hsl(350, 60%, 50%);
}

a:link {
  color: var(--link);
}

a:visited {
  color: var(--link-visited);
}

a:hover {
  color: var(--link-hover);
}

a:active {
  color: var(--link-active);
}
<a href="#">Link Example</a>

Now that we’ve written out the Custom Properties globally on the <a> element and used them on our link states, we don’t need to write them again. These are scoped to our <a> element’s ruleset so they are only set on anchor tags and their children. This allows us to not pollute the global namespace.

Example: Grayscale link

Going forward, we can control the links we just created by changing the Custom Properties for our different use cases. For example, let’s create a gray-colored link.

.grayscale {
  --link: LightSlateGrey;
  --link-visited: Silver;
  --link-hover: DimGray;
  --link-active: LightSteelBlue;
}
<a href="#" class="grayscale">Link Example</a>

We’ve declared a .grayscale ruleset that contains the colors for our different link states. Since the selector for this ruleset has a greater specificity then the default, these variable values are used and then applied to the pseudo-class rulesets for our link states instead of what was defined on the <a> element.

See the Pen
Patterns for Practical Custom Properties: Example 4.0
by Tyler Childs (@tylerchilds)
on CodePen.

Example: Custom links

If setting four Custom Properties feels like too much work, what if we set a single hue instead? That could make things a lot easier to manage.

.custom-link {
  --hue: 30;
  --link: hsl(var(--hue), 60%, 50%);
  --link-visited: hsl(calc(var(--hue) + 60), 60%, 50%);
  --link-hover: hsl(var(--hue), 80%, 60%);
  --link-active: hsl(calc(var(--hue) + 120), 60%, 50%);
}

.danger {
  --hue: 350;
}
<a href="#" class="custom-link">Link Example</a>
<a href="#" class="custom-link danger">Link Example</a>

See the Pen
Patterns for Practical Custom Properties: Example 4.1
by Tyler Childs (@tylerchilds)
on CodePen.

By introducing a variable for a hue value and applying it to our HSL color values in the other variables, we merely have to change that one value to update all four link states.

Calculations are powerful in combination with Custom Properties since they let
your styles be more expressive with less effort. Check out this technique by Josh Bader where he uses a similar approach to enforce accessible color contrasts on buttons.

Pattern 5: Mixins

A mixin, in regards to Custom Properties, is a function declared as a Custom Property value. The arguments for the mixin are other Custom Properties that will recalculate the mixin when they’re changed which, in turn, will update styles.

The custom link example we just looked at is actually a mixin. We can set the value for --hue and then each of the four link states will recalculate accordingly.

Example: Baseline grid foundation

Let’s learn more about mixins by creating a baseline grid to help with vertical rhythm. This way, our content has a pleasant cadence by utilizing consistent spacing.

.baseline,
.baseline * {
  --rhythm: 2rem;
  --line-height: var(--sub-rhythm, var(--rhythm));
  --line-height-ratio: 1.4;
  --font-size: calc(var(--line-height) / var(--line-height-ratio));
}

.baseline {
  font-size: var(--font-size);
  line-height: var(--line-height);
}

We’ve applied the ruleset for our baseline grid to a .baseline class and any of its descendants.

  • --rhythm: This is the foundation of our baseline. Updating it will impact all the other properties.
  • --line-height: This is set to --rhythm by default, since --sub-rhythm is not set here.
  • --sub-rhythm: This allows us to override the --line-height — and subsequently, the --font-size — while maintaining the overall baseline grid.
  • --line-height-ratio: This helps enforce a nice amount of spacing between lines of text.
  • --font-size: This is calculated by dividing our --line-height by our --line-height-ratio.

We also set our font-size and line-height in our .baseline ruleset to use the --font-size and --line-height from our baseline grid. In short, whenever the rhythm changes, the line height and font size change accordingly while maintaining a legible experience.

OK, let’s put the baseline to use.

Let’s create a tiny webpage. We’ll use our --rhythm Custom Property for all of the spacing between elements.

.baseline h2,
.baseline p,
.baseline ul {
  padding: 0 var(--rhythm);
  margin: 0 0 var(--rhythm);
}

.baseline p {
  --line-height-ratio: 1.2;
}

.baseline h2 {
  --sub-rhythm: calc(3 * var(--rhythm));
  --line-height-ratio: 1;
}

.baseline p,
.baseline h2 {
  font-size: var(--font-size);
  line-height: var(--line-height);
}

.baseline ul {
  margin-left: var(--rhythm);
}
<section class="baseline">
  <h2>A Tiny Webpage</h2>
  <p>This is the tiniest webpage. It has three noteworthy features:</p>
  <ul>
    <li>Tiny</li>
    <li>Exemplary</li>
    <li>Identifies as Hufflepuff</li>
  </ul>
</section>

We’re essentially using two mixins here: --line-height and --font-size. We need to set the properties font-size and line-height to their Custom Property counterparts in order to set the heading and paragraph. The mixins have been recalculated in those rulesets, but they need to be set before the updated styling will be applied to them.

See the Pen
Patterns for Practical Custom Properties: Example 5.0
by Tyler Childs (@tylerchilds)
on CodePen.

Something to keep in mind: You probably do not want to use the Custom Property values in the ruleset itself when applying mixins using a wildcard selector. It gives those styles a higher specificity than any other inheritance that comes along with the cascade, making them hard to override without using !important.

Pattern 6: Inline properties

We can also declare Custom Properties inline. Let’s build a lightweight grid system demonstrate.

.grid {
  --columns: auto-fit;

  display: grid;
  gap: 10px;
  grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
}
<div class="grid">
  <img src="https://www.fillmurray.com/900/600" alt="Bill Murray" />
  <img src="https://www.placecage.com/900/600" alt="Nic Cage" />
  <img src="https://www.placecage.com/g/900/600" alt="Nic Cage gray" />
  <img src="https://www.fillmurray.com/g/900/600" alt="Bill Murray gray" />
  <img src="https://www.placecage.com/c/900/600" alt="Nic Cage crazy" />
  <img src="https://www.placecage.com/gif/900/600" alt="Nic Cage gif" />
</div>

By default, the grid has equally sized columns that will automatically lay themselves into a single row.

See the Pen
Patterns for Practical Custom Properties: Example 6.0
by Tyler Childs (@tylerchilds)
on CodePen.

To control the number of columns we can set our --columns Custom Property
inline on our grid element.

<div class="grid" style="--columns: 3;">
  ...
</div>

See the Pen
Patterns for Practical Custom Properties: Example 6.1
by Tyler Childs (@tylerchilds)
on CodePen.


We just looked at six different use cases for Custom Properties — at least ones that I commonly use. Even if you were already aware of and have been using Custom Properties, hopefully seeing them used these ways gives you a better idea of when and where to use them effectively.

Are there different types of patterns you use with Custom Properties? Share them in the comments and link up some demos — I’d love to see them!

If you’re new to Custom Properties are are looking to level up, try playing around with the examples we covered here, but add media queries to the mix. You’ll see how adaptive these can be and how many interesting opportunities open up when you have the power to change values on the fly.

Plus, there are a ton of other great resources right here on CSS-Tricks to up your Custom Properties game in the Custom Properties Guide.

See the Pen
Thank you for Reading!
by Tyler Childs (@tylerchilds)
on CodePen.