English
Design tokens and theming
@xihan-ui/tokens provides two things: design tokens (one artifact of CSS custom properties) and the theme runtime (which resolves user preferences into attributes on the root element). It sits on the first level of the dependency graph and depends on no other package.
The two token layers
primitive ──► semantic ──► component-private slots
the palette roles with override points of
itself meaning a single componentprimitive: raw values independent of context. They never change in any scope and are declared once.
css
--xh-color-brand-500: oklch(0.632 0.209 277);
--xh-color-neutral-950: oklch(0.145 0.005 258);
--xh-space-4: 16px;
--xh-radius-seed: 8px;Colors use oklch instead of hex / hsl: different hues at the same lightness look equally bright, so dark inversion and contrast adjustments need no color-by-color hand tuning.
semantic: roles with meaning, pointing at primitives. Skins may only consume this layer.
css
--xh-bg-canvas: var(--xh-color-neutral-0);
--xh-bg-surface: var(--xh-color-neutral-0);
--xh-bg-brand: var(--xh-color-brand-600);
--xh-fg-default: var(--xh-color-neutral-900);
--xh-fg-muted: var(--xh-color-neutral-600);
--xh-border-subtle: color-mix(in oklab, var(--xh-ink) 4.7%, transparent);
--xh-control-h-md: 32px;
--xh-shape-control: var(--xh-radius-md);
--xh-elevation-floating: var(--xh-shadow-md);
--xh-motion-duration-enter: var(--xh-duration-normal);
--xh-layer-modal: var(--xh-z-modal);
--xh-overlay-max-w: 20rem;The semantic layer is grouped by role: bg-* backgrounds, fg-* foregrounds, border-* strokes, control-* control sizes, shape-* shapes, ring-* focus rings, elevation-* elevation, motion-* durations and easings, layer-* stacking, overlay-* overlay sizes and text-* typography.
The token sources are JSON in DTCG format (packages/design/tokens/tokens/), and the artifacts are generated by a build script; tokens.css / tokens.json / src/generated/tokens.ts are all committed. CI regenerates and compares them, and the build fails when sources and artifacts drift apart.
Shape scale
Corner radii have one semantic scale; components never write their own radius values.
| Token | Value | Use |
|---|---|---|
--xh-shape-inset | 2px | Inner layers embedded in a control: checkbox boxes, candidate rows of in-page lists, clear buttons inside fields, color swatches |
--xh-shape-control | 2px | The control itself: Button, Input, Select Trigger, Toggle, kbd, tooltip, the track of the segmented radio group; square Tags, status tags and Alert bars |
--xh-shape-surface | 4px | Static containers that form a surface: Card, Panel, list containers, the track of Tabs segment, the outlined squares of a pagination |
--xh-shape-overlay | 4px | Overlays taken out of the document flow: Popover, Menu, Dialog, the card preset of Notification, the two page-facing corners of a Drawer (the two edge-hugging corners take no radius, the toast preset takes control); references surface, so changing the surface radius moves overlays along |
--xh-shape-circle | 50% | Perfect circles: avatars, round icon buttons, radio indicators |
--xh-shape-pill | 9999px | Pills: status chips such as Badge (Tags and result marks are square and take control), plus one-dimensional objects such as tracks, indicator bars, handles and scrollbar thumbs |
The shape tokens reference the raw scale --xh-radius-sm / md / lg (2 / 4 / 8px): inset and control take sm, surface takes md, and overlay references surface. Ordinary buttons, fields, cards and overlays do not use pill; Tags and result marks are square. List rows inside anchored overlays and the list rows of Command and Transfer are full-width rows without radius; an inner radius never exceeds the outer radius minus the padding; joined controls drop the radius on the touching sides. Light, dark and compact density do not change the shape identity. A square box takes circle and never fakes a circle with pill.
Press feedback
Discrete action controls (Button, Toggle, icon buttons, pagination buttons, toolbar buttons) share one press timeline:
| Phase | Duration | Easing | Result |
|---|---|---|---|
| Press | --xh-motion-duration-press (150ms) | --xh-motion-ease-press | Background enters active; scale goes from 1 to --xh-motion-scale-press (1 by default, only the face changes) |
| Release | --xh-motion-duration-release (200ms) | --xh-motion-ease-release | Scale returns to 1; background, stroke and text color return to hover / rest over --xh-motion-duration-micro |
By default only the face changes and nothing scales; for a press bounce, set --xh-motion-scale-press back to 0.97 on the root, with no change to the timeline.
Collection items such as Menu Item, Listbox Item, Tree Node and Table Row, and disclosure triggers such as Accordion / Collapsible, follow the same rhythm (150ms to the active face on press, back to hover / rest over micro on release), but only switch the surface: they never scale the whole row, and zero feedback is not allowed. With reduced motion, --xh-motion-scale-press becomes 1 and both durations become 1ms, while the color feedback stays.
Scrolling inside components
Scrollbars come in only two kinds, fixed by the identity of the scrolling surface rather than decided by each component:
| Kind | Applies to | Tokens |
|---|---|---|
| Custom-drawn bar (wired through the Scrollbar component) | content / list / column under the positioner of the Overlay family; small fixed-height lists (Listbox content, Transfer list, time columns, Cascader column) | type defaults to scroll-hover; thickness --xh-scrollbar-thickness-sm (4px) in overlays and --xh-scrollbar-thickness-md (6px) in pages; --xh-scrollbar-track-bg: transparent on the shell |
| Native thin bar | In-page structural containers (Table, Tree, Transfer panels, Virtualizer viewport, Dialog / Drawer / FloatingPanel body, Layout sider / content, SideNav popout, Log / MessageFeed viewports, the calendar year grid, Typography pre) and scroll containers you build yourself | The reset layer :where([data-scope][data-part], [data-xh-scroll]) sets scrollbar-width: thin + scrollbar-color: var(--xh-fg-scrollbar-thumb) var(--xh-bg-scrollbar-track) for all of them |
The thumb has three color steps: --xh-fg-scrollbar-thumb / -hover / -active are 15% / 25% / 35% of the foreground color, shared by both kinds. Add data-xh-scroll to your own scroll container to get the same thin bar.
Boundary behavior goes by identity too: overscroll-behavior: contain only applies to overlay scrolling surfaces, modal bodies and bottom-pinned viewports, while in-page structural containers stay auto; scrollbar-gutter: stable only applies to containers whose content height changes dynamically (Log, MessageFeed, Dialog / Drawer body) and is guarded by :not([data-xh-scrollbar]); edge fading is only provided by the fade variant of ScrollArea and by Marquee. The page scrollbars of the documentation site use the same type as component scrollbars, without extra overrides.
Typography roles
The text-* typography tokens are picked by role. Hierarchy is expressed by size, weight, line height and spacing together, never by color alone:
| Role | Size / weight / color | Spacing to neighbors |
|---|---|---|
| Field labels (single fields and compound single fields such as Slider, Rating, Signature, Color*) | --xh-text-label-size 14 / --xh-text-label-weight 400 / --xh-fg-muted | --xh-space-2 to the control when stacked; in horizontal Form layout the label column takes 5 / 24 of the row, with --xh-space-4 to the control |
| Group titles (RadioGroup, CheckboxGroup, Listbox, Tree, TagGroup, Descriptions) | 14 / --xh-text-label-weight 400 / --xh-fg-muted | --xh-space-2 to the group |
| Field descriptions / helpers (Field, Fieldset) | --xh-text-caption-size 12 / --xh-fg-subtle / --xh-leading-normal; no extra color change when disabled | Right under the control; the helper row is at least --xh-space-5 high |
| Other descriptions (descriptions inside Card, Alert, Dialog and the like) | --xh-text-secondary-size 13 / --xh-fg-muted / --xh-leading-normal; step descriptions of Steps take the secondary caption 12 / --xh-fg-subtle | — |
| Error messages | 12 / --xh-fg-danger | Share the helper row under the control with the description |
| Titles of overlays and small surfaces (Popover, HoverCard, Popconfirm, FloatingPanel, Approval, EmptyState, Citation) | 14 / --xh-font-weight-medium | — |
| Section and panel titles (Card, Descriptions, Alert, Notification, Steps; Dialog, Drawer, Tour) | heading-3 (--xh-text-heading-3-*): 16 / 500 | — |
| Page titles (PageHeader) | heading-2 (--xh-text-heading-2-*): 20 / 500 | — |
| Large numbers (Statistic, Timer, NumberAnimation) | sm / md / lg take heading-2 / heading-1 / display (--xh-text-display-size): 20 / 24 / 28 | — |
| Secondary marks (counts, shortcuts, timestamps, ordinals) | --xh-text-caption-size 12 | — |
The required asterisk and error messages are common-layer rules: the asterisk --xh-glyph-mark-required sits before the label text, takes --xh-text-caption-size + --xh-fg-danger and is --xh-space-1 away from the text; fields with their own label do not draw a separate one. Field labels and field descriptions do not change color when disabled, other labels take --xh-fg-subtle when disabled, and single-line labels use --xh-leading-none. Icons inside controls follow the size step with --xh-glyph-size-sm / md / lg (16 / 18 / 20); --xh-glyph-size-text (1em, following the text) is only for purely inline text components such as Tag, Kbd, Breadcrumb, Typography and Highlight.
Components do not set a font; font-family is inherited from the host as usual. The library's default sans-serif stack lives in --xh-font-family-sans (system UI fonts first, then PingFang, Hiragino Sans GB, Microsoft YaHei and Noto Sans SC for Chinese) and only reaches the page root when you import the optional entry:
ts
import "@xihan-ui/styles/global.css";This entry is not part of the main @xihan-ui/styles entry. The rule sits in the lowest xihan.reset layer with its selector wrapped in :where(), so any font the host sets still wins. Code and tabular figures use --xh-font-family-mono.
The eight-axis visual environment runtime
VisualEnvironmentController is the single entry for the state and DOM projection of mode, brand, density, dir, contrast, motion, transparency and material. One setPreference synchronously commits all eight axes of a scope:
ts
import { setMotionOverride } from "@xihan-ui/motion";
import {
brandId,
createMotionOverrideSink,
createVisualEnvironmentController,
} from "@xihan-ui/tokens/runtime";
const visual = createVisualEnvironmentController({
root: document.documentElement,
storageKey: "app-visual-environment",
onStorageError: detail => reportStorageFailure(detail),
motionSink: createMotionOverrideSink(setMotionOverride),
initial: {
mode: "system",
brand: brandId("xihan"),
density: "comfortable",
dir: "ltr",
contrast: "system",
motion: "system",
transparency: "system",
material: "standard",
},
});
visual.setPreference({ mode: "dark", density: "compact" });
visual.getState();
visual.subscribe(state => console.log(state));
visual.dispose();Enabling storageKey requires onStorageError as well. Parse failures, private browsing or an exceeded quota are reported explicitly; the controller keeps maintaining its in-memory state but never pretends a failed persistence succeeded.
The motionSink of the root controller is explicit dependency injection: it syncs the resolved motion into Presence, smooth scrolling and JS animations. Only the documentElement root scope may pass it; local controllers only change their own DOM scope and cannot implicitly pollute the global JS override.
The eight-axis state is projected into the full attribute surface that portals can bridge:
html
<html data-theme="dark" data-brand="xihan" data-density="compact" data-contrast="more" data-motion="reduce" data-transparency="reduce" data-material="liquid" dir="rtl">Preference, state and system axes
VisualEnvironmentPreference | VisualEnvironmentState | |
|---|---|---|
| Meaning | The intent submitted by the user or the server | The fully settled facts |
undefined | Clears this layer's override and inherits from the parent controller | Does not occur; all eight axes are non-empty |
'system' | Only valid for mode / contrast / motion / transparency | Does not occur; resolved to a concrete value |
brand, density, dir and material have no platform media query, so they do not accept a fake system. The other four read prefers-color-scheme, prefers-contrast, prefers-reduced-motion and prefers-reduced-transparency. SSR environments without window / matchMedia use light, default contrast, default motion and default transparency, without guessing the client's preferences.
The baseline of the eight axes is light, xihan, comfortable, ltr, default, default, default, standard. contrast='default' is the official value for explicitly restoring normal contrast; the old base is no longer accepted.
| DOM axis | Description |
|---|---|
data-theme | Light and dark each have a full set of semantic values; system follows the system without JS, see below |
data-brand | Switches the brand color: presets work directly, custom brands are registered with registerBrand first |
data-density='compact' | Tightens control heights, padding and gaps without shrinking font sizes or glyphs |
dir | Logical properties drive the LTR / RTL layout; quantities that only know physical directions (translate, gradient angles, the left and right sides of clip-path) are multiplied by --xh-direction-sign, which inherits from the nearest dir (1 for ltr, -1 for rtl), so a subtree set back to ltr flips back; physical coordinates such as transform-origin take --xh-direction-start / --xh-direction-end (the horizontal position of the inline start / end edge: 0% / 100% for ltr, the other way round for rtl) |
data-contrast | more strengthens boundaries; default explicitly returns to the normal level |
data-motion | reduce triggers local CSS motion reduction |
data-transparency | reduce switches translucent materials opened by a theme back to solid recipes |
data-material | liquid turns the navigation-layer parts floating above content (float buttons, media controls, the sticky top bar) into liquid surfaces; standard keeps the original material; the nearest ancestor wins |
Follow the system color scheme without JS
The controller's mode: "system" resolves light or dark only once the script runs. When a page just wants to follow the system without loading a script for it, or needs to be right before the script arrives, write data-theme="system" in the HTML:
html
<html data-theme="system">On a light system it takes the same values as light. When the system switches to dark, the dark token values, color-scheme and the few skin rules that only hold in dark (tones, solid buttons and so on) switch over through prefers-color-scheme, without any script, and follow every later system switch. system is only a value written in the DOM; the controller still projects only light / dark, so a controller created later on the same root replaces the attribute with the resolved value.
Preset brands
@xihan-ui/tokens ships a few named brands. The list is BRAND_PRESETS (such as blue, green, orange, pink) and the type is BrandPreset; BRAND_PRESET_COLORS gives each preset a representative color (its brand 600), for drawing swatches and preset lists. Each preset is a [data-brand='<id>'] value block in tokens.css that only swaps the brand primitives, without registerBrand:
ts
visual.setPreference({ brand: brandId("green") });Each preset takes the hue of the same name from the base palette, and every step is derived from the relative luminance of the same step of the default brand; indigo is the former default brand (hue 258), kept as a preset after the default moved to an indigo at hue 277. Contrasts that involve the brand (text on solid backgrounds, brand text color, the brand tint with text on it, the focus ring) therefore meet the same targets as the default brand; light, dark and both high-contrast levels are checked step by step by a gate. A value block of the same name injected by registerBrand is not layered and wins over the preset.
data-brand only switches the brand when it sits on the same element as data-theme: semantic tokens are declared at the theme boundary, so swapping brand primitives on a child element leaves the semantic tokens at the values resolved outside. The visual environment controller always writes both attributes on its root; when writing them by hand, put data-theme on the same element as well.
Parent scopes, portals and cleanup
Local controllers inherit explicitly through parent. When the parent state changes, a child controller only recomputes the axes without a local preference; setPreference({ motion: undefined }) removes this layer's override and restores inheritance immediately. dispose unsubscribes from the parent scope and the system media queries, and restores the eight attributes of the root element exactly as they were before it took over.
ts
const page = createVisualEnvironmentController({
root: document.documentElement,
initial: { mode: "dark", motion: "system" },
});
const editor = createVisualEnvironmentController({
root: document.querySelector("#editor")!,
parent: page,
initial: { density: "compact", transparency: "reduce" },
});React's XhConfigProvider and Vue's provideXhConfig accept controller options with an explicit root through config.visualEnvironment; nested providers pick up the parent controller automatically. The Web Components <xh-config> is a local scope by itself and can set mode, brand, density, direction, contrast, motion, transparency and material directly. Physical portals are still bridged with the resolved eight axes by Core's VisualBridge; the adapters keep no visual state of their own.
The five-axis legacy view
createThemeController, resolveTheme and applyThemeAttrs still exist, but they are only an explicit five-axis view of the new controller: they have no resolver, media query or persistence of their own. New code should use the eight-axis controller; a standalone applyThemeAttrs only writes five axes, while the controller always maintains the full eight-axis DOM contract.
The color-scheme declaration is written into the dark value block, so native controls inside nested dark regions follow along. SSR should output the eight resolved attributes on first paint to avoid a flash before the client takes over.
Material recipes
There are five token recipes, each providing the same nine tokens: bg, backdrop, border, highlight, shadow, separator, fg, fg-muted and focus-surface; M5 liquid has five extra tokens of its own. By default every recipe is a solid surface; translucency (a translucent background plus a backdrop filter) may only be turned on by a theme for transient overlays. There is no glass material and no compatibility alias. Two more stacking levels, raised and floating, are made of elevation tokens: they have no --xh-material-* tokens and are always written as a solid stroke + solid background + the matching elevation.
| Id | Tokens | Use | Optics |
|---|---|---|---|
| M0 solid | --xh-material-solid-* | Default for static content surfaces: border-default stroke + surface background + no shadow; fields at rest are stroked the same way on --xh-bg-field (white by default) | Solid background, no highlight, no shadow |
| M1 soft | --xh-material-soft-* | Secondary surfaces with a soft tint; not for Card or fields. Button and Tag are flat surfaces and do not take its top light or shadow | Solid background, a subtle top light and a two-step contact shadow, no backdrop blur |
| M2 frosted | --xh-material-frosted-* | Anchored transient overlays such as short lists, menus, tooltips and bubbles; anchored panels with grids or several columns use solid + border-default + --xh-elevation-floating instead | Solid surface background, no backdrop blur, 1px border-default stroke, a medium (md) shadow, no top light |
| M4 elevated | --xh-material-elevated-* | Modal and strong-feedback surfaces (sheets) such as Dialog, Drawer, Command, Tour and Notification; always with a 1px stroke | Solid surface background, no backdrop blur, a large (lg) shadow |
| M5 liquid | --xh-material-liquid-* | Only under data-material="liquid": the navigation layer floating above content — float buttons (FloatButton, BackTop, the scroll-to-bottom of MessageFeed / Log), media controls (Carousel controls and pagination bar, the ImageViewer control layer), the sticky Layout header | Readable floor of 0.48 (light) / 0.61 (dark) opacity, 8px blur, 140% saturation, an ink hairline plus a 1px rim light, edge refraction in Chromium |
| raised (stacking level) | solid stroke + solid background + --xh-elevation-raised | Card and parts that can be lifted or dragged (the slider of Tabs segment, switch thumbs and the like), registered per part; the stroke must be there and the shadow is only a bonus; hover lifting only when interactive | Solid background; --xh-elevation-raised takes the small (sm) shadow |
| floating (stacking level) | solid background + --xh-border-default + --xh-elevation-floating | Anchored panels with grids or several columns: NavigationMenu content, Date / Time / DateRange / TimeRange picker content | Solid background, no translucency, a medium (md) shadow |
| Suffix | Use |
|---|---|
bg / backdrop | Background color / backdrop filter |
border / highlight / shadow | Outer boundary, top highlight and shadow |
separator | Inner divider lines |
fg / fg-muted | Body and secondary text, always opaque |
focus-surface | A solid isolation layer laid inside the focus ring on keyboard focus |
M2 frosted has a single implementation in the library: the material family recipe @xihan-ui/styles/material.css. Components project data-xh-material="frosted" onto a part; the recipe paints the stroke, background, foreground and shadow of the content surface of anchored overlays, while the backdrop filter and the 1px top light are empty by default and left to themes that want translucency. The surfaces of float buttons belong to Action Control, and the recipe gives them the interaction steps (tint one and two on hover and press, focus-surface on keyboard focus). A custom overlay surface joins by writing the same attribute and overrides with --xh-frosted-bg, --xh-frosted-border, --xh-frosted-fg, --xh-frosted-shadow, --xh-frosted-backdrop and --xh-frosted-highlight.
Card has only three forms: outline (default, solid stroke + surface background + --xh-elevation-raised, the small shadow), subtle (--xh-bg-subtle + a transparent placeholder border + no shadow) and ghost (no border, background or shadow; only dividers are allowed). Boundaries are drawn only by the stroke; neither shadows nor tints serve as boundaries.
The environment axes degrade in place under the same token names: high contrast strengthens boundaries; reduced transparency and print force solid backgrounds and turn off backdrop filters (even when a theme turned translucency on); forced colors express everything through Canvas / CanvasText. All four levels are verified in both the light and the dark theme against black, white, mid gray, the page and the brand background: body text at least 4.5:1 and the focus ring at least 3:1 against its isolation layer.
Theme and contrast can be declared on different levels: each level takes the nearest data-theme and data-contrast. A child theme does not cancel the high contrast of an ancestor, data-contrast="default" restores the normal level locally, and sibling regions do not affect each other.
Where frosted overlay surfaces are used
- The name stays frosted, but the default value is a solid popup layer: it does not sample the content behind it and has no top boundary light; a 1px stroke carries the visible boundary, and layering never relies on the shadow alone.
- Only for anchored transient overlays; never for body text, the main part of forms, Card, Table, Notification or the main reading surface of Dialog.
- When a theme wants frosted translucency, give
--xh-material-frosted-bga translucent background, give backdrop a blur (no more than 16px, saturate no more than 1.08) and give highlight an inner top light of at most 1px. Once translucency is on, do not putbackdrop-filteron the page root, large scrolling areas or overlapping layers, do not animate the blur radius, and pair the focus ring with the solid isolation surface offocus-surface.
The compact recipe (Tooltip)
Tooltip is not inverted: it uses the same overlay face as Popover, taking its background, text and edge from the frosted material (M2) and combining three compact recipes. Only when tone is set does it switch to a solid tone fill, and then a 20% mix of the on-color carries the 1px visible boundary (a neutral stroke is invisible on a colored fill):
| Recipe | Normal value |
|---|---|
--xh-material-frosted-compact-alpha | Opaque (--xh-alpha-opaque) |
--xh-material-frosted-compact-backdrop | none, no blur |
--xh-material-frosted-compact-shadow | The medium (md) shadow (--xh-shadow-md in light, the same three dark layers in dark) |
High contrast, forced colors and print remove the shadow; reduced transparency keeps a small shadow to show the overlay's position. Components consume the optical parameters only through these three recipes and never pick alpha or blur primitives directly.
Liquid material
data-material is the application-level material axis, standard by default. With liquid, the navigation-layer parts floating above content turn into liquid surfaces. It can sit on any ancestor and the nearest one wins; the portal visual bridge carries it to the instance shell; under standard the same parts keep their original material. When you use the visual environment controller, set it through its material preference (the controller always maintains this attribute on the root).
The current liquid parts: the FloatButton trigger and its expanded actions, BackTop, the scroll-to-bottom of MessageFeed / Log, the page and play buttons and the pagination bar of Carousel (resting on a liquid capsule), the toolbar, counter, page and close buttons of ImageViewer, and the header of a fixed Layout. Transient overlays, modals, Notification, Card, Table, forms and body containers never use liquid, which the check-material-scope gate enforces.
html
<html data-material="liquid">
<!-- For images, videos, canvases and other areas whose colors cannot be read, the author declares how light the layer below is -->
<section data-xh-backdrop="dark" data-xh-backdrop-busy>…</section>
</html>The author only writes this one attribute. Once the components are mounted, the liquid surface of @xihan-ui/core/visual-environment takes over; the parts of one document share one set of listeners, with nothing extra to install or call:
- The tint follows the computed color of the layer under the part and the author's declaration: a light layer below gets the light tint, a dark one the dark tint, and the part becomes a black-ink or white-ink zone accordingly (relative luminance 0.179 ± 0.04 with hysteresis, so content scrolling across the boundary does not flicker).
- When the layer below is uniform and its tint is known, the surface switches to the clear level (0.24 light / 0.34 dark); when the layer below has text, gradients, images, videos, canvases or cannot be read, it stays at the readable floor, and labels are at least 4.5:1 on any layer below. Content set
inerton the page (absent from the hit stack) is treated as unreadable too. - The layer below is re-read on scroll, window resize, image load, the end of transitions or animations, and when state-machine-driven translations settle (carousel paging, image viewer inertia).
- With a fine pointer, the 1px rim light turns toward the pointer; with a coarse pointer, no pointer or reduced motion it stays fixed at the top left (top right in RTL).
- In Chromium the layer below is refracted within 18px of the edge; in other engines, above 640 × 120, or with more than 3 in one viewport, it only blurs without refraction.
| Extra token | Use |
|---|---|
--xh-material-liquid-tint | Tint of the clear level |
--xh-material-liquid-alpha-clear / -alpha-floor | Opacity of the clear level and of the readable floor |
--xh-material-liquid-rim-far | The weak bright edge on the side away from the light |
--xh-material-liquid-bezel | Width of the refraction band |
Server rendering and the state before mounting output the static form: the tint follows the theme and the opacity takes the readable floor. With reduced transparency and in print the opacity is 1 without backdrop filters or refraction; high contrast uses opacity 1, and forced colors express it through system colors.
Using the tokens directly
ts
import type { TokenName } from "@xihan-ui/tokens";
// Typed token names on the TypeScript side
import { tokens } from "@xihan-ui/tokens";
// The machine-readable artifact: for Figma variables, a Tailwind theme or design annotations
import tokens from "@xihan-ui/tokens/tokens.json" with { type: "json" };Related
- Installation: the import order of tokens and skins
- Accessibility and keyboard: the focus ring contract
- Skin layering, in Chinese: 皮肤与样式分层
