-
Notifications
You must be signed in to change notification settings - Fork 28
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.
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 │
└──────────────────────────────────────────────────────────┘
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.
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.
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 referencesStyleX 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.
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.
This is the key shift: consumers don't need StyleX in their build pipeline. Astryx ships pre-compiled CSS and supports arbitrary consumer styling.
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.
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>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.
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>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.
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.createThemecouldn't cross the dist boundary cleanly - The
xstyleprop confused AI agents (they treated it likestyle)
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.
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.
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.
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.
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.
| 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.
- Distribution — Packages, versioning, source and dist bundles
-
API Conventions — The three styling escape hatches (
className,style,xstyle) - Component Authoring Guide — StyleX patterns for contributors
- Contributing with AI Assistants — How the consumer experience affects AI code generation
- #506 — The styling strategy discussion that drove this shift