Skip to content

AcceptedOwner sparkles:uiReviewed 2026-09-21

Sparkles design system — Specification (TOK / ACC / FMT) ​

Abstract ​

sparkles:ui expresses a design system as data: one value that names every visual decision an application makes, from colors and lengths to glyph choices and font roles, and projects it onto a terminal, a GPU window and a web page by one rule set. This specification defines the core of that framework. Widgets name slots, the roles they play, rather than colors. A slot's appearance may vary with interaction states such as hover or disabled. A slot a theme leaves unset falls back to a documented default, and a state it leaves unset to the slot's resting appearance. Layout lengths are whole character cells, while finer metrics carry a declared fallback for targets that cannot draw them. Every theme is measured against contrast floors and stored in the Design Tokens Community Group format.

Introduction ​

The sparkles:ui toolkit renders one widget tree to a terminal's character grid, to a GPU window and to static HTML, as the backends specification describes. The applications built on it and the project's documentation sites should share one visual language, while a user of an application such as hue still switches at runtime among dozens of editor color schemes borrowed from upstream. A design system, in the sense of web practice, is what makes both possible: a vocabulary of named decisions, such as "primary text", "focus ring" or "overlay padding", whose values a theme supplies.

A palette of named colors is not yet a design system. It cannot say what a hovered or disabled control looks like without a separate color per state. It does not say which roles a component may use, so restyling one component can silently restyle another. It sets no floor on legibility and has no file format that a user or a web tool can edit. The three targets compound each gap: a corner radius or a hairline border exists in a window and in CSS but has no direct form on a character grid. Parity between targets is worth little if the design language must be authored separately for each.

The framework makes the design language one value. A widget names a slot, the role it plays, instead of a color. Each slot is a design token on one of three tiers: raw values, semantic roles, and parts of a single component, each tier aliasing the one below. A semantic role a theme leaves unset falls back to a documented default. Resolving a slot also takes the active interaction states, and a state the theme does not override leaves the resting appearance unchanged, so every theme resolves every slot in every state. Layout lengths are whole cells. A metric finer than a cell, such as a corner radius, is given in device pixels together with a defined projection onto a target that cannot draw it, so no metric is silently dropped.

Each component declares the slots it uses, and a test holds it to that set. Every theme is measured against WCAG contrast floors. A failure is a test failure for the Sparkles theme and for any theme that declares it must conform. A borrowed scheme that fails is labelled rather than rejected, so it keeps its upstream colors. Themes are stored as Design Tokens Community Group (DTCG) documents, which sparkles:wired reads and writes.

This page specifies the tokens, the accessibility floors and the theme file format: the contract every theme fills. It assigns no token values; the concrete Sparkles theme, the brand the repository's own applications and documentation follow, has its own page. Which features each target supports and how a terminal is probed for them, glyph and typography projection, the shared keyboard bindings, and the CSS mapping for the web are specified by sibling pages of this tree. Syntax highlighting stays outside the token model: sparkles:syntax resolves its rules, and the tokens treat them as opaque. Text measurement and fonts are consumed, not owned: grid widths, width profiles, cell wrapping and scaled cell footprints belong to base/text, font matching and fallback to the font specification, and proportional paragraphs to text layout; the design system owns the font roles that name them. Following the operating system's light or dark preference is out of scope and deferred, as the overview records.

Vocabulary defines the terms the requirements use. Tokens specifies tiers and paths, interaction states, and units and metrics; Accessibility the contrast floors and the rule that color never carries meaning alone; and Theme file format the DTCG mapping. Relationship to THM lists which rows of the toolkit's theme specification these requirements realise. Where a requirement table's "Traces to" column marks a symbol proposed, that symbol does not exist yet. The overview names the owning package of every obligation in this tree, and its sibling pages cover capabilities, glyphs and typography, the keyboard vocabulary and the web. testing.md holds the oracles and evidence, PLAN.md the delivery order, and decisions.md the choices behind each requirement.

Vocabulary ​

