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.

One context, several rolesCOMPOSITION

WORKSPACE / READY TO SHARE

A coherent palette, without repeated colors

Surface, alternate surface, and button roles belong to the same semantic context.

Release notes

This text reads the inherited current palette.

Alternate surface / supporting detail
Checks passed

The children inherit feature. Only the status message selects its own success palette.

How the architecture fits together

01 / SKIN

Semantic tokens

Defines every tone's canvas, roles, states, text, borders, and light.

02 / THEME

Scoped variables

Light 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 classes

Fill, border, text, and inset classes consume those current variables.

05 / COMPONENT

Variant recipes

Components 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

Build a surface in three stepsCOMPOSITION
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.

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.

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

Nearest tone winsCOMPOSITION
Parent: tone--accent

Current variables inherit through ordinary DOM descendants.

Child: tone--info
Grandchild inherits info
Sibling: no tone selector

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.

Two local modesCOMPOSITION
theme--light / neutral root

Local text and browser control scheme are light.

Explicit accent in light mode
theme--dark / neutral root

Local text and browser control scheme are dark.

Explicit accent in dark mode

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.

<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

Typography consumes context tooCOMPOSITION

tone-text / direct tone color

text-subtle / general subtle text

tone-text-surface / paired surface text

tone-text-surface-alt / paired alternate text

tone-text-button / paired button text

Surface fill in text

Alternate fill in text

Button fill in text

tone-border / direct tone color
b-subtle
b-default
b-focus
b-tone

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.

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.

The default inset width is .25em. inset-size-* changes the width, including zero to remove it; the unit follows the element's font size.

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.

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.