Skip to main content

Colors

Colors define the semantic color language of ttoss — brand identity, hierarchy, interaction meaning, contrast, state.

The system has two layers: Core Colors (intent-free palette primitives) and Semantic Colors (stable contracts consumed by UI code). Components consume semantic colors only — never core directly.


UX contexts in 60 seconds

Every semantic color token starts with a UX context — a plain description of what kind of UI the color is for. There are five, and they cover the whole surface area of a UI:

UX contextUse it forTypical components
actionanything the user triggersbuttons, toggles, menu items, action icons
inputanything the user enters or selects data intotext fields, selects, checkboxes, radios
navigationanything that moves the user between views or sectionslinks, tabs, breadcrumbs, pagination
feedbacksurfaces that report the outcome of an action or system eventtoasts, alerts, banners, inline validation
informationalpresentational surfaces — hold, group, layer, frame, or display content; never drive a transactionbody text, page backgrounds, cards, panels, dialogs, dividers, list rows, accordions

Picking a context is usually trivial: "is the user about to act, type, move, hear back, or just see/contain something?"

Interactivity is not a tiebreaker. A focusable Card, clickable panel, or expandable accordion is still informational — its purpose is presentational. Focusability and disclosure are orthogonal capabilities (covered by focus.ring.color and the expanded state).

Advanced. The five contexts are a formal projection of the nine FSL Entity Kinds — see FSL Entity Kind Mapping below. Most component authors never need to read the FSL layer.


Scope

Colors carry meaning and visual contrast — nothing else. Depth lives in elevation, line geometry in borders, whole-element transparency in opacity, charts in data visualization tokens. Color may pair with those families; it does not replace them.

Color names express intent, not appearance.


Core Colors

Core colors are intent-free primitives — they define which colors exist in a theme (brand, neutral, hue scales) at sufficient depth for semantic remapping across modes, but not where they are used.

Core token structure

core.colors.{family}.{scale}
  • family: a palette family such as brand, neutral, red, green, blue
  • scale: an ordered step inside that family

Core groups

A theme MUST define brand and neutral; hue families are open. Add a hue family only when needed to support a concrete semantic mapping.

FamilyRole in the paletteRequired steps
brandIdentity hue. Depth allows light/dark remapping without new values.open subset across 100..900
neutralZero-saturation anchor for surfaces, text contrast, dividers, subdued UI. Step 0 = white-end, 1000 = black-end, 500 = canonical mid.step 500 mandatory; others open
Hue scales (red, orange, green, yellow, teal, purple, pink, \u2026)Optional palette families used as semantic mapping sources.open

brand and neutral are palette-layer conventions, not semantic roles \u2014 do not encode usage (main, cta, danger, link, surface, focus) in core names. neutral is functionally equivalent to "gray" in other systems.\n\n> Why CoreColorRef is open. It is typed as '{core.colors.${string}}' \u2014 a template literal, not a closed union derived from the concrete theme. Type safety for color usage lives at the semantic layer (legal ux \u00d7 role \u00d7 dimension \u00d7 state and contrast pairings), not the palette-ref level. A closed union would break extensibility for derived themes and create a circular dependency between Types.ts and baseTheme.ts.

Example (Core Color Definition)

const coreColors = {
colors: {
brand: {
100: '#E6F0FF',
300: '#8CB8FF',
500: '#1463FF',
700: '#0B3EA8',
900: '#082861',
},

neutral: {
0: '#FFFFFF',
50: '#F8FAFC',
100: '#F1F5F9',
200: '#E2E8F0',
300: '#CBD5E1',
400: '#94A3B8',
500: '#64748B',
700: '#334155',
900: '#0F172A',
1000: '#020617',
},

red: {
100: '#FEE2E2',
300: '#FCA5A5',
500: '#EF4444',
700: '#B91C1C',
900: '#7F1D1D',
},

green: {
100: '#DCFCE7',
300: '#86EFAC',
500: '#22C55E',
700: '#15803D',
900: '#14532D',
},
},
};

Expected consumption pattern: semantic color tokens reference core colors by alias.


Semantic Colors

Semantic colors are the public color API — stable contracts that translate raw palettes into UI meaning along four axes: where in the experience (ux), what role (role), which visual layer (dimension), which state (state).

Token structure

{ux}.{role}.{dimension}.{state?}

See Usage Examples below for concrete tokens.


