Course outline

HTML & CSS / Maintainable presentation systems / Lesson 14

Custom properties and adaptable themes

Use custom properties as contextual design values while understanding inheritance, fallback behavior, and theme contrast.

About 4 min readContent reviewed
On this page

Lesson goals

  • Name meaning rather than accidental appearance
  • Scope a theme to a component
  • Make theme behavior respect users

Name meaning rather than accidental appearance

Custom properties provide named values that participate in the cascade and can be referenced with var(). A token such as --surface communicates a role better than --light-gray when the same role may become dark in another theme. Distinguish raw palette values from semantic roles and component-specific adjustments if the system needs those layers. Too many aliases can obscure a simple value, so choose a small documented vocabulary. Custom properties do not create design correctness automatically; the resulting foreground, background, focus ring, and error states still need to work together in every actual composition.

Understand inheritance and substitution

Ordinary double-dash custom properties inherit by default and are resolved in the element’s computed-value context. Their names are case-sensitive. Setting a property on a component can therefore specialize descendants without repeating each final declaration. A var fallback is used when the referenced custom property is unavailable or invalid in the relevant custom-property sense; it is not a general type validator for the destination property. If --tone contains 16px, color: var(--tone, blue) does not select blue merely because 16px is not a color. The resulting declaration can be invalid at computed-value time, with the property’s specified fallback behavior under CSS rules.

Scope a theme to a component

The first example gives the document default surface and text roles, then overrides them within a dark panel. The same component declarations consume those roles in both contexts. No JavaScript is needed to demonstrate the scoped cascade, and the example does not claim to implement a user preference switch or persistence. A real theme chooser needs an accessible control, a clear initial policy, and a strategy for applying the choice without a distracting flash. Test links, form controls, images, selected states, and focus indicators in addition to ordinary paragraphs, since a theme can fail at its edges even when the main panel looks attractive.

html Scoped theme
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scoped theme</title>
<style>
:root { --surface: #fff; --text: #17212b; } .panel { background: var(--surface); color: var(--text); padding: 1rem; margin-block: 1rem; } .dark { --surface: #17212b; --text: #fff; }
</style>
</head>
<body>
<main><h1>Scoped themes</h1><section class="panel"><h2>Default panel</h2><p>Shared component styling.</p></section><section class="panel dark"><h2>Dark panel</h2><p>The same roles have different values.</p></section></main>
</body>
</html>
Expected output
One light panel and one dark panel share a component contract.

Both panels consume the same semantic properties. The dark panel overrides their values locally, and its descendants inherit the resulting text color.

Use fallback values and calculations intentionally

The second example references an optional spacing token with a concrete fallback, then uses calc to derive a related inset. The custom property is absent in one panel and supplied in the other, so the distinction is predictable. A fallback may itself contain a var expression, but deeply nested chains become difficult to audit. Document units and accepted value shapes for public tokens, because --space: 2 and --space: 2rem are not interchangeable in every destination. Circular custom-property references do not produce a useful arithmetic loop; they invalidate the dependency cycle according to the custom-property rules.

html Token fallback
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Token fallback</title>
<style>
.panel { padding: var(--space, 1rem); margin-block: 1rem; border-inline-start: calc(var(--space, 1rem) / 4) solid #174e9a; } .roomy { --space: 2rem; }
</style>
</head>
<body>
<main><h1>Spacing fallback</h1><div class="panel">Fallback spacing.</div><div class="panel roomy">Explicit spacing token.</div></main>
</body>
</html>
Expected output
The roomy panel has twice the declared padding and border thickness.

The first panel uses one rem padding and a quarter-rem border. The second uses two rem and a half-rem border because its token is present.

Make theme behavior respect users

A preferred color scheme can inform an initial presentation, but it does not replace testing or a product’s choice policy. color-scheme can tell the browser which schemes a component supports for native rendering, and must agree with the actual surface. Forced-colors users may rely on system colors, so avoid overriding their choices indiscriminately. Reduced motion, contrast, and typography preferences are separate concerns from dark mode. Keep semantic meaning outside color tokens: a danger token still needs a visible message where an error must be explained. Review inheritance boundaries because a nested widget may unintentionally inherit tokens it cannot safely consume.

Common errors and fixes

A color fallback fails to replace a length token

Why it happens
The token exists but is invalid for color after substitution.
How to fix it
Validate token contracts and provide suitable typed values.
Prevent it next time
Test wrong-type as well as missing-token cases.

A nested component changes unexpectedly

Why it happens
It inherits an undocumented public token.
How to fix it
Scope or rename the token and document its consumers.
Prevent it next time
Review nested compositions during theme changes.

Dark mode hides a focus ring

Why it happens
Only background and body text were reviewed.
How to fix it
Define and test all interactive color roles.
Prevent it next time
Include focused and error states in theme acceptance.

Check your understanding

1. Will color: var(--ink, red) become red when --ink is 12px?

Show worked solution

No. The custom property is present, so its token sequence is substituted. Because a length is not a valid color, the declaration becomes invalid at computed-value time rather than selecting the var fallback. Repair the token’s value contract instead of assuming fallback performs destination-type checking.

2. Design semantic tokens for a notice component across light and dark themes.

Show worked solution

Use roles for notice surface, text, border, and focus where interactive content exists. Give each theme coherent pairs, retain the notice’s visible message, and verify contrast and focus against neighboring surfaces. Theme tokens should change presentation without changing the notice’s meaning or controls.

Key takeaways

  • Custom properties participate in cascade and inheritance.
  • var fallback is not general type validation.
  • Theme every state, not only the background.

Official references