Topic 05

Custom Properties: A Small Design System

CSSHands-on

A site uses the same few colours, spacings and fonts over and over. The accent colour appears on links, on buttons, on the edge of every card. Write that colour as a hex code in forty places and changing it means forty edits, and missing one. Custom properties, CSS's variables, let you give each value a name once and use the name everywhere. Changing the accent colour becomes one edit.

A decorator's paint chart is the picture. Each swatch has a name, such as Accent, and every wall in the job sheet says "paint this Accent" rather than giving a mix. Change the Accent swatch on the chart and every wall painted Accent changes with it. Unlike a real paint chart, the change reaches every wall the moment you save.

Declaring and Using a Custom Property

A custom property is declared like any other property, except that you invent the name and it must start with two dashes. It is used with the var() function, which takes the name and hands back its value. Here is your site's stylesheet, rebuilt around named values. It replaces the body rule from topic 1, so delete that rule, and put this block at the very top of css/style.css.

css/style.css
:root {
  --color-text: #1f2328;
  --color-bg: #ffffff;
  --color-accent: #0b5cad;
  --space-s: 0.5rem;
  --space-m: 1rem;
  --space-l: 2rem;
  --font-body: system-ui, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-mono: Menlo, Consolas, monospace;
}

The selector :root matches the root of the page's tree, which is the html element. Every element on the page sits inside it, so values declared there reach everywhere. The block declares eight names: a near-black for text, white for the background, a blue for the accent, three steps of spacing, and two font stacks, one for ordinary text and one with every letter the same width for code. On its own, this block changes nothing on screen, because nothing uses the names yet.

css/style.css
body {
  color: var(--color-text);
  background-color: var(--color-bg);
  font-family: var(--font-body);
  line-height: 1.5;
}

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

.project-card {
  border-top: 4px solid var(--color-accent);
  padding: var(--space-m);
}

Put these three rules below the :root block. The first gives the whole page its text colour, background and font. The second makes every link the accent blue, which replaces the browser's default link colour with the site's own. The third styles the project cards your Projects page will hold: a thick accent-coloured line along the top edge, and one step of space between the card's edge and its content, a property Chapter 4 explains fully.

Save, and look at the preview. The text is near-black on white in the system font, the lines are more open, and the links are a softer blue than the browser's default. Nothing dramatic, and that is the point: the design now lives in named decisions.

Tokens: A Small Set of Decisions

Named values like these are called design tokens: the small set of decisions a design is built from, each with a name that says what it is for. Notice that the names describe the job, not the value. It is --color-accent, not a name like "blue", because next month the accent might be green, and a name that says blue would then be a lie in forty places.

A small site needs only a handful of tokens. Only decisions that repeat deserve one. A value used once, such as the thickness of that card line, is clearer written directly where it is used.

One set of tokens, used everywhere
Declared on
:root, holding three colours, three spaces and two fonts
Used by
body: var(--color-text), var(--color-bg), var(--font-body)
Used by
a: var(--color-accent)
Used by
.project-card: var(--color-accent), var(--space-m)

Fallbacks and Scope

The var() function accepts a second value after a comma, a fallback, used when the name has no value at all. Writing var(--color-accent, navy) gives navy if the accent was never declared, which also rescues you from a typo in the name. Names are case-sensitive, so a capital letter in the wrong place counts as a different name.

Custom properties are inherited, like text colour. A value declared on :root reaches every element, but you can also declare the same name on any element, and the new value then applies to that element and everything inside it. This is called scope. To see it, paste the card below into the main of projects.html, and then add the extra rule.

projects.html
<article class="project-card">
  <h2><a href="https://example.com/budget">Budget Tracker</a></h2>
  <p>Adds up a month of spending from a spreadsheet.</p>
</article>
Fragment: try it, then remove it
.project-card {
  --color-accent: #1a7f37;
}

With the card alone, the browser shows it with a blue line along its top, a blue title that links to the project, and the description. Add the extra rule and both the line and the card's title link turn green, while the menu links at the top of the page stay blue. The rule changed the accent only inside the card, and every var() inside it picked up the new value. This is exactly how the dark theme in Chapter 5 works: it redeclares the same tokens with dark values, and the whole site follows. Remove the green rule before you go on; the card itself can stay.

Seeing Tokens in DevTools

Inspect a link on your page. In the Styles pane, its rule shows the var() expression, and hovering over it shows the value it resolves to. Now select the html element at the top of the tree: its rule at the top of the Styles pane is your :root block. Click the accent's value, type a different colour, and every link and card border on the page changes at once. It is the fastest way to try out a new colour scheme, and the reason the tokens exist. As always, copy the one you like back into your file.

Common Confusions
  • "Custom properties are the same as Sass variables." Sass, a tool some projects use to generate CSS, has variables that are replaced by fixed values before the browser ever sees them. Custom properties live in the browser and can change while the page is open, and per element, which is what makes theming possible.
  • "Every value should be a variable." Only decisions that repeat. A value used once is clearer written directly where it is used.
  • "The two dashes are optional." Without them, the browser reads the line as an unknown property and ignores it, so nothing is declared and nothing warns you.
Why It Matters
  • A handful of tokens gives a small site a consistent look, and a new colour scheme is a few edits in one block.
  • The dark theme in Chapter 5 and the theme switch in Chapter 9 are built on the tokens from this page.

Knowledge Check

Which line uses a custom property rather than declaring one?

  • A line reading --color-accent: #0b5cad;
  • A line reading --space-m: 1rem;
  • A line reading color: var(--color-accent);
  • A line reading :root { at the top

Why are the site's tokens declared on :root?

  • Custom properties can only be declared there
  • Every element inherits from it, so tokens reach everywhere
  • The browser loads the :root rule before the others
  • Values declared there can never be overridden

A rule sets --color-accent to green on .project-card. Which links turn green?

  • Every link on the page, since the value changed
  • No links at all, only the card's top border
  • Only the menu links in the page header
  • Only the links that sit inside the card

What happens if you write color-accent: #0b5cad; without the two dashes?

  • It works the same, since the dashes are optional
  • The browser guesses you meant a custom property
  • The line is ignored, and nothing gets declared
  • The page shows an error until the dashes are added

You got correct