FSL Entity Kind Mapping

The ux axis is a projection-scoped subset of FSL Entity Kinds (FSL Structural Language §17.1). The resolver uses this normative table to translate ComponentExpression.responsibility → token UX context:

FSL Entity KindToken uxNotes
Actionaction1:1
Inputinput1:1
Selectioninputcheckbox, radio, picker — no separate selection UX context
Navigationnavigation1:1
Feedbackfeedback1:1
Collectioninformationalmenu, list, table
Overlayinformationaldialog, popover
Disclosureinformationalaccordion, collapsible panel, <details> — in-place reveal (FSL §1); uses expanded state for open/closed contract
Structureinformationalpanel, shell, frame

Interaction patterns that do not correspond to an Entity Kind (tooltips, helper banners, search/filter widgets) are expressed through existing kinds — typically Overlay for guidance and Input for discovery.


Role Coverage

role is a discriminated union of two decision classes (see FSL Lexicon §5) — a token carries one or the other, never both:

  • Emphasis: primary, secondary, accent, muted
  • Valence: positive, caution, negative

A valence implies its own emphasis. Intensity within a valence is expressed by dimension (e.g. negative.background is louder than negative.text), not by combining emphasis with valence. Each UX context enables only the subset that has stable meaning in it:

ClassRoleactioninputnavigationfeedbackinformational
Emphasisprimary
Emphasissecondary
Emphasisaccent
Emphasismuted
Valencepositive
Valencecaution
Valencenegative

Why some cells are empty:

  • action.positive / action.caution — Outcome and risk live in feedback.*; an Action's own colour expresses only negative evaluation (FSL §5). Destructive consequence (FSL §6) is a frequent driver of that choice, but the two dimensions are distinct — negative may also encode adverse-but-non-destructive intent (cancel paid subscription).
  • navigation.* valences — Navigation communicates location (current, visited), not health state.
  • feedback.secondary / accent — Feedback is direct: primary and muted cover its emphasis range.
  • input.accent — Inputs use primary for the brand-influenced active state; accent creates hierarchy ambiguity.

Picking a role

Valence dominates emphasis: if the token communicates outcome or validity (success / warning / error / destructive), pick the valence first — emphasis is implicit. Otherwise pick the emphasis that matches hierarchy weight in the current view.

Emphasis (no outcome to communicate):

You want to communicate…Role
the single most important element on this viewprimary
an alternative coexisting with the primary onesecondary
a highlight that draws attention without being the main pathaccent
presence with low priority (helper text, divider, optional control)muted

Only one primary per view per {ux}. If two candidates compete for primary, one of them is secondary.

Valence (outcome / validity to communicate):

The token reports…Role
success, completion, validity confirmedpositive
risk that needs attention but the user is not blockedcaution
failure, invalid state, or adverse intent (including destructive consequence)negative
no outcome — just hierarchyuse emphasis instead

Intensity within a valence is expressed by dimension, not by combining with emphasis.

feedback.negative.primary.background.default — combining valence + emphasis is forbidden. ✅ feedback.negative.text.default — quiet error (foreground only). ✅ feedback.negative.background.default — loud error (filled surface).


Dimension and State Registry

The foundation keeps a small canonical registry. ux is defined in UX contexts; role in Role Coverage. Domain-specific semantics (social, commerce, gamification) do not belong to the foundation \u2014 model them at the pattern/application layer unless promoted through governance.

Dimension level

dimensionMeaning
backgroundfills and surface backgrounds
borderoutlines, separators, rings, and other line-color pairings
textreadable foreground, labels, and text-like icons

State level

stateMeaning
defaultresting/base state
hoverpointer hover
activepress/engaged moment
focusedkeyboard/programmatic focus
disabledunavailable/non-interactive
selectedselected item in a set
checkedon/off control state
pressedpressed toggle state
expandeddisclosure open state
currentcurrent location in navigation
visitedvisited link state
indeterminatemixed/unknown boolean state
droptargetvalid drag-and-drop destination

Keep the state set stable. Add a new state only when the meaning cannot be expressed by an existing one.

Picking a state (disambiguation)

Several states sound interchangeable but answer different questions. Pick by what the state asserts about the element, not by the verb in the component name.

