Semantic tone classes
COLOR WITH A PURPOSE
Choose an intent. Inherit a palette. Paint a role.
Ten semantic tones share one class vocabulary. Your skin supplies the colors; the active theme supplies the mode; the nearest tone supplies the context.
WORKSPACE / READY TO SHARE
A coherent palette, without repeated colors
Surface, alternate surface, and button roles belong to the same semantic context.
This text reads the inherited current palette.
The children inherit feature. Only the status message selects its own success palette.
How the architecture fits together
01 / SKIN
Semantic tokensDefines every tone's canvas, roles, states, text, borders, and light.
02 / THEME
Scoped variablesLight and dark roots expose the skin's palette as CSS custom properties.
03 / CONTEXT
Current palette tone--info binds inherited current variables to the info tokens.
04 / PAINT
Shared classesFill, border, text, and inset classes consume those current variables.
05 / COMPONENT
Variant recipesComponents compose the same classes into their public variants.
The system separates meaning from appearance. A skin can change what accent looks like without changing component markup. Paint classes are shared across all tones, rather than generating another set of paint rules for every color.
The token contract
Every semantic tone has this structure in both theme modes:
semantic.tone.info
├─ tone direct accent color
├─ canvas / canvascolor canvas fill / plain color
├─ overlay overlay color
├─ spotlight.field / light field and radial light
├─ surface / surfaceAlt / button
│ ├─ rest / hover / active / disabled
│ │ └─ background / bgcolor / border / text
│ └─ focusRing
├─ root.text.default / subtle
├─ root.border.subtle / default / focus
├─ text.default / subtle
└─ border.subtle / default / focus background is the complete fill and may contain a gradient. bgcolor is the plain color used by the flat fill families. Role text and border tokens are paired with that role's fill. General text and border tokens serve typography and structural accents outside a matched role.
From a theme token to a painted element
/* Conceptual binding for tone--info; the framework generates this. */
.tone--info {
--ps-current-surface-rest-background:
var(--ps-semantic-tone-info-surface-rest-background);
}
.tone-fill-surface {
background: var(--ps-current-surface-rest-background);
} getCurrentThemePalette() returns symbolic CSS variable references for use in style builders. getThemePaletteVarName(path) and getCurrentThemePaletteVarName(path) produce the variable names when you need to inspect or integrate them directly.
The theme root starts with the neutral current palette. At that root, general current text and border variables use neutral's root.text and root.border aliases. An explicit tone--neutral instead selects neutral's regular text and border tokens, just as the other tone selectors do. This allows document chrome and a neutral component to have different typography and border needs.
Implementation: token contract ↗ · current palette bindings ↗ · paint and state presets ↗
Selection, paint, and geometry
tone--info Context selected. This box still uses its surrounding paint.
A consumer such as text-subtle already sees the new palette.+ fill + text Matched surface colors, inherited by ordinary children.
+ border + geometry Border color, width, padding, and radius complete the recipe.
Selecting a palette alone adds no background, border, or text paint.
<div class="tone--info tone-fill-surface tone-border-surface
tone-text-surface b-1 rounded-md p-3">
An information surface
</div> Tone border classes set color. Add a width such as b-1, b-2, or bl-2 to make the border visible. Spacing and radius remain independent utilities. Pair role fill, border, and text when you want the skin's intended contrast.
All ten semantic tones
Every card uses the same recipe. Its alternate surface and button role inherit the selected tone. These meanings are conventions; your skin determines their actual colors.
tone--neutral One palette. Several useful roles.
tone--accent One palette. Several useful roles.
tone--feature One palette. Several useful roles.
tone--secondary One palette. Several useful roles.
tone--custom One palette. Several useful roles.
tone--ghost One palette. Several useful roles.
tone--info One palette. Several useful roles.
tone--success One palette. Several useful roles.
tone--warning One palette. Several useful roles.
tone--danger One palette. Several useful roles.
All ten fill families
The bare swatches keep the fills visible without placing potentially incompatible text over them. Compare the regular role backgrounds with their flat counterparts: a skin may give the former chrome or gradients, while flat always takes bgcolor.
tone-fill Direct tone color tone-fill-canvas Canvas background tone-fill-surface Surface background tone-fill-surface-alt Alternate surface background tone-fill-button Button background tone-fill-flat Surface bgcolor only tone-fill-flat-alt Alternate surface bgcolor only tone-fill-flat-solid Button bgcolor only tone-fill-spotlight Field + radial light tone-fill-glass Light + translucent veil Use tone-fill for a direct tone accent and tone-fill-canvas for canvas paint. Neither implies matching readable text. Surface, alternate surface, and button each have their own paired text and border classes. Spotlight and glass use alternate surface text and borders in the component recipes.
Inheritance and nested contexts
Current variables inherit through ordinary DOM descendants.
Still accent, with the alternate surface role.
The info child overrides only its own subtree. Its sibling continues to use accent.
<section class="tone--accent">
<div class="tone-fill-surface tone-text-surface">Inherits accent</div>
<aside class="tone--info">
<div class="tone-fill-surface tone-text-surface">Inherits info</div>
</aside>
</section> Nested theme roots
html[data-theme="light"] and html[data-theme="dark"] set the document mode. .theme--light and .theme--dark create local theme regions, including inside the opposite mode. A theme root supplies its own text color and color-scheme, and rebases the current palette to neutral. Select a tone on that root or inside it to establish another intent. Root bindings use zero specificity so an explicit tone selector can win on the same element.
Local text and browser control scheme are light.
Local text and browser control scheme are dark.
Both regions stay in their assigned mode while you toggle the site's theme. Each starts neutral, then explicitly selects accent for its inner surface.
Inspect a real recipe
Change the tone, role, and state. The inspector reads the generated current variables from the actual preview element. Switch the site theme to see the values change without changing the recipe.
Every role and interaction state
All three matched roles are shown at rest, hover, active, and disabled. Hover snapshots paint the hover tokens directly so they remain visible; the other samples use the actual state selectors. The rest samples also respond to your pointer.
surface · rest surface · hover The hover token is painted directly so the state remains visible without a pointer.
surface · active surface · disabled surface-alt · rest surface-alt · hover The hover token is painted directly so the state remains visible without a pointer.
surface-alt · active surface-alt · disabled button · rest button · hover The hover token is painted directly so the state remains visible without a pointer.
button · active button · disabled <button type="button" class="tone--accent
tone-fill-button-all tone-border-button-all tone-text-button-all
b-1 rounded-md p-2">
All four states
</button> The unsuffixed class paints rest. -hover applies under :hover; -active applies under :active or .active; -disabled applies under native :disabled. -all combines the available states for that family. A hover suffix alone does not paint rest, and .active does not create button behavior or an ARIA state. aria-disabled="true" alone does not match :disabled.
Exact class and state reference
The table lists every generated family. Append only the suffixes listed here; state support differs by family.
| Class family | Rest | -hover | -active | -disabled | -all | Source |
|---|---|---|---|---|---|---|
tone-fill | Yes | Yes | Yes | — | — | Direct tone color |
tone-fill-canvas | Yes | Yes | Yes | — | — | Canvas |
tone-fill-surface, tone-fill-surface-alt, tone-fill-button | Yes | Yes | Yes | Yes | Yes | Role background |
tone-fill-flat, tone-fill-flat-alt, tone-fill-flat-solid | Yes | Yes | Yes | Yes | Yes | Role bgcolor |
tone-fill-spotlight, tone-fill-glass | Yes | Yes | Yes | Yes | Yes | Field / light / veil |
tone-border | Yes | Yes | Yes | — | Yes | Direct tone color |
tone-border-surface, tone-border-surface-alt, tone-border-button | Yes | Yes | Yes | Yes | Yes | Role border |
tone-text | Yes | Yes | Yes | — | — | Direct tone color |
tone-text-surface, tone-text-surface-alt, tone-text-button | Yes | Yes | Yes | Yes | Yes | Role text |
tone-text-bg-surface, tone-text-bg-surface-alt, tone-text-bg-button | Yes | Yes | Yes | Yes | Yes | Role fill clipped to text |
tone-inset-l, tone-inset-r, tone-inset-t, tone-inset-b, tone-inset-x, tone-inset-y | Yes | Yes | Yes | — | — | Direct tone inset shadow |
The general border family has an -all class but no disabled state. The general text, direct fill, canvas, and inset families have no -all class. Focus is a separate concern: these preset families do not generate -focus classes.
Text, borders, and clipped fills
tone-text / direct tone color
text-subtle / general subtle text
tone-text-surface / paired surface text
tone-text-surface-alt / paired alternate text
Surface fill in text
Alternate fill in text
The clipped samples use role backgrounds as glyph paint; they are decorative text, not the paired readable role text colors.
text-subtle, prose-meta, and text-eyebrow read current general subtle text. b-subtle, b-default, b-focus, and b-tone read current general border or tone tokens and set border color with !important. Avoid competing border-color classes on the same element.
Spotlight and glass: all five origins
Spotlight combines a field with radial light. Glass paints radial light and a translucent veil; it does not add backdrop blur. Both read --ps-spotlight-from, which inherits independently of the tone context. Every origin is visible below, with an inner glass panel sharing its parent's light position.
spotlight-from-top-left spotlight-from-top spotlight-from-top-right spotlight-from-bottom-left spotlight-from-bottom-right | Origin class | Light position |
|---|---|
spotlight-from-top-left | 8% 0% (also the default) |
spotlight-from-top | 50% 0% |
spotlight-from-top-right | 92% 0% |
spotlight-from-bottom-left | 8% 100% |
spotlight-from-bottom-right | 92% 100% |
Spotlight's light reach grows from 46% at rest to 60% on hover and 72% when active; disabled paints the field alone. Glass uses a 75% light reach and veil strengths of 45%, 58%, and 70%; disabled paints only a 25% veil. A custom origin can use style="--ps-spotlight-from: 35% 20%".
<div class="tone--feature tone-fill-spotlight tone-text-surface-alt
spotlight-from-top-right rounded-md p-3">
<div class="tone-fill-glass tone-border-surface-alt
tone-text-surface-alt b-1 rounded-md p-3">
Same light origin, inherited feature palette
</div>
</div> Insets: all directions and sizes
Insets paint an internal edge using the direct tone color. x combines left and right; y combines top and bottom. Each class sets box-shadow, so use the combined direction rather than stacking individual inset classes to combine edges.
tone-inset-l tone-inset-r tone-inset-t tone-inset-b tone-inset-x tone-inset-y The default inset width is .25em. inset-size-* changes the width, including zero to remove it; the unit follows the element's font size.
inset-size-0 inset-size-1 inset-size-2 inset-size-3 inset-size-4 inset-size-5 inset-size-6 | Size | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| Width | 0 | .25em | .5em | .75em | 1em | 1.5em | 2em |
<div class="tone--warning tone-fill-surface tone-text-surface
tone-inset-l inset-size-2 p-3">
An attention marker without changing the outer border
</div> How components compose these classes
Internally, resolveComponentClasses() combines a variant's rest recipe, optional interaction classes, and an explicit tone selector. With no tone supplied, the component inherits its surrounding context. The 18 variants below use the actual resolver with tone: 'accent' and variantMode: 'stateful'; hover and press them to see their behavior.
solid surface surfaceAlt spotlight glass flat flatAlt flatSolid outlineFill outline subtle subtleBtn link sheen underline rail bracket none | Variants | Recipe |
|---|---|
solid, surface, surfaceAlt | Matched button, surface, or alternate fill + border + text |
spotlight, glass | Light fill + alternate surface border and text |
flat, flatAlt, flatSolid | Plain role bgcolor + matched border and text |
outlineFill, outline | Outline recipes with button fill on interaction |
subtle, subtleBtn | General text with surface or button fill on interaction |
link | General text + hover underline |
sheen | Button fill clipped to text |
underline | General text + bottom inset on interaction |
rail | Left border and spacing + left inset on hover |
bracket | General text + top and bottom insets on interaction |
none | No variant recipe |
variantMode: 'stateless' keeps the rest recipe and omits the resolver's interaction additions. stateful adds the appropriate hover and active recipes; it does not automatically add every utility family's disabled classes. Components may provide their own disabled styling and behavior. none removes the variant recipe, while component base styles and an explicitly supplied tone still apply.
<Btn tone="info" variant="surface" variantMode="stateful">
A component using the surface recipe
</Btn> Read the complete variant recipes ↗
Focus and custom styles
Each role exposes a focusRing token. Consume it in component or custom styles; the tone presets do not automatically draw a focus indicator. General border.focus is also available for structural focus borders.
import { getCurrentThemePalette } from '@purestack/ts-style'
const current = getCurrentThemePalette()
// Inside your registered style-builder callback:
root.select('.my-action:focus-visible').css({
outline: `2px solid ${current.button.focusRing}`,
outlineOffset: '3px',
}) The symbolic reference keeps the style responsive to the nearest tone and theme. Apply tone--danger to that action and its focus ring follows the danger button role without another selector.
Continue with themes and skins for palette authoring and utility classes for geometry and layout.