Skip to content

Why StyleX

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

Why StyleX

Astryx uses StyleX internally for all component styling. Consumers don't need StyleX — they use pre-compiled CSS and can style with whatever they want.

This page explains why StyleX is the right internal authoring tool, how the consumer experience works without it, and what the tradeoffs are.


The Short Version

StyleX gives the Astryx team compile-time enforcement, atomic CSS output, and the ability to keep the majority of internal styles private. Consumers never see StyleX — they import a CSS file, use typed props, and override with className using Tailwind, CSS modules, plain CSS, or whatever they prefer.

┌──────────────────────────────────────────────────────────┐
│  WHAT CONSUMERS SEE                                       │
│                                                           │
│  import '@xds/core/xds.css';                              │
│  <Button variant="primary" className="mt-4">Save</Button>  │
│                                                           │
│  • Pre-compiled CSS — no build plugin needed              │
│  • className for overrides — any styling library works    │
│  • CSS custom properties for theming                      │
│  • Stable class names (astryx-button, astryx-card) for selectors│
├──────────────────────────────────────────────────────────┤
│  WHAT Astryx AUTHORS SEE                                     │
│                                                           │
│  stylex.create({ base: { color: colorTokens.textPrimary } })     │
│                                                           │
│  • Compile-time token enforcement                         │
│  • Type-safe variants and themes                          │
│  • Atomic CSS deduplication                               │
│  • Internal styles stay internal                          │
└──────────────────────────────────────────────────────────┘

Why StyleX for Authoring

1. Compile-Time Constraint Enforcement

StyleX uses TypeScript to enforce that only valid tokens are used. If a token doesn't exist, the code doesn't compile.

// StyleX: invalid tokens are compile errors
import { colorTokens } from './tokens.stylex';
const styles = stylex.create({
  bad: { color: colorTokens.doesNotExist }, // ← TypeScript error
});

This matters for a design system because constraints are the product. Every component must use the token system — not because we wrote it in a doc somewhere, but because the compiler enforces it. Raw hex values, arbitrary pixel values, and off-scale spacing can't sneak in.

2. Internal Styles Stay Internal

StyleX generates atomic, deterministic class names that are implementation details. Consumers interact with components through typed props and className, not by inspecting and depending on internal CSS classes.

This means Astryx can refactor its internal styling freely — reorganize token references, change how hover states layer, restructure the cascade — without breaking consumers. The internal implementation is genuinely private.

3. Type-Safe Token System

StyleX tokens are TypeScript values. Theme overrides are type-checked against token definitions. If a token is renamed or removed, every usage gets a compile error — not a silent visual regression.

// Token definition
export const colorTokens = stylex.defineVars({
  accent: 'blue',
  textPrimary: 'black',
});

// ← TypeScript catches typos and stale references

4. Atomic CSS and Bundle Scaling

StyleX deduplicates identical property-value pairs across the entire component library. The compiled xds.css output grows sublinearly — it plateaus as components reuse the same atomic styles. For a library with 90+ components, this keeps the CSS bundle small.

5. Scoped Internal Theming

Internally, stylex.createTheme() applies token overrides to DOM subtrees. This powers Astryx's theme composition — nested themes, mode switching, component-level overrides — all resolved at compile time.


Consumer Experience: No StyleX Required

This is the key shift: consumers don't need StyleX in their build pipeline. Astryx ships pre-compiled CSS and supports arbitrary consumer styling.

Pre-Compiled CSS

Astryx compiles all StyleX internally and ships a single CSS file:

// That's it — no Babel plugin, no PostCSS config
import '@xds/core/xds.css';

The CSS is wrapped in @layer xds so consumers can control specificity relative to their own styles.

className for Overrides

Every component accepts className. Consumers use whatever styling approach they prefer:

// Tailwind
<Button variant="primary" className="mt-4 rounded-full">Save</Button>

// CSS modules
<Card className={styles.dashboardCard}>...</Card>