The state asserts…State
pointer is currently over the elementhover
pointer/key is currently down on the element (transient, lasts only while held)active
element has keyboard or programmatic focusfocused
element is non-interactivedisabled
element is one of many in a set and the user picked it (tab, list row, segment)selected
element is a two-state control that is currently on (checkbox, radio, switch)checked
element is a toggle button that is currently engaged (persistent, not transient)pressed
disclosure / accordion / details is currently openexpanded
element is the user's current location in a navigation set (active route, current step)current
link points to a URL the user has visitedvisited
boolean control is in a mixed/unknown state (parent checkbox over partial children)indeterminate
element is a valid drop destination during an active dragdroptarget

Common confusions resolved:

  • Tab in a tablistselected (one of many) and, when it represents the live route, also current. Not active, not pressed.
  • Toggle button ("Bold" in a toolbar)pressed (persistent). active is the brief moment of clicking.
  • Checkbox / Switch / Radiochecked. Not selected, not pressed.
  • Open accordion sectionexpanded. Not active, not selected.
  • Currently viewed nav itemcurrent. Not selected, not active.
  • Button mid-clickactive. Releases back to default / hover.

Not every {ux} × role × state is valid. Allowed roles per context are in Role Coverage; allowed states per context are below. Both are enforced by Types.ts — a token outside its row will not type-check.

Most contexts share an interactive base: default, hover, active, focused, disabled, droptarget. feedback is the exception — feedback is communicative, not interactive (FSL §3), so only default, focused (focusable wrapper / close button), and disabled apply.

uxAllowed states (full, no implicit base)
actiondefault, hover, active, focused, disabled, droptarget, pressed, expanded
inputdefault, hover, active, focused, disabled, droptarget, selected, checked, indeterminate, pressed, expanded
navigationdefault, hover, active, focused, disabled, droptarget, selected, current, visited, expanded
feedbackdefault, focused, disabled (communicative, not interactive)
informationaldefault, hover, active, focused, disabled, droptarget, selected, visited, expanded

Dimension expectations

Not every implementation needs all three dimensions. Components choose which they consume.

PatternDimensions used
Text linktext
Ghost buttontext, border
Filled buttonbackground, text
Surface / cardbackground, border, text

Relationship to Modes

Core palette values are immutable across modes; modes remap which core tokens the semantic layer references. Token names and component code never change. If a semantic color works in one mode but fails in another, remap the semantic reference to a different core token — do not mutate the core value or rename the semantic token.

Modes remap references, not values.


Cross-cutting tokens (siblings of semantic.colors.*)

Two tokens carry system-wide defaults that no {ux} owns. They live as siblings of semantic.colors.* per model.md §6, not inside it:

  • semantic.focus.ring.color — system focus indicator color
  • semantic.overlay.scrim — modal backdrop

They are not parallel vocabulary: per-context tokens ({ux}.{role}.border.focused) answer "how does this {ux} look when focused?"; cross-cutting tokens answer "what is the system default when no {ux} applies?".

Focus color — which token to pick

The component is…Use
an Action / Input / Navigation / Feedback (clear FSL Entity Kind){ux}.{role}.border.focused from semantic.colors.*
an Informational surface made interactive (focusable Card, profile chip, custom widget with no obvious {ux})semantic.focus.ring.color
an Input with validation valence (negative, caution) where focus must inherit the valence colourinput.{negative|caution}.border.focused (overrides the system default)

The two paths are not duplicates — they answer different questions and are picked by which question the component is asking.

Example

A focusable profile card (no obvious {ux}):

  • line geometry from semantic.border.outline.surface + semantic.focus.ring.{width,style} on :focus-visible
  • focus colour from semantic.focus.ring.color (system default)

A text input in error:

  • line geometry from semantic.border.outline.control + semantic.focus.ring.{width,style} on :focus-visible
  • focus colour from input.negative.border.focused (per-context override; the negative valence outranks the system default)

A raised card may combine:

  • surface color from informational.primary.background.default
  • outline color from informational.muted.border.default
  • shadow from elevation.surface.raised

Stacking informational surfaces

Multiple informational surfaces commonly overlap in the visual hierarchy — a Dialog (Overlay) over a page, a Card (Structure) over a panel, a row inside a List (Collection). They share the same UX context by design (see FSL Entity Kind Mapping) and may resolve to the same informational.*.background value, especially in dark modes where the available core.colors.neutral range is compressed.

