Design tokens
Astryx 0.7.1: Design tokens.
Color
Semantic color tokens for surfaces, text, icons, borders, and status indicators.
Overview
Colors are semantic: tokens describe purpose, not appearance. Every color adapts automatically between light and dark modes via CSS light-dark(). Themes override the resolved values, so your code never references raw hex colors.
Color Tokens
Layered surface hierarchy: body → surface → card → popover. Each level sits visually above the previous one.
{
"type": "token-ref",
"topic": "tokens",
"section": "Color Tokens"
}
Use color tokens in StyleX
Import the typed color tokens and use them in stylex.create(); they resolve to the active theme and color mode.
import * as stylex from '@stylexjs/stylex';
import {colorVars} from '@astryxdesign/core/theme/tokens.stylex';
const styles = stylex.create({
container: {
backgroundColor: colorVars['--color-background-surface'],
color: colorVars['--color-text-primary'],
borderColor: colorVars['--color-border'],
},
accent: {
color: colorVars['--color-text-accent'],
},
});
Best Practices
-
Use semantic tokens (--color-text-primary) instead of raw hex values.
-
Rely on the surface hierarchy (body → surface → card → popover) for layering.
-
Use status colors (success, error, warning) only for their semantic meaning.
-
Hardcode hex values, since they won't adapt to dark mode or custom themes.
-
Mix accent colors with status colors in the same context.
-
Use --color-on-accent on non-accent backgrounds.
Spacing
Spacing scale tokens for padding, gap, and margin: the rhythmic foundation of design system layouts.
Overview
The design system uses a 4px base-unit spacing scale, written in rem so gaps and padding grow with the reader's browser font size the way text does: each step is 0.25rem, which is 4px at the default 16px root. Component gap props accept step values that map to these tokens. The scale provides fine-grained control at the small end (2px, 4px, 6px) and consistent rhythm at larger sizes (multiples of 4px).
Scale
Fifteen spacing tokens, from 0 to 3rem (0px to 48px at the default 16px root).
{
"type": "token-ref",
"topic": "tokens",
"section": "Spacing Tokens"
}
Usage
Stack, HStack, VStack, Grid, Toolbar, OverflowList, and ChatMessageList take a gap step: 0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, or 10 (×4px). Carousel takes the steps from 0 to 4. For custom layouts, use the spacing tokens in StyleX.
// Via component props (preferred)
<Stack gap={4}>{/* 16px gap */}</Stack>
// Via StyleX tokens (custom layouts)
import {spacingVars} from '@astryxdesign/core/theme/tokens.stylex';
const styles = stylex.create({
custom: {
padding: spacingVars['--spacing-4'],
gap: spacingVars['--spacing-3'],
},
});
Best Practices
-
Use component gap props when available; they handle automatic spacing compensation.
-
Stick to the scale for consistency. If a value isn't on the scale, reconsider the design.
-
Use smaller steps (0.5–2) for tight internal spacing and larger steps (4–8) for section gaps.
-
Use arbitrary pixel values outside the scale.
-
Mix spacing tokens with raw px/rem values in the same component.
Shape
Border radius tokens for consistent component rounding, from sharp corners to full pills.
Overview
The radius scale uses a semantic naming system: inner → element → container → page. Each level fits a specific context. Themes can multiply the entire scale via a radius multiplier, with --radius-none and --radius-full as fixed anchors.
Radius Scale
Seven radius tokens, from 0px to 9999px.
{
"type": "token-ref",
"topic": "tokens",
"section": "Radius Tokens"
}
Concentric Radius
Inside a padded, rounded container, give nested elements a radius of outer radius minus padding so the curves stay concentric. Card does not do this for your content.
.inner {
border-radius: max(0px, calc(var(--radius-container) - var(--spacing-4)));
}
Best Practices
-
Use --radius-element for interactive controls (buttons, inputs, selectors).
-
Use --radius-container for content containers (cards, panels, dialogs).
-
Use --radius-full for pill shapes (badges, tags, avatar status dots).
-
Use --radius-page for small elements; it's meant for page-level containers.
-
Hardcode radius values; they won't scale with theme radius multipliers.
Elevation
Shadow tokens for visual elevation and inset state rings.
Overview
Elevation tokens create depth through box-shadow. Three levels (low, med, high) establish a visual hierarchy for floating elements. Inset shadows provide focus and selection rings for interactive components.
Elevation Scale
Each level adds stronger offset and spread. Shadow color adapts to dark mode automatically via light-dark().
{
"type": "token-ref",
"topic": "tokens",
"section": "Shadow Tokens"
}
Choosing a level
Pick the level by how far the surface sits from the page, not by how much shadow you want. Elevation encodes stacking order: a higher level means the surface is layered over more of the UI. Use exactly one level per surface, and only raise a surface above none when it actually sits above other content.
| Level | When to use | Examples |
|---|---|---|
| none | The component is flat and embedded in the surface; it is part of the page, not layered above it. This is the default for every surface except ChatComposer. | A Card in a grid, an inline Banner, a standard Button |
| low | The component is in the normal page flow but should read as distinct from the background. Use for emphasis or to separate the component from the surface behind it; the component still sits on the page, it is not floating over other content. | A raised Card that needs emphasis, a ChatComposer |
| med | The component sits over other content on the same page; it floats above nearby elements but not the whole screen. | A Popover, a floating Banner, a floating action Button |
| high | The component is placed over the entire UI; it is the topmost layer and typically has a backdrop or takes focus from everything else. | A modal Dialog, a full-screen overlay surface |
If two surfaces overlap, the one on top takes the higher level. If a surface does not overlap anything, it is none or low; never med or high.
The elevation prop
Configurable surfaces expose a single elevation prop instead of asking consumers to hand-write a box-shadow. It takes the graded enum none | low | med | high, narrowed per component to the steps that surface needs: Card, ClickableCard, SelectableCard, Button, IconButton, ButtonGroup, ToggleButton, and Banner expose the full scale, while ChatComposer exposes only none | low. none is a flat literal (box-shadow: none); the other levels map to the --shadow-* tokens above, so a surface stays theme-agnostic.
Prop defaults preserve current appearance: every surface defaults to none except ChatComposer, which defaults to low to keep its raised look. Set elevation="none" to flatten it; the flat composer draws a border with the same rest / hover / focus treatment as a text input.
import {Plus} from 'lucide-react';
// Flat by default; raise only when the surface needs to float.
<Card elevation="low">Raised card</Card>
// A floating action button.
<IconButton icon={<Icon icon={Plus} />} label="New" variant="primary" elevation="med" />
// Flatten the composer (defaults to 'low').
<ChatComposer elevation="none" onSubmit={handleSubmit} />
Always float; no prop: intrinsic overlays (Dialog, Popover, Tooltip, Toast, HoverCard, DropdownMenu, and the components that compose them) bake their elevation in and never expose the prop. Flow content (inputs, Text, layout) is never elevated; input fields use inset rings, which are a separate concept from elevation.
Usage
When building a custom surface, read elevation from the token scale rather than a hand-rolled shadow; the same tokens the elevation prop resolves to.
import {shadowVars} from '@astryxdesign/core/theme/tokens.stylex';
const styles = stylex.create({
dropdown: {
boxShadow: shadowVars['--shadow-med'],
},
dialog: {
boxShadow: shadowVars['--shadow-high'],
},
inputFocused: {
boxShadow: shadowVars['--shadow-inset-selected'],
},
});
Best Practices
-
Reach for the
elevationprop on a configurable surface before writing any custom shadow. -
Choose the level by how far the surface sits from the page:
nonewhen flat/embedded,lowwhen in-flow but distinct,medwhen over page content,highwhen over the whole UI. See "Choosing a level". -
Use inset shadows for input focus/selection states; they compose better than outlines.
-
Hand-write a
box-shadowin app code; set theelevationprop or readshadowVarsinstead. -
Raise a surface that does not sit above other content; a non-overlapping surface is
noneorlow, nevermedorhigh. -
Stack multiple elevation levels on the same element.
-
Use elevation shadows for decorative borders. Use --color-border tokens instead.
Motion
Duration and easing tokens for animations and transitions.
Overview
Motion serves two purposes: it makes interfaces easier to understand, and it makes them more pleasant to use. A panel animating open helps the eye track what changed and where new content sits in the layout. These moments reduce cognitive load and make the interface feel responsive.
At the same time, well-tuned motion gives an application a sense of craft and polish that users notice, even if they can't name it.
The design system provides duration and easing tokens that keep these moments consistent across your application.
Duration
Nine duration tokens in fast, medium, and slow bands.
{
"type": "token-ref",
"topic": "tokens",
"section": "Duration Tokens"
}
Easing
One easing curve, --ease-standard.
{
"type": "token-ref",
"topic": "tokens",
"section": "Easing Tokens"
}
Where Motion Helps
Some parts of the interface benefit from animation. Panels, dialogs, and collapsible sections are disorienting when they appear instantly, so animation gives the eye something to follow. Toasts and notifications need just enough entrance to be noticed without startling. State changes like a switch flipping or a selection highlighting feel more intentional with a brief transition.
The common thread is that these are moments where the user needs to understand what happened.
Where Motion Hurts
Table row hovers. List item highlights. Anything the user does dozens of times per minute. Adding perceptible duration to these interactions makes the interface feel like it's catching up to the cursor. Keep these fast enough that the user never notices a delay.
Animations that block interaction are worse. If a user has to wait for a panel to finish sliding before they can click something inside it, the animation has become an obstacle. Motion should never stand between the user and their next action.
Movement Principles
- Not everything needs an animated exit. Elements the user is moving away from, such as tooltips, hover cards, and dropdown menus, can disappear instantly. The user has already shifted their attention. Animate the exit only when it helps orient the user, like a panel closing or a dialog dismissing to reveal what's underneath.
- When you do animate exit, match the entrance. A panel that slides in from the right should slide back out to the right.
- Direction should match the action. Navigating deeper into content should feel like moving forward. Going back should feel like returning. This keeps the user oriented in the structure of the application.
- Contextual UI should feel connected to its trigger. A dropdown should expand from the button that opened it. A popover should appear near the element it describes. This doesn't apply to global UI like command palettes or toasts, which have their own fixed positions.
Respecting User Preferences
Some users experience motion sensitivity; animation that feels polished to one person can cause discomfort for another. Astryx components already switch to instant changes when the OS reduced-motion setting is on. Do the same in your own animations with @media (prefers-reduced-motion: reduce).
Animate with motion tokens
Set transition durations and easing from the motion tokens, so a theme that retunes motion retunes your animations too.
import {durationVars, easeVars} from '@astryxdesign/core/theme/tokens.stylex';
const styles = stylex.create({
fadeIn: {
transitionProperty: 'opacity',
transitionDuration: durationVars['--duration-fast'],
transitionTimingFunction: easeVars['--ease-standard'],
},
slideUp: {
transitionProperty: 'transform, opacity',
transitionDuration: durationVars['--duration-medium'],
transitionTimingFunction: easeVars['--ease-standard'],
},
});
Best Practices
-
Animate transitions that involve spatial change: panels opening, content expanding, elements entering the screen.
-
Use fast tokens for small, frequent interactions. Use medium tokens for larger transitions that rearrange the layout.
-
Honor reduced motion preferences at the OS level.
-
Let hover states or high-frequency interactions feel like they're lagging behind the user.
-
Let animation delay when a user can interact with new content. The transition should complete before (or not block) the next action.
-
Move elements in ways that contradict where they came from or where they're going.
Pinned install:
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0