-
Notifications
You must be signed in to change notification settings - Fork 27
System Architecture
Astryx is a design system for building internal tools and products. It serves three audiences: product builders constructing UIs, theme authors defining visual identity, and AI assistants generating code. This page describes the system as it exists today.
A component library that ships pre-compiled CSS and typed React components. Consumers import a CSS file and use props — no build plugin needed, no styling library required. Internally, Astryx uses StyleX for authoring (see Why StyleX), but that's an implementation detail consumers don't interact with.
import '@xds/core/xds.css';
import { Button, Stack, Card } from '@xds/core';
<Stack gap="md">
<Card>
<Button variant="primary">Save</Button>
</Card>
</Stack>90+ components. Four published packages. See Distribution for details on what ships and how.
Astryx serves developers across three jobs. Each job has a different interface and a different level of control.
| Job | What the Builder Wants | Astryx Interface |
|---|---|---|
| Construct pages | "Use components to build UIs" | Component API — typed props, composition |
| Set visual style | "Define my project's look and feel" |
defineTheme() — CSS custom properties |
| Customize components | "Change something beyond what the theme offers" |
className, stable class names, swizzle |
Props (use a component as designed)
↓ need different colors/spacing?
Theme tokens (defineTheme with token overrides)
↓ need different component-level styles?
Theme component overrides (defineTheme with components config)
↓ need different structure or behavior?
className + CSS (target stable class names)
↓ need full control?
Swizzle (eject the component source, own it)
Each step gives more control at the cost of more responsibility. Most builders never go past theme tokens.
From least effort to most control. Each level gives more power at the cost of more responsibility.
Props define intent, not style. Components expose constrained options — variants, sizes, slots — and handle styling internally.
<Button variant="primary" size="md">Save</Button>
<TextInput label="Name" value={name} onChange={setName} />
<Selector items={items} value={v} onChange={setV} label="Pick">
{(item) => <CustomItem label={item.label} />}
</Selector>Typed props, finite options, consistent patterns across all components. See API Conventions.
Themes are CSS custom property overrides created with defineTheme(). No StyleX knowledge needed.
const myTheme = defineTheme({
name: 'my-brand',
tokens: {
'--color-accent': ['#0077B6', '#48CAE4'], // [light, dark] tuple
'--color-surface': ['#F0F8FF', '#0A1628'],
'--radius-container': '16px',
},
});
<Theme theme={myTheme} mode="system">
<App />
</Theme>Every component references tokens — when the theme changes them, all components respond automatically. Token values can be [light, dark] tuples for automatic mode switching via CSS light-dark().
Themes can be pre-compiled to static CSS with npx xds theme build for zero-runtime theming.
defineTheme() also accepts component-level style overrides targeting stable class names via @scope:
const myTheme = defineTheme({
name: 'my-brand',
tokens: { /* ... */ },
components: {
button: {
base: { fontWeight: '600' },
'variant:secondary': { borderWidth: '1px', borderStyle: 'solid' },
},
badge: {
'variant:ghost': { border: '1px solid var(--color-divider)' },
},
},
});The key syntax (component, variant:value, variant:value+prop:value) compiles to scoped CSS selectors targeting stable class names.
Every component renders stable, predictable class names (astryx-button, astryx-card) with variant values as additional classes. Every component accepts className for consumer overrides.
<button class="astryx-button primary md ...">Save</button><Button variant="primary" className="my-override">Save</Button>Consumers can use Tailwind, CSS modules, plain CSS — whatever they prefer. The pre-compiled CSS is wrapped in @layer xds so consumer styles naturally win in specificity.
When you need to change structure or behavior, swizzle ejects the full component source into your project:
npx xds swizzle Button
# → src/components/xds/Button/Button.tsxYou get the real implementation. You own it. It doesn't auto-update.
| Level | Mechanism | What Changes | Stays in Sync |
|---|---|---|---|
| Use as-is | Props | Nothing | ✅ Always |
| Token overrides | defineTheme({ tokens }) |
Colors, spacing, radii, typography | ✅ Always |
| Component overrides | defineTheme({ components }) |
Per-component variant styles | ✅ Theme-scoped |
| CSS overrides |
className targeting stable classes |
Anything CSS can reach | |
| Swizzle | npx xds swizzle |
Full source, yours to own | ❌ Ejected |
Tokens are CSS custom properties that components reference for all visual values. Themes override token values; components respond automatically.
| Category | Purpose | Examples |
|---|---|---|
--color-* |
Semantic colors, text, icons, status, overlays, dividers |
accent, surface, textPrimary, hoverOverlay, negative
|
--spacing-* |
Consistent spacing scale |
space0 (0px), space1 (4px), space2 (8px), space4 (16px) |
--radius-* |
Border radius for different contexts |
rounded, container, element, content
|
--elevation-* |
Box shadows |
base, thumb, dialog, hover, menu
|
--transition-* |
Animation durations |
fast (0.15s), normal (0.2s) |
--typography-* |
Font families |
fontFamilyBody, fontFamilyCode, fontFamilyHeading
|
--size-* |
Component sizing |
sm (18px), md (26px), lg (36px) |
--text-size-*, --line-height-*, --font-weight-*
|
Typography scale | Various sizes and weights |
Components must use tokens — never hardcoded values. This is enforced at compile time internally by StyleX's type system (see Why StyleX). When a designer says "make the accent blue darker," you change one token value and every component updates.
Beyond design tokens, containers (Card, Section, Layout areas) set CSS custom properties that communicate their padding to children. This enables automatic edge-to-edge bleed — Table, Divider, and Section read these variables and apply negative margins to escape container padding.
Two directional variables: --container-padding-inline (horizontal) and --container-padding-block (vertical). See Container Padding System for the full reference.
Components are authored with StyleX internally. This gives the system team compile-time token enforcement, atomic CSS output, private internals (class names are opaque and can change freely), and type-safe variants. Consumers never see this layer — they interact through props, className, and CSS custom properties. See Why StyleX for the full rationale.
Astryx treats AI compatibility as a first-class architectural concern, not an afterthought. Three mechanisms keep the system AI-friendly:
The CLI (npx xds) is how AI assistants discover and learn components. Instead of bundling massive doc files into context, agents retrieve what they need on demand:
npx xds component --list # All components by category
npx xds component Button # Full docs for Button
npx xds component Button --compact # Token-optimized for LLMs
npx xds component --brief-all # All components, ~250 chars each
npx xds component Button --source # Read the source codeThe agent-docs command injects a compressed component index into project AGENTS.md / CLAUDE.md files, directing agents to use CLI retrieval rather than prior knowledge. This means agents discover components through browsing, not hallucination.
Key insight: Docs discoverability matters more than doc completeness. Adding "browse the catalog first" to agent instructions improved component usage from 8→11 and reduced raw styling fallback significantly.
Vibe Tests are structured evaluations that measure how well an LLM can use Astryx to build real UIs. They're both a quality metric and a design tool.
As a quality metric: Each run generates scores across five dimensions (correctness, accessibility, code quality, efficiency, maintainability) and compares Astryx against baseline targets (shadcn/Tailwind, raw HTML). This catches regressions — if an API change makes LLMs less effective, the data shows it.
As a design tool: When debating API options, stop arguing and test both. This is how Accordion became CollapsibleGroup (discoverability jumped from 3.5/5 to 4.7/5) and how we discovered that className was a better override mechanism than xstyle for AI-generated code.
Key metrics:
- Decisions/element — how many styling decisions the LLM makes per UI element (Astryx ~1.7 vs baseline ~2.3)
- Semantic ratio — percentage of styling using tokens vs raw values
- Escape hatches — where and why the LLM drops out of the design system
The component API is designed so that valid usage is easy and invalid usage is hard:
| Design Decision | Why It Helps AI |
|---|---|
| Typed props with finite options |
variant="primary" has 4 choices, not infinite CSS classes |
Required label on interactive elements |
Forces accessible code by default |
| Consistent patterns across components | Learn once, apply everywhere |
| No arbitrary styling values | Can't generate off-token colors or spacing |
| Constrained composition | Slots and children, not arbitrary nesting |
The gap report system (npx xds gap-report) provides a structured way for agents (and humans) to report when the component library doesn't cover a use case — turning gaps into signals rather than hallucinations.
| Package | Purpose |
|---|---|
@xds/core |
Components, hooks, utilities, tokens, pre-compiled CSS |
@xds/cli |
CLI tooling (xds component, xds theme build, etc.) |
@xds/theme-default |
Default theme |
@xds/theme-neutral |
Neutral theme variant |
All versioned together. See Distribution for what's in each bundle, the build pipeline, and consumer setup.
- Distribution — Packages, versioning, source and dist bundles
- Why StyleX — Why StyleX is the right internal authoring tool
- API Conventions — Naming, props, composition, styling conventions
- Component Authoring Guide — File structure, patterns, tokens (for contributors)
- Component Lifecycle — End-to-end guide for adding components
- Vibe Tests — Evaluation methodology and results
- Contributing with AI Assistants — Contribution workflow