Keep shared design tokens in page CSS, expose component-specific variables as the styling API, and connect them with a small page-owned mapping. This tutorial builds a local Mac demo that keeps those responsibilities separate.
What belongs to the page, and what belongs to the component?
For someone editing an authored webpage on a Mac, the concrete task is to theme a reusable component without making its implementation depend on the page’s token names. The example below uses a synthetic orbit-tile component containing an abstract shape and a short label.
A design token and a public component variable can both be CSS custom properties. Their distinction comes from the contract you document. Ordinary unregistered custom properties participate in the cascade and inherit; declaring them on :root makes them available through inheritance across the document. There is no special CSS keyword that makes a property a design token. Using CSS custom properties (variables)
Use this ownership table to decide where a declaration belongs. The names are illustrative and local to this example.
| Layer | Example | Responsibility |
|---|---|---|
| Page design tokens | --scene-accent, --scene-rounding |
Shared visual choices for the authored page |
| Page integration rules | A rule targeting orbit-tile |
Translate page choices into the component’s API |
| Public component variables | --orbit-tile-fill, --orbit-tile-radius |
Documented controls available to consumers |
| Component implementation | .shape, layout rules, literal defaults |
Render the component without requiring page tokens |
The useful test is portability: could another page supply different token names while using the same component unchanged? With a page-owned mapping, it could. The new page would map its own tokens to the same public component variables.
How does the styling contract cross Shadow DOM?
Shadow DOM separates selector matching from inheritance. A page selector cannot ordinarily select an internal shape through the shadow boundary. Inherited property values can still reach it through the host. The CSS Shadow specification describes the flattened element tree used for this relationship; separate selector trees do not eliminate inheritance. CSS Shadow Module Level 1
Keep the public variables unset inside the component and supply defaults where they are consumed. For example, background-color: var(--orbit-tile-fill, #667085) accepts a consumer’s inherited value while providing a standalone default. Declaring --orbit-tile-fill: #667085 on :host instead would give the host its own value and block a value inherited from an ancestor. A consumer could still target the host directly. Styling Web Components
For this example, document just two public controls:
| Public variable | Accepted value for this example | Default when absent | Effect |
|---|---|---|---|
--orbit-tile-fill |
A valid CSS color | #667085 |
Shape background color |
--orbit-tile-radius |
One nonnegative length or percentage | 1rem |
Shape corner radius |
These accepted values are an API agreement. The baseline implementation does not register or automatically validate them. Internal class names are implementation details, so page authors should customize the public variables rather than depend on .shape.
Build a complete local example on your Mac
Use a plain-text editor and a browser supporting CSS custom properties, Shadow DOM, and autonomous custom elements. This example uses an autonomous element extending HTMLElement; MDN distinguishes that approach from customized built-in elements, which Safari does not plan to support. Using custom elements
In TextEdit, choose File > New, then Format > Make Plain Text. Paste the complete document below and save it as token-demo.html. If prompted, choose Use .html. When reopening it for editing, select Ignore rich text commands in the open dialog, or enable Display HTML files as HTML code instead of formatted text in settings. Work with HTML documents in TextEdit on Mac
The document’s first stylesheet owns tokens, integration rules, and page layout. The component creates a separate stylesheet inside its shadow root. HTML defines the style element as a way to embed CSS; placing all the source in one file keeps the example’s resource setup explicit. HTML Standard: The style element
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Orbit tile token demo</title>
<style>
/* Page-owned shared tokens */
:root {
--scene-accent: #4058b8;
--scene-rounding: 12px;
}
/* Page-owned integration mapping */
orbit-tile {
--orbit-tile-fill: var(--scene-accent);
--orbit-tile-radius: var(--scene-rounding);
}
/* A local page theme */
.alternate {
--scene-accent: #276f64;
--scene-rounding: 24px;
}
/* An intentional override for one component instance */
orbit-tile.special {
--orbit-tile-fill: #a65b20;
--orbit-tile-radius: 50%;
}
body {
max-width: 36rem;
margin: 3rem auto;
padding: 0 1rem;
font: 1rem/1.5 system-ui, sans-serif;
}
main { display: grid; gap: 1rem; }
</style>
</head>
<body>
<main>
<orbit-tile>Sample A</orbit-tile>
<section class="alternate" aria-label="Alternate samples">
<orbit-tile>Sample B</orbit-tile>
</section>
<orbit-tile class="special">Sample C</orbit-tile>
</main>
<script>
class OrbitTile extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
:host { display: block; }
.tile {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: #f2f4f6;
color: #202630;
}
.shape {
display: block;
width: 4rem;
height: 4rem;
flex: 0 0 4rem;
background-color: var(--orbit-tile-fill, #667085);
border-radius: var(--orbit-tile-radius, 1rem);
}
`;
const tile = document.createElement('div');
tile.className = 'tile';
const shape = document.createElement('span');
shape.className = 'shape';
shape.setAttribute('aria-hidden', 'true');
tile.append(shape, document.createElement('slot'));
root.append(style, tile);
}
}
customElements.define('orbit-tile', OrbitTile);
</script>
</body>
</html>
Open the saved file in Safari, Chrome, or Firefox. The example has no external scripts, fonts, images, or stylesheets. The JavaScript passed a syntax check; the following visual results are expected outcomes derived from the declarations, rather than a report of browser execution.
| Instance | Expected shape | Value path |
|---|---|---|
Sample A |
Blue, with 12px corners |
Root tokens → host mapping → component |
Sample B |
Green, with 24px corners |
Section tokens → host mapping → component |
Sample C |
Orange circle | Instance override → component |
Notice that the component’s JavaScript contains no --scene-* references. Its stylesheet only consumes the public --orbit-tile-* names. The page can reorganize its theme vocabulary without changing the component.
Where should token-to-component mapping happen?
Map values where the intended theme is available. In this demo, the orbit-tile rule runs on each host. That lets the second host resolve --scene-accent after inheriting the section’s green value.
Avoid moving that mapping to :root without considering resolution timing. If --orbit-tile-fill: var(--scene-accent) is computed there, descendants inherit its resolved value. Changing only --scene-accent on a later section does not rebuild the inherited alias. Custom properties resolve their var() references before their computed values are inherited. CSS Custom Properties for Cascading Variables Module Level 1
The practical decision is scope: use a host-level mapping when each instance should follow its surrounding page tokens. If an entire region intentionally shares one public component value, declaring that public variable on the region can be appropriate—but remove the host mapping for that variable so it does not replace the inherited value.
For repeated internal uses, an implementation alias can keep fallback declarations together. A name such as --_orbit-tile-fill can signal an internal convention, but the underscore does not create enforced privacy. Direct consumption, as shown in the complete example, is easier to follow when each value is used once. Styling Web Components
How can you diagnose an override that does not work?
Use the three instances as a small contract test. Change one declaration at a time, save, and reload. These checks help distinguish a missing resource, an inheritance problem, and an invalid value.
- Change the root accent to
#7251a1. Expect onlySample Ato adopt it; the section and instance have their own choices. - Change the section accent to
#32758a. ExpectSample Bto change without editing component code. - Remove the entire integration mapping. Expect
Sample AandSample Bto use the component’s gray fill and1remradius;Sample Cretains its explicit public overrides. - Restore the mapping before continuing other experiments.
If all shapes disappear, first check that the document is saved as HTML and its script is intact. If labels appear but shapes are unstyled, inspect the component stylesheet. A document stylesheet alone does not style shadow descendants by ordinary selectors; the implementation must attach its internal styles to the shadow tree. MDN documents both embedded styles and constructed stylesheets for that purpose. Using shadow DOM
If only an ancestor override fails, look for a declaration of the public variable directly on the host. In this example, the integration mapping is such a declaration. Either change the page token that feeds it, override the public variable on the instance, or remove the mapping when ancestor-level public theming is the intended contract.
If a shape becomes transparent after assigning --orbit-tile-fill: 12px, the fallback will not repair it. The variable exists, but its substituted value is invalid for background-color. That declaration becomes invalid at computed-value time; it does not return to an earlier background declaration. Remove the assignment or supply a valid color. CSS Custom Properties for Cascading Variables Module Level 1
Should public variables use @property?
Registration is optional for this contract. @property adds syntax constraints, an initial value, and explicit inheritance behavior. MDN marks it Baseline 2024, available across current browsers since July 2024, while warning that older browsers may lack support. The complete example avoids making registration a prerequisite. @property CSS at-rule
If you introduce registration, coordinate it at document level. Chrome’s implementation documentation reports that registrations inside shadow roots are ignored; its report is dated August 2024, so treat it as a documented interoperability boundary rather than evidence of a fresh browser test. The supported path described there is document-scope registration, with the component consuming the property across the boundary. Author-defined CSS names and shadow DOM: In specification and in practice
Registration also changes behavior. Setting inherits: false would break the example’s host-to-shape inheritance. A registered initial color means an unset property has that color, so a consuming var() fallback is no longer the missing-value default. Keep inherits: true for this design and deliberately choose which mechanism owns the default. @property CSS at-rule
The finished contract should let a page author explain every value’s path: a shared page choice, a mapping into a documented component control, and a component-owned fallback. Keep the mapping close to its intended theme scope, leave inherited public inputs unset internally, and retain the three-instance example whenever you revise the API.
Sources
- Using CSS custom properties (variables) — MDN Web Docs.
- CSS Shadow Module Level 1 — W3C CSS Working Group.
- Styling Web Components — Chrome for Developers.
- Using custom elements — MDN Web Docs.
- Work with HTML documents in TextEdit on Mac — Apple Support.
- HTML Standard — WHATWG.
- CSS Custom Properties for Cascading Variables Module Level 1 — W3C.
- Using shadow DOM — MDN Web Docs.
- @property CSS at-rule — MDN Web Docs.
- Author-defined CSS names and shadow DOM: In specification and in practice — Chrome for Developers.