Skip to content

System Architecture

Cindy Zhang edited this page Jun 23, 2026 · 1 revision

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.


What Astryx Is

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.


Builder Jobs

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

Escalation Path

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.


Customization

From least effort to most control. Each level gives more power at the cost of more responsibility.

Props (use components as designed)

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.

Theme tokens (change colors, spacing, radii)

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.

Theme component overrides (change specific component styles)

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.

className + CSS (target stable class names directly)

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.

Swizzle (eject and own the component source)

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.tsx

You get the real implementation. You own it. It doesn't auto-update.

Summary

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 ⚠️ Manual
Swizzle npx xds swizzle Full source, yours to own ❌ Ejected

Token System

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.

Container Padding Variables

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.


Internal Styling

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.


AI-Native Design

Astryx treats AI compatibility as a first-class architectural concern, not an afterthought. Three mechanisms keep the system AI-friendly:

CLI as the LLM Interface

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 code

The 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 as Quality Signal

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

API Design for Constraints

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.


Packages

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.


Related

Clone this wiki locally