This document is the canonical reference for both humans and coding agents working on the shared design system for FAST example apps. It defines the semantic, role-based naming model, the theme contract, and the rules for adding or modifying tokens.
If you only need a quickstart, see README.md.
- Provide a single shared token vocabulary for every app in
examples/*. - Stay framework- and runtime-neutral: tokens are plain CSS custom properties that work in any stylesheet, template, or shadow root.
- Use semantic, role-based names so styles describe intent (
background,foreground,stroke,status-danger,text-global-body3) instead of color values or component identities. - Support light, dark, and system-driven themes through a single attribute on
<html>, with no JavaScript runtime owned by this package.
- This package does not ship a component library.
- This package does not ship a JavaScript or TypeScript theme API. There are
no helpers to import, no theme objects, and no observable change events.
Apps that need a runtime toggle implement it themselves with two lines of
DOM code (see
README.md). - This package is not a Theme Builder, token-authoring tool, or palette generator.
- This package is not published to npm outside this monorepo.
- This package is not formally aligned with any external design language. Token names are semantic and use-site-oriented; values, scale, and coverage are tuned for FAST example apps.
tokens.css ─▶ :root CSS custom properties ─▶ var(--fast-*) ─▶ rendered UI
▲
│
<html data-theme="..."> attribute (set by the app)
The flow is intentionally short:
- The app imports
tokens.cssat startup. - The stylesheet registers every shared token on
:root. Color and elevation values resolve through the CSSlight-dark()function, driven by the inheritedcolor-scheme. - Components reference tokens through
var(--fast-...).
Every token follows the same kebab-case shape:
token = "--fast-" category [ "-" subcategory ] [ "-" slot ] [ "-" variant ] [ "-" state ] ;
category = "text" | "padding" | "gap" | "corner" | "stroke" | "ctrl" |
"background" | "foreground" | "status" | "shadow" |
"duration" | "curve" ;
subcategory = "global" | "style" | "content" | "between-content" |
"width" | "divider" | "ctrl" | "layer" | "web-page" |
"focus" | "card" | "flyout" | "danger" | "success" | "warning" ;
slot = lowercase kebab-case named slot (e.g. "primary", "secondary",
"subtle", "brand", "neutral-primary", "neutral-secondary",
"hint", "on-brand", "on-outline", "outer", "key", "ambient") ;
variant = lowercase kebab-case modifier (e.g. "solid", "tint") ;
state = "rest" | "hover" | "pressed" | "disabled" |
"selected-rest" | "selected-hover" | "selected-pressed" |
"selected-disabled" ;
Ordering rules:
- Always start with
--fast-. - Keep segments in
category → subcategory → slot → variant → stateorder. - Omit any segment that does not apply rather than inserting a placeholder.
- Use lowercase kebab-case for every segment.
- State (
rest,hover,pressed,disabled, optionally prefixed withselected-) is always the trailing segment when it applies.
Tokens are scoped by use-site (e.g. ctrl-brand, ctrl-on-outline,
web-page, layer, card, flyout) rather than by abstract color role.
| Category | Purpose |
|---|---|
text |
Typography ramp (text-global-*), font styles (text-style-*). |
padding |
Inset spacing for content within a container (padding-content-*). |
gap |
Spacing between sibling elements (gap-between-content-*). |
corner |
Border radius scale (corner-{none, small, medium, large, circular}). |
stroke |
Border colors (stroke-divider-*, stroke-ctrl-on-outline-*) and widths (stroke-width-*). |
ctrl |
Control-level tokens that don't fit stroke/background/foreground, such as focus rings (ctrl-focus-outer-*). |
background |
Surface fills: pages (background-web-page-*), layers (background-layer-*), control fills (background-ctrl-brand-*, background-ctrl-subtle-*). |
foreground |
Text and icon colors, scoped by where they appear (foreground-ctrl-neutral-*, foreground-ctrl-brand-*, foreground-ctrl-on-brand-*, foreground-ctrl-hint-*). |
status |
Semantic status colors with solid and tinted variants (status-{danger, success, warning}-{background, foreground} and the -tint-{background, foreground, stroke} siblings). |
shadow |
Elevation. Atomic pairs (shadow-card-rest-key, shadow-card-rest-ambient) plus a composed alias (shadow-card-rest). |
duration |
Motion durations (duration-{ultra-fast, faster, fast, normal, gentle, slow, slower, ultra-slow}). |
curve |
Motion curves (curve-{accelerate, decelerate, easy-ease}-{max, mid, min}, curve-easy-ease, curve-linear). |
The ramp uses semantic role names. Lower number = larger.
| Tier | Font size | Line height | Intended use |
|---|---|---|---|
caption2 |
10 | 14 | Smallest helper text, badges. |
caption1 |
12 | 16 | Captions, helper text, dense table data. |
body3 |
14 | 20 | Standard UI text, form controls. |
body2 |
16 | 22 | Comfortable reading body. |
body1 |
18 | 26 | Large body / lead paragraph. |
subtitle2 |
18 | 26 | Subsection heading. |
subtitle1 |
20 | 26 | Section heading. |
title2 |
20 | 26 | Smaller card or panel title. |
title1 |
24 | 32 | Page or large card title. |
display2 |
28 | 32 | Smaller display heading. |
display1 |
38 | 40 | Largest display heading (marketing surfaces). |
Every --fast-text-global-<tier>-font-size has a same-tier
--fast-text-global-<tier>-line-height partner. Pair them.
t-shirt sizes with full words and hyphens:
none, xx-small, x-small, small, medium, large, x-large,
xx-large, xxx-large (padding only).
padding-content-* is for inset within a container. gap-between-content-*
is for distance between sibling elements (margins, CSS gap). The two are
separate categories because they semantically describe different intents,
even when the numeric value coincides.
none (0), small (2), medium (4), large (6), circular (9999px).
stroke-width-default (1px), stroke-width-divider-strong (1.5px). Focus
rings have their own dedicated token: ctrl-focus-outer-stroke-width (2px).
rest, hover, pressed, disabled. For selectable controls, the
selected- prefix yields selected-rest, selected-hover,
selected-pressed, selected-disabled.
The historical web vocabulary of default/active is not used in this
package — rest and pressed are the authoritative names.
--fast-background-layer-primary-solid— primary card / surface fill.--fast-background-ctrl-brand-rest— brand button background, rest state.--fast-background-ctrl-brand-hover— brand button background, hover.--fast-foreground-ctrl-on-brand-rest— text on top of a brand fill.--fast-foreground-ctrl-neutral-secondary-rest— secondary / muted body text (timestamps, metadata).--fast-foreground-ctrl-hint-default— placeholder and helper text only.--fast-stroke-divider-subtle— soft border around a card or surface.--fast-ctrl-focus-outer-stroke/--fast-ctrl-focus-outer-stroke-width— keyboard focus ring color and width.--fast-status-danger-tint-foreground— destructive button text color (subtle, no solid fill).--fast-status-danger-tint-background— destructive button hover fill.--fast-text-global-body3-font-size— paired with--fast-text-global-body3-line-height.--fast-padding-content-medium— 16-pixel inset.--fast-gap-between-content-x-small— 8-pixel gap between siblings.--fast-corner-large— 6-pixel border radius.--fast-shadow-card-rest— composed card elevation (key + ambient layers).--fast-duration-fastpaired with--fast-curve-easy-ease.
tokens.css declares one block:
:root {
color-scheme: light dark;
/* theme-independent tokens (typography, padding, gap, corner, motion) */
/* theme-resolving tokens — color and elevation */
--fast-background-web-page-primary: light-dark(#fafafa, #1f1f1f);
--fast-foreground-ctrl-neutral-primary-rest: light-dark(#242424, #ffffff);
/* ... */
}
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }Each color or elevation token wraps its light value and dark value in the
CSS light-dark() function. The browser picks the matching
value from the inherited color-scheme, which follows
prefers-color-scheme unless data-theme forces it.
This produces three states:
<html> attribute |
Active palette |
|---|---|
data-theme="light" |
Light, always. |
data-theme="dark" |
Dark, always. |
| no attribute | Light by default; switches to dark while prefers-color-scheme: dark matches. |
An attribute (rather than a class) is used because it is an explicit, single state container that is easy to inspect in dev tools, easy to set during SSR or hydration, and easy to assert in tests.
For an intentionally single-theme app, hard-code the attribute in markup
(<html data-theme="light">) and never touch it from JavaScript. The
package does not ship separate single-theme stylesheets; one file covers
every case.
The light-dark() function requires Chrome / Edge 123+, Firefox 120+, or
Safari 17.5+ (all shipped 2023–2024). This package targets evergreen
browsers and does not ship a fallback. Apps that need to support older
browsers should provide their own static token sheet.
The token name stays semantic across themes; only the value changes.
| Token | Light theme | Dark theme |
|---|---|---|
--fast-background-web-page-primary |
Near-white page surface | Near-black page surface |
--fast-background-layer-primary-solid |
White card / panel | Dark gray card / panel |
--fast-foreground-ctrl-neutral-primary-rest |
Near-black body text | Near-white body text |
--fast-background-ctrl-brand-rest |
Brand blue | Brand blue, tuned for dark contrast |
--fast-status-danger-tint-foreground |
Danger text on light surface | Danger text on dark surface |
--fast-stroke-divider-subtle |
Soft light divider | Soft dark divider |
Status colors come in two flavors:
- Solid (
status-danger-background,status-danger-foreground) — a filled background with high-contrast text. Use for solid status pills, toast bars, error chips. - Tinted (
status-danger-tint-background,status-danger-tint-foreground,status-danger-tint-stroke) — a low-saturation background with the status color used for the text/icon. Use for destructive buttons, inline form errors, info banners, and other subtle status surfaces.
A destructive text button uses the tint pair: red text at rest
(status-danger-tint-foreground), a soft red background on hover
(status-danger-tint-background), and a red outline on focus
(status-danger-tint-stroke).
When working on tokens in this package, follow these rules.
- Pick the token whose use-site matches the styled element. A card
surface is
--fast-background-layer-primary-solid. A muted helper text is--fast-foreground-ctrl-neutral-secondary-rest. A subtle row divider is--fast-stroke-divider-subtle. - Do not pick a token because its current value happens to match a desired hex code. The value will change between themes; the name will not.
- Reserve
--fast-foreground-ctrl-hint-defaultfor placeholder and helper text only. For secondary body text (timestamps, captions, metadata) use--fast-foreground-ctrl-neutral-secondary-rest.
Need a value?
├─ Can an existing token express the same intent? → Reuse it.
├─ Is the value only local to one component?
│ → Define a local custom property inside that component's css`...`
│ block, backed by a --fast-* token. Do not add it to the package.
├─ Will multiple example apps or shared patterns reuse it?
│ → Add a token, following the naming grammar.
└─ Does it describe a brand-new design domain (e.g. blur, opacity scale)?
→ Coordinate with maintainers before adding a category.
- Stay inside an existing category whenever possible.
- Match the grammar:
--fast-<category>-<subcategory>-<slot>[-<variant>][-<state>]. - Keep names semantic. Avoid product-, component-, or palette-specific
suffixes such as
button-primary-blue. - For colors and shadows, add both light and dark values in a single
light-dark(lightValue, darkValue)declaration. Theme-independent colors (such as--fast-foreground-ctrl-on-brand-rest, which is always white) can stay as a plain value. - Keep scales monotonic: t-shirt sizes go from smallest to largest, durations from fastest to slowest, shadows from lowest to highest elevation.
- Update the category catalog in this document if the new token introduces a new subcategory or slot.
- Update the catalog table in
README.md.
- The value is only meaningful inside one component. Keep it local.
- The value is a one-off marketing color. Use a literal value inside that component only and document the choice in a comment.
- A near-equivalent token already exists. Reuse and adjust the existing token rather than introducing a synonym.
- Does the name match the
--fast-<category>-<subcategory>-<slot>[-<variant>][-<state>]grammar? - Is every segment lowercase kebab-case?
- Is the use-site semantic (not product- or component-specific)?
- For interactive variants, is the state segment (
rest,hover,pressed,disabled) last? - Are paired font-size and line-height values both added?
- For colors and shadows, are both light and dark values supplied via
light-dark()(or is the token genuinely theme-independent)? - Would another example-app developer understand the token from its name alone, without extra docs?
These constraints exist so automated edits stay coherent with the design
language. When making changes to any file in examples/:
- Never hard-code colors, font sizes, spacing, radii, border widths, or
shadows in component CSS. Use a
var(--fast-...)token. - Never invent ad-hoc tokens at
:rootinside an example app. Either add the token to this package or scope a local custom property to the component's host or shadow root. - Do not import any JavaScript or TypeScript from
@microsoft/fast-examples-design-system. The package exports CSS only. - Do not introduce a new theme name (such as a "high-contrast" theme) without first adding the corresponding selector and token block to this package and updating this document.
- Theme switching is two lines of DOM code:
document.documentElement.setAttribute("data-theme", "light")orremoveAttribute("data-theme")for system preference. Implement it inline in the consuming app; do not factor it into a shared helper here. - When refactoring an app to use this design system, prefer tokens that
match the original intent rather than tokens that match the original value.
For example, an existing
#666foreground used for helper text should become--fast-foreground-ctrl-neutral-secondary-rest, not--fast-foreground-ctrl-hint-default(which is reserved for placeholders).
- Importing JavaScript from this package. There is none — the previous iteration exported helpers; the current iteration intentionally does not.
- Hard-coding colors, font sizes, spacing, radii, border widths, or shadows in component styles.
- Introducing app-specific tokens at
:rootfrom inside an example app. - Adding aliases or synonyms for tokens that already exist.
- Naming tokens after products or components (e.g.
--fast-todo-row-bg). - Using class names instead of the
data-themeattribute for theme switching. - Using the historical state vocabulary (
default,active,default-hover) — the current package usesrest,hover,pressed,disabled.
README.md— quickstart and token catalog../README.md— overview of theexamples/workspace../DESIGN.md— workspace-level design guidance