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.
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.
<!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>
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.
<!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>
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.