Design Tokens
This lesson explains design tokens for Web Components, how to define shared color, spacing, and typography values with CSS custom properties, and how to theme encapsulated components across applications.
What Are Design Tokens?
Design tokens are named design decisions such as colors, spacing,
font sizes, border radii, and shadows. Instead of hardcoding
#2563eb or 16px throughout components,
you store values once and reference them everywhere.
In Web Components, tokens are usually implemented as
CSS custom properties. They work well with Shadow
DOM because custom properties can pierce shadow boundaries, making
them ideal for shared theming.
| Token Type | Example |
| Color | --color-primary |
| Spacing | --space-md |
| Typography | --font-size-body |
| Radius | --radius-sm |
| Shadow | --shadow-card |
| Motion | --duration-fast |
Basic Token Example
:root {
--color-primary: #2563eb;
--color-surface: #ffffff;
--space-sm: 0.5rem;
--space-md: 1rem;
--radius-md: 0.5rem;
}
class AppCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
article {
background: var(--color-surface, #fff);
padding: var(--space-md, 1rem);
border-radius: var(--radius-md, 0.5rem);
}
</style>
<article><slot></slot></article>
`;
}
}
Global tokens on :root flow into shadow styles through
var(). Fallback values keep components usable even
when a host app does not define every token.
Primitive vs Semantic Tokens
| Layer | Purpose | Example |
| Primitive | Raw design values | --blue-600 |
| Semantic | Meaning in UI context | --color-primary |
| Component | Component-specific alias | --app-button-bg |
:root {
--blue-600: #2563eb;
--color-primary: var(--blue-600);
}
app-button {
--app-button-bg: var(--color-primary);
}
Primitive tokens capture the palette. Semantic tokens express
intent. Component tokens expose a small override surface for
consumers.
Component-Level Token API
:host {
--app-button-bg: var(--color-primary, #2563eb);
--app-button-color: var(--color-on-primary, #ffffff);
--app-button-radius: var(--radius-sm, 0.375rem);
}
button {
background: var(--app-button-bg);
color: var(--app-button-color);
border-radius: var(--app-button-radius);
}
<app-button style="--app-button-bg: #059669;">
Confirm
</app-button>
Define public tokens on :host so apps can theme a
component without breaking Shadow DOM encapsulation.
Shared Token File Structure
design-system/
├── tokens/
│ ├── colors.css
│ ├── spacing.css
│ ├── typography.css
│ └── index.css
└── components/
└── app-button.js
/* tokens/colors.css */
:root {
--gray-900: #111827;
--blue-600: #2563eb;
--color-text: var(--gray-900);
--color-primary: var(--blue-600);
}
/* tokens/index.css */
@import "./colors.css";
@import "./spacing.css";
@import "./typography.css";
Centralize tokens in dedicated CSS files and import them once in
the app or design system entry point.
Theming with Design Tokens
:root {
--color-surface: #ffffff;
--color-text: #111827;
}
:root[data-theme="dark"] {
--color-surface: #111827;
--color-text: #f9fafb;
}
Because components read semantic tokens, switching themes means
updating token values on :root or a theme container,
not rewriting component CSS.
Design Tokens in Lit
import { LitElement, css, html } from "lit";
class AppBadge extends LitElement {
static styles = css`
:host {
--badge-bg: var(--color-primary, #2563eb);
--badge-color: var(--color-on-primary, #fff);
}
span {
background: var(--badge-bg);
color: var(--badge-color);
padding: var(--space-xs, 0.25rem) var(--space-sm, 0.5rem);
border-radius: var(--radius-full, 999px);
}
`;
render() {
return html`<span><slot></slot></span>`;
}
}
Lit components use the same token pattern in static styles.
Global tokens still cascade into shadow styles through
var().
Tokens with Adopted Stylesheets
const tokenSheet = new CSSStyleSheet();
tokenSheet.replaceSync(`
:host {
--app-card-padding: var(--space-md, 1rem);
}
`);
const componentSheet = new CSSStyleSheet();
componentSheet.replaceSync(`
article {
padding: var(--app-card-padding);
}
`);
this.shadowRoot.adoptedStyleSheets = [
tokenSheet,
componentSheet
];
Shared constructable stylesheets can define token aliases once and
be adopted by many component instances. See
Adopted Stylesheets
for more detail.
Fallback Values in var()
button {
background: var(--app-button-bg, var(--color-primary, #2563eb));
padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
}
Use layered fallbacks so components remain usable in isolation,
inside a partially themed app, or in Storybook-style demos without
full global token setup.
Spacing and Typography Scales
:root {
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--font-size-sm: 0.875rem;
--font-size-md: 1rem;
--font-size-lg: 1.25rem;
--font-family-sans: system-ui, sans-serif;
}
Consistent scales keep components visually aligned across a design
system. Prefer scale tokens over arbitrary pixel values inside
shadow styles.
Tokens vs Hardcoded Styles
| Approach | Pros | Cons |
| Design tokens | Consistent theming, easier updates | Requires naming discipline |
| Hardcoded values | Fast for prototypes | Hard to theme and maintain |
::part() styling | Fine-grained external control | More consumer complexity |
Full Token-Based Button Component
class AppButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
:host {
--btn-bg: var(--color-primary, #2563eb);
--btn-color: var(--color-on-primary, #fff);
--btn-radius: var(--radius-sm, 0.375rem);
--btn-padding-y: var(--space-xs, 0.375rem);
--btn-padding-x: var(--space-md, 1rem);
}
button {
background: var(--btn-bg);
color: var(--btn-color);
border: 0;
border-radius: var(--btn-radius);
padding: var(--btn-padding-y) var(--btn-padding-x);
font: inherit;
cursor: pointer;
}
</style>
<button type="button"><slot></slot></button>
`;
}
}
When to Use Design Tokens
- You build a shared Web Components design system.
- Multiple apps need consistent branding and theming.
- You support light, dark, or white-label themes.
- Components use Shadow DOM and need external theme control.
- Design and engineering share a common visual language.
- You want to change spacing or colors globally with one update.
Design Token Best Practices
- Use semantic names such as
--color-primary, not --blue in components. - Expose a small public token API on
:host. - Always provide sensible
var() fallbacks. - Keep primitive and semantic token layers separate.
- Document which tokens consumers can override.
- Prefer tokens over
::part() for broad theming. - Version and publish token CSS with the component library.
Common Design Token Mistakes
- Hardcoding colors inside shadow styles after adopting tokens.
- Using inconsistent token naming across components.
- Exposing too many component tokens with no clear purpose.
- Skipping fallbacks and breaking isolated component demos.
- Mixing primitive color names into component CSS directly.
- Expecting global classes to style shadow internals.
- Changing token meaning without versioning the design system.
Key Takeaways
- Design tokens store reusable visual decisions by name.
- CSS custom properties are the standard token mechanism in Web Components.
- Tokens pierce Shadow DOM and enable encapsulated theming.
- Primitive, semantic, and component token layers keep systems maintainable.
- Fallback values make components resilient in any host app.
- Good tokens make design systems easier to scale across teams.
Pro Tip
Publish a small tokens.css file with your component
library. Apps import it once on :root, and every
shadow component immediately inherits the same visual foundation.