Differentiation between stacked informational surfaces is paid in this order — never in colour:

  1. elevation is the primary separator. Overlay → elevation.surface.overlay, Structure/Collectionelevation.surface.flat | raised. Drop shadows are local to each level, so the rule survives arbitrary nesting (Card inside Dialog inside Drawer): each level paints its own shadow over whatever sits beneath it.
  2. border.outline.surface is the secondary separator. A 1px outline at ≥ 3:1 contrast against the adjacent background guarantees a perceptual edge even when shadow is suppressed (high-contrast preferences, print).
  3. Theme-side step displacement is the optional reinforcement. A theme MAY map the page background and informational.primary.background.default to different core.colors.neutral steps (for example page = neutral.1000, surface = neutral.900). This is a mode/mapping decision (see Modes), not a vocabulary change.

This is the operational form of Rules of Engagement #4: colour expresses intent, not depth. If two stacked surfaces still feel indistinguishable after applying (1) + (2) + (3), the answer is to strengthen elevation/border or remap a step — never to introduce a new colour bucket.


Rules of Engagement (non-negotiable)

  1. Semantic-only consumption. Components consume semantic colors only; core never directly.
  2. Intent, not appearance. Names express role and meaning — forbid buttonBlue, dangerBg, darkBorder, cardBorderSoft, textOnDark. No component or mode names in foundation tokens.
  3. Keep the registry small. Do not expand ux, role, or state casually; promote new entries only through governance.
  4. Color does not model depth. Use elevation for depth, borders for line geometry; do not invent extra color roles to encode them.
  5. Validate pairings, not swatches. A color is only valid when its intended text ↔ background or border ↔ adjacent surface pairing is valid.

Usage Examples

UsageToken example
Filled primary button backgroundaction.primary.background.default
Filled primary button labelaction.primary.text.default
Input border at restinput.primary.border.default
Input border on focusinput.primary.border.focused
Current nav item textnavigation.primary.text.current
Muted body copyinformational.muted.text.default
Negative feedback surfacefeedback.negative.background.default
Positive feedback textfeedback.positive.text.default

Example (Semantic Color Definition)

const semanticColors = {
action: {
primary: {
background: {
default: '{core.colors.brand.500}',
hover: '{core.colors.brand.700}',
active: '{core.colors.brand.900}',
disabled: '{core.colors.neutral.200}',
},
text: {
default: '{core.colors.neutral.0}',
disabled: '{core.colors.neutral.500}',
},
border: {
default: '{core.colors.brand.500}',
focused: '{core.colors.brand.700}',
disabled: '{core.colors.neutral.200}',
},
},
},

informational: {
muted: {
text: {
default: '{core.colors.neutral.500}',
},
border: {
default: '{core.colors.neutral.200}',
},
},
},

feedback: {
negative: {
background: {
default: '{core.colors.red.100}',
},
text: {
default: '{core.colors.red.900}',
},
border: {
default: '{core.colors.red.500}',
},
},
},
};

Theming

Themes tune core palette values, which core tokens semantic tokens reference, and alternate semantic mappings per mode. Semantic token names never change across themes. A theme becomes more muted, vivid, angular, enterprise, or playful by changing core values and semantic mappings — not by inventing parallel semantic vocabulary.


Validation

Errors (validation must fail when)

  • a semantic color token uses an invalid ux → role combination
  • a semantic color token uses a state outside the allowed state restrictions for that contract
  • any required semantic pairing fails the contrast targets defined below
  • any supported mode fails the same required pairings for the same semantic contract

Warning (validation should warn when)

  • a separately defined state token resolves to the same color as the state it is meant to distinguish
  • a separately defined focused, selected, or current token resolves to the same color as its default state
  • two distinct semantic tokens in the same ux / dimension / state resolve to the same color

Required pairings

Validation must check at least these pairings:

  1. Text pairing

    • *.text.* against the corresponding *.background.*
    • normal text: ≥ 4.5:1
    • large text: ≥ 3:1
  2. Border / non-text pairing

    • *.border.* against the adjacent background it sits on
    • minimum: ≥ 3:1
  3. Focus pairing

    • the focused color against the adjacent background
    • and, when focus distinction depends on color, against the prior unfocused state
  4. Selected/current pairing

    • the selected or current color against the adjacent background
    • and, when distinction depends on color, against the prior state

Color tokens define the semantic contrast contract. Meaning that depends on more than color alone is validated at the pattern, component, and final output layers.