For the Nucleus team, our aim has always been to work off the back of what the browser provides. We were excited to use CSS custom properties in our Design System. But we soon realised that we needed to be careful with how we used them with our web components.

What is a CSS custom property?

A CSS custom property is a variable that can be used in CSS. They are defined using the -- prefix, and can be used in any CSS property that accepts a value. For example, --colour-primary can be used in color: var(--colour-primary).

Why use them?

CSS custom properties have been around for a while, and are supported in all modern browsers. They are a great way to define a value that can be used in multiple places, and can be changed in one place to update all the places it is used.

For example, if you wanted to change the primary colour of your website, you could change the value of --colour-primary and all the places it is used would be updated. This makes them quite popular to use in Design Systems and can be used in conjunction with Design Tokens.

If you have not used CSS custom properties before, I would recommend reading this CSS-Tricks article to get a better understanding of them.

Two ways to use them

There are two main ways to use CSS custom properties. The first is to define them in the :root, and the second is to define them in an element. Let's look at an example of each.

Defining them in the root of the document

This is the most common way to use CSS custom properties, especially in a Design System. They are defined in the :root, and can be used anywhere in the document. For example, if you wanted to define a primary colour, you could define it like this:

:root {
  --colour-primary: #ff0000;
}

To then use on a heading, you could do this:

h1 {
  color: var(--colour-primary);
}

Defining them in an element

Scoping CSS custom properties to an element means that they can only be used in that element, and any child elements. This is useful if you want to have the property change due to some logic, like a media query. For example, if you wanted to define a primary colour for a button, you could define it like this:

button {
  --colour-primary: #ff0000;
}

Instead of updating the color property, the custom CSS property can be updated.

button span {
  color: var(--colour-primary);
}

@media (min-width: 768px) {
  button {
    --colour-primary: #0000ff;
  }
}

Where the trouble begins

To understand this, let's use the Design System's official web component button called <ds-button>. We make the background colour of the <ds-button> utilise the global CSS custom property --colour-primary defined in the :root.

See the Pen ds-button by Nucleus (@nucleus-ds) on CodePen.

<ds-button> is styled using the brand primary colour. There are currently no problems. All the teams across the business are using <ds-button> component in their journeys, and it is working as expected.

Later that year, a product team is asked to make a change to their journey to celebrate "Sustainability day". This means making the journey more green in colour. They make changes to their imagery and use components that have green colours in them. Even with these changes, the <ds-button> is still the same colour as the brand and makes their journey look out of place and not green enough.

After some investigation, they decide to change the primary colour of the button in their journey by adding CSS to redefine the value of --colour-primary to #00ff00 on <ds-button> and the button now looks green, and only in their journey.

They do this by defining the new value on the <ds-button> element which has a higher specificity than :root.

ds-button {
  --colour-primary: #00ff00;
}

See the Pen ds-button green by Nucleus (@nucleus-ds) on CodePen.

Because of this they did not need to bother the Design System team to make the change. The business congratulated them on making the company stand out for the companies values. Everyone is happy.

The problem

After the Sustainability day, the team need to revert the changes back to the original brand colours. So they changed the value of --colour-primary back to #ff0000 and the <ds-button> now looks red again.

ds-button {
  --colour-primary: #ff0000;
}

Unfortunately, because they have now hard coded the value of --colour-primary in the button component, it will always be red. Even if the Design System team changes the value of --colour-primary to something else, the button will still be red. The Design System team will be unaware that the team have done this.

You might be asking, why did the team hard code the value of --colour-primary? The answer is simple, they did not know they had done it. When making these changes, they were only concerned with making it back to the brand colours. All tests passed, and the journey looked good. This is an easy mistake to make, and one that is hard to spot.

Down the line, the Brand team comes to the Design System team and tell them they are updating the brand colours. The Design System team will update the value of --colour-primary to the new brand colour.

Big grins all around as it was only a few lines of code to change the brand colours. Unfortunately, when visiting the website, they will be disappointed to see that the button is still red in that journey. The teams that made the change will have to be asked to remove the --colour-primary in the button component.

Why is this a problem?

Most Design Systems will have more than one Design Token, which means that teams have access to potentially hundreds of CSS custom properties.

If a team hard codes the value of some of these properties in a component, it will be very difficult to find all the places it is used. When the Design System team wants to make a change to the design of the components they will have to find where these components have been used and see what has been hard coded. This will be a very time consuming task and reduces the benefit of having a Design System.

If your Design System is there to provide the brand to other teams building journeys, then exposing tokens as CSS custom properties is a bad idea. It allows them to make changes that can affect the brand.

Accessibility

A lot of time and considerations are made to make spacing, colours and typography accessible. If a team hard codes the value of a CSS custom property in a component, they will be overriding the Design System and potentially making the component inaccessible.

For the Nucleus team, we have a lot of automated and manual tests to make sure our component are as accessible as we can make them. Exposing these CSS custom properties means we could no longer guarantee a level of accessibility of our components.

No new learnings

For the Product teams to bypass the Design System team on design decisions, it means that no new learnings are shared across the design community. This means that the Design System team will not be able to learn from the Product teams and improve the Design System. It also means that the Product teams will not be able to learn from the Design System to improve their design decisions.

In the example above, other teams might have wanted to celebrate Sustainability day and the Brand team could have made decisions to make a green alternatives that aligns to the business values.

A Design System is there to provide a consistent experience across the business. If teams are bypassing the Design System team, then the Design System is not doing its job.

Breaking the component

With the use of a Shadow DOM a component can change its internal structure on an almost infinite number of ways. These are not normally shared with the teams as it is the inner workings of the component. By hard coding the value of a CSS custom property, it may break a scenario that was not seen in testing.

Does this mean we should not use CSS custom properties?

No. CSS custom properties are a very powerful tool and can allow for a no build way of creating reusable CSS. The problem is emitting CSS custom properties in a component that becomes part of the component specification.

Understand if this is something that you want teams to have control over. There are always exceptions to the rule, but if you want to make sure that the Design System's components encapsulate the design decisions, then you should not emit CSS custom properties from a component.

As component curators and maintainers, we have the responsibility to not just understand what our system is there to do, but what it could do.

We need to make sure that these two align as much as possible, so that we do not create something that is unmanageable. Making everything available to teams to change can mean that in the future, we will not be able to make changes to the Design System as there will be too many breaking changes.

Alternatives

Using a CSS preprocessor like Sass or Less. This will allow you to use variables in your CSS. This will mean that the Design System team can control the values of these variables and teams will not be able to change them. These can still be utilised by Design Tokens, such as using Style Dictionary to generate Sass variables.

Scoping CSS custom properties to an element within in a component (in the :host) means we can utilise CSS for specific design logic and not use JavaScript. However, it will still be controlled by the Design System team and not emitting CSS custom properties that can be changed by Product teams.

Conclusion

Make sure you know how your teams are using your Design System. If you are emitting CSS custom properties, make sure you know how they are being used. If you are not emitting CSS custom properties, know that you are making a conscious decision to not allow teams to change the design decisions.