TermMeaning
tokenone named design decision with a value: a color, a length, a glyph choice, a font role. Named by a dotted path (text.primary, border.weight)
tierprimitive (a raw value: indigo.500), semantic (a role: text.primary), component (a part of one component: scrollbar.thumb, diff.added)
slota semantic or component token a widget references instead of a color — the existing Slot enum (THM1)
interaction staterest, hover, focused, selected, pressed, disabled — a second axis on slot resolution
visualthe resolved appearance a backend paints (sparkles.ui.style.Visual)
targetone rendering backend instance with a declared capability set (capabilities.md)
projectionthe rule by which a target that cannot honour a token renders it anyway (a px radius becomes rounded box-drawing)
themeone complete assignment of values to tokens: sparkles.ui.theme.Theme

Tokens (TOK) ​

Tiers and paths ​

IDRequirementStatusTraces to
TOK1Every token belongs to exactly one tier. A component token must resolve to a semantic token or a primitive; a semantic token to a primitive or a literal; the alias graph must be acyclic and every alias must resolve. Violation: a theme whose alias graph has a cycle or a dangling reference is rejected at load with the offending path named, never resolved to a default.partialtokens.d TokenTier, tierOf; state aliases read their target's rest only, so they cannot chain (D46); rejecting a cyclic or dangling theme file at load is FMT2 (M5)
TOK2Every slot has exactly one path: segments of [a-z0-9]+ joined by ., the first starting with a letter (so indigo.500 is a valid primitive), unique across the slot enum. The path is declared once, as @WireName data on the Slot member, and resolved by sparkles:wired (wireNames) — the same table the JSON writer and the DTCG theme file use; the CSS custom-property name is derived from that table, never spelled separately. Violation: two slots with the same path, or a path failing the grammar, is a test failure.fullstyle.d Slot (@WireName), tokens.d slotPaths, writeCssName; WEB1
TOK3The semantic vocabulary must contain at least the roles below. A theme may leave a role unset; resolution then follows the documented fallback (the last column), so every role is total for every theme.fullstyle.d Slot (the roles), defaultTwoslashPalette (scheme defaults), theme.d effectivePalette (derived from the page colors + the accent probe)

The minimum semantic set (TOK3):

GroupRolesFallback when unset
textprimary, secondary, muted, disabled, inverseprimary = page fg; the rest are alpha mixes of it toward the page bg
surfacebase, raised, overlay, sunkenbase = page bg; the rest are tone steps per the derivation rule
borderdefault, strong, focusdefault = text.muted; focus = accent.primary
accentprimary, secondaryprobed from the syntax rules (function / markup.link) as today
statuserror, warning, info, success (each fg + bg tint)the existing error/warn/info slots; success = diff-added hue
selectionbgexisting selection
linkfg, underlineaccent.primary; hoverUnderline
focusringborder.focus

Interaction states ​