// Plain CSS
<Stack className="my-layout">...</Stack>

Three Styling Escape Hatches

All components accept three styling props, in priority order:

Prop For When to use
className Tailwind, CSS modules, plain CSS Most consumer overrides
style Inline overrides Dynamic values, one-offs
xstyle StyleX styles from stylex.create() Consumers who also use StyleX internally

className is the primary override mechanism. xstyle remains available for consumers who happen to use StyleX in their own apps and want optimal atomic deduplication across the boundary — but it's not the default path.

Theming via CSS Custom Properties

Themes are CSS custom property overrides. No StyleX knowledge needed:

const myTheme = defineTheme({
  name: 'my-brand',
  tokens: {
    '--color-accent': ['#0077B6', '#48CAE4'],     // [light, dark]
    '--color-surface': ['#F0F8FF', '#0A1628'],
    '--radius-container': '16px',
  },
});

<Theme theme={myTheme}>
  <App />
</Theme>

Stable Class Names for Advanced Theming

Every component renders stable class names (astryx-button, astryx-card) with variant values as additional classes. These enable CSS-based theming via scoped selectors:

/* Target specific variants */
.astryx-button.primary { /* ... */ }
.astryx-card { /* ... */ }

This gives theme authors full CSS selector power without depending on internal implementation classes.


What Changed (and Why)

Astryx originally required consumers to run StyleX's build plugins to use the library. That meant:

  • Every consuming app needed Babel + PostCSS configuration for StyleX
  • LLMs couldn't write StyleX fluently — they fell back to verbose inline styles
  • stylex.createTheme couldn't cross the dist boundary cleanly
  • The xstyle prop confused AI agents (they treated it like style)

Vibe test data across 13 runs showed Astryx code was 37–60% more verbose than baseline (shadcn + Tailwind) on the same prompts — primarily because LLMs couldn't write StyleX and fell back to style={{}} blocks.

The solution: keep StyleX internally (all the authoring benefits still apply) but remove it as a consumer requirement. Pre-compiled CSS + className gives consumers zero build overhead and full styling freedom. See #506 for the full discussion.


Tradeoffs

StyleX Learning Curve (for contributors)

Contributors to packages/core/ need to learn StyleX patterns. This is a real cost — StyleX is less common than Tailwind in the broader ecosystem. The Component Authoring Guide covers the patterns, and existing components serve as templates.

Verbose Internal Syntax

StyleX uses JavaScript objects where Tailwind uses concise class strings. A Tailwind group-hover:opacity-100 is two words; the equivalent StyleX pattern requires CSS variable workarounds. This is the cost of StyleX's encapsulation guarantees — worth it for the system team, but real.

Complex Selectors

Parent-child state relationships (e.g., "show delete button when list item is hovered") require a CSS variables pattern in StyleX rather than Tailwind's ergonomic group-hover:*. The pattern is pure CSS (no runtime), but more verbose to set up.

Smaller Ecosystem

Tailwind has far more community components, templates, and tooling. For the Astryx system team this matters less (we're building the system, not consuming one), but contributors from Tailwind backgrounds face a learning curve.


Summary

Concern Astryx Approach
Internal styling StyleX — compile-time enforcement, atomic output, private internals
Consumer styling className — any library (Tailwind, CSS modules, plain CSS)
Consumer build requirement None — import xds.css, done
Theming CSS custom properties via defineTheme()
Component overrides Stable class names + CSS selectors
AI/LLM friendliness Consumers use props + className (high fluency), not StyleX
Token enforcement Compile-time for authors, CSS custom properties for consumers
Bundle strategy Pre-compiled atomic CSS in @layer xds

StyleX is the right internal authoring tool because constraints are the product. Consumers get full freedom because that's what makes adoption frictionless. The two concerns are cleanly separated — what's good for system authors and what's good for system consumers don't have to be the same thing.


Related

Clone this wiki locally