IDRequirementStatusTraces to
TOK4Slot resolution takes an interaction state set as a second argument: resolve(theme, slot, states) → Visual. It is total: for any slot and any state set, a theme that sets no override for that state yields exactly the rest visual. Violation: a resolved visual that differs from rest when the theme declares no override for any active state.fullstyle.d InteractionState, StateSet, Palette.states/overlay, resolveSlot/resolveVisual (states parameter), Widget.states, display_list.d; colours, per-channel slot aliases and attributes (D46: stateAlias, addComponentStates, test resolveSlot.stateAliasesAndAttributes); the components report states (chrome.d, tree_widget.d, property_view.d)
TOK5When several states are active, the override of the highest-precedence state that has one wins; precedence, low to high, is hover < focused < selected < pressed < disabled. A theme override for a state is sparse: it may set only the fields it changes — colors, attributes and metrics (a pressed button may inset by a cell; a hovered thumb may widen) — and unset fields fall through to rest. A metric override changes layout, so it participates in the frame's relayout like any other model change, never as a paint-time adjustment. Violation: a disabled widget that also reads as hovered.partialstyle.d StateSet.highest; resolveSlot walks the states highest-first per channel (test resolveSlot.statesFallThroughToRest), aliases and attributes included (D46). Metric overrides admitted (D22), not built: no consumer yet
TOK6Every component declares the slots it uses as compile-time data (a slots member), and a test asserts that the display list it builds references no slot outside that set. The component's documentation page lists the slots from the declaration, not from prose. Violation: an op in the component's display list whose slot is not in its declared set.partialtokens.d firstUndeclaredSlot/slotsWithin; chromeSlots, gutterSlots, inspectorSlots, treeWidgetSlots, lanternSlots, propertyViewSlots, treeViewSlots, gridBackdropSlots (over each preset's ops), tableWidgetSlots with their tests. Pending: the twoslash/source-view/hue views; the docs-page listing (M3)

Units and metrics ​

IDRequirementStatusTraces to
TOK7The cell is the universal layout unit: paddings, gaps, widths and heights are integer cells on every target (LAY integer-unit rule). A metric whose meaning is sub-cell — corner radius, border weight, shadow offset/blur, font scale, a type step (GLY10) — is typed in CSS px, which are density-independent: a pixel target multiplies them by its density, so a 16 px radius is as round on a 440 dpi phone as on a desktop. Each carries a defined projection onto a cell target (GLY2). A target declares per px-typed metric whether it honours or projects it; a metric is never silently dropped.partialstyle.d Palette (mixed units, no declaration); RaylibCanvas.densityScaled
TOK8Metrics are named by role, not by the feature that first needed them, and each carries a role-first token path, matching the slots: overlay.radius, overlay.pad.inline/.block, overlay.gap, overlay.width.max/.min, border.width, border.accent.width, docs.width.max, signature.indent, shadow.offset.x/.y, shadow.blur, code.font.scale, docs.font.scale, tag.font.scale, arrow.size — replacing popupRadius, borderWidth, accentBorder, popupPadX/Y, popupMaxWidth/MinWidth, sigIndent. Renames are one-shot with the Palette migration; no alias period.fullstyle.d Palette scalar fields (@WireName paths)
TOK9The syntax channel stays opaque to the token model: syntax rules are (selector, TextStyle) data resolved by sparkles:syntax, exactly as THM states. The token model may reference a syntax rule's foreground (the accent probe) but never resolves selectors.fulltheme.d ruleFgFor
TOK10Application-domain component tokens (diff.*, coverage.*, twoslash.*) live in sparkles:ui's component tier under a domain namespace, because the slot index must stay a closed enum for the display list. Their values belong to the theme; their existence is the toolkit's. Revisit if a third application domain appears (decisions.md D16).fullstyle.d Slot (diff/cov groups)

Accessibility (ACC) ​

Floors are the color-derivation values: WCAG 2.x contrast ratio, computed from sRGB relative luminance.

IDRequirementStatusTraces to
ACC1For every theme, a test computes conformance: text.primary on surface.base ≥ 4.5:1 and every chrome/accent foreground on its band ≥ 3:1, in the theme's own scheme. The result is data (ThemeConformance) rendered in ui-gallery's theme page and the docs, and exposed to a theme picker.not startedtesting.md O2
ACC2A built-in theme that fails the floors is labelled, not rejected: the 36 borrowed schemes keep upstream fidelity. Only Sparkles and any theme that declares conformance: required must pass, and for those a failure is a test failure. Violation: a required theme shipping below floor; or a failing theme rendered without its label where labels are shown.not startedSPK2
ACC3Never color alone. Every status.* token has a paired status mark in the glyph channel with an ASCII fallback (GLY3); a component that shows status must emit the mark whenever the target's color depth is none or ansi16, and may omit it above that only if the theme says so. Violation: a monochrome render of a status row indistinguishable from a neutral row.fullglyphs.d Mark (pairwise distinct per charset, test ui.glyphs.marks.*); tasklist.d always emits the mark
ACC4Focus is visible in monochrome. Under a target with no color, the focused element must differ from its unfocused rendering by attribute (reverse, underline or bold) or glyph, never only by a color token. Checked by the baseline profile render of every ui-gallery page with focus placed on each focusable (O1).fullui-gallery tests ui_gallery.render.focusIsVisibleInMonochrome (page list ⇄ page on every page) and ui_gallery.render.focusMovesVisiblyInsideEveryPage (each page's focusKey walk: an attribute or a symbol glyph, not a word, changes at every step)
ACC5Reduced motion is a capability input (TargetCapabilities.reducedMotion): when set, spinners render their static frame, eased scrolling jumps, and toasts appear without transition. The source is the host (OS preference later via sparkles:appearance; a --reduced-motion flag and env now).not startedcapabilities.md

Theme file format (FMT) ​

The file format is the Design Tokens Community Group's Design Tokens Format Module 2025.10 (DTCG; D12), used both to author user themes and to interchange with web tooling. It realises THM9. The generic format, parsed, resolved and written canonically, is sparkles.ui.dtcg; the mapping onto a theme is sparkles.ui.theme_file. The project's own data lives under the $extensions key dev.petar-kirov.sparkles (D56).

IDRequirementStatusTraces to
FMT1A theme file is a DTCG document. A slot is a group at its token path (TOK2) whose fg and bg leaves are color tokens; an interaction state is a group inside the slot (scrollbar.thumb.hover.bg); page.fg/page.bg are the page colors; px metrics are dimension tokens and cell metrics and font scales number tokens, each at its role-first path (D55). Fonts are font.family.<face> fontFamily stacks and font.<role> aliases of a face (D63); the box chrome maps onto border/shadow tokens once Theme carries it.partialtheme_file exportTheme, loadTheme; tests theme_file.palette.slotsStatesAndMetricsRoundTrip, theme_file.fonts.facesAndRolesRoundTrip. Pending: box chrome
FMT2Aliases resolve in both 2025.10 forms, {a.b} and the $ref JSON Pointer, with $type inherited and group $extends applied. A file may name a base, a built-in theme or another file; the base's tokens come first and the file's after them, the later winning per token, the DTCG resolver rule. A file with no base stands alone, its unset slots derived from its page colors (D57). A state channel that aliases another slot's same channel is a D46 alias. Unresolvable aliases and cycles reject the file (TOK1).fulldtcg collectTokens, DtcgTokens.resolved; theme_file loadTheme; tests theme_file.load.overlaysABase, dtcg.collectTokens.rejectsCyclesAndDanglingAliases
FMT3Loading must fail closed on malformed input: invalid JSON, a wrong $type on a token the theme maps, a dangling or cyclic alias, or an out-of-range value yields a DtcgError naming the JSON path. Unknown $extensions and tokens the theme does not map are kept for the round trip; the latter are reported as warnings. Malformed input must never become an assertion.fulldtcg DtcgError; ThemeFile.warnings; test theme_file.load.malformedCorpusFailsWithAPath (O4)
FMT4Loading keeps the file's own document, so save(load(file)) writes the file's primitives, aliases, descriptions and unknown extensions back, in canonical form: keys sorted, two-space indentation (D58). Colors and dimensions written by the toolkit use the 2025.10 object forms; a draft #rrggbb or 4px value the file wrote is kept as written.fulldtcg writeDtcg; theme_file saveTheme; tests theme_file.builtins.roundTrip, theme_file.load.unknownExtensionsSurviveASave
FMT5Every built-in theme is exported to DTCG and the exports are checked in as goldens, so the format is exercised by real documents and a change to any built-in is visible in review as a token diff. An export writes only what a theme sets, so a theme with a derived palette exports without one and loads back to the same palette.fulltheme_file exportTheme; the 36 exports in libs/ui/test/data/themes/ and test theme_file.builtins.exportsMatchTheCheckedInGoldens (SPARKLES_UPDATE_GOLDENS=1 rewrites them); hue theme <name> --export
FMT6Syntax rules are groups at syntax.<selector> with the slots' fg/bg leaves and their attributes and underline in the group's extension, so a rule that sets only attributes is a group with no color token (D14).fulltheme_file exportTheme, loadTheme; test theme_file.load.attrsOnlyRulesAndDraftHex

Relationship to THM ​

THM rowRealised by
THM2TOK3 (the minimum semantic set) and TOK4/TOK5 (the state axis)
THM6TOK7–TOK10 (metrics and domain tokens in the one value)
THM8CAP4 (per-target application, never a tty snapshot)
THM9FMT1–FMT6

→ Overview · Capabilities · Glyphs · Testing