-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Swizzle Ergonomics
Status: Not yet implemented. This is a design exploration for the future swizzle system. The dual-path architecture (theme extension + functional override) and Tailwind format option described here are forward-looking designs, not current features.
Exploration — January 2026
The swizzle layer has competing tensions:
- Close to core (StyleX, Astryx patterns) → easy contributions, shared utilities
- Close to builder (Tailwind, familiar patterns) → AI can help, team can maintain
| Question | Answer | Implication |
|---|---|---|
| Is contribution back to core important? | Yes, but unblocking builders is more important | Optimize for builder ergonomics first |
| What gets swizzled? | Most swizzled content is use-case dependent | Won't flow back to core anyway |
| Small tweaks or structural changes? | Two camps: DS teams (heavy customization) + Regular builders (functionality > styling) | Need to serve both personas |
| Is AI assistance critical? | Yes — common workflow to unblock builders | Must be AI-friendly |
| Can we invest in tooling/docs? | Absolutely | Can build abstractions to bridge the gap |
Who: Designers/engineers maintaining company design system Goal: Customize components to match brand guidelines Skill level: High — willing to learn StyleX Swizzle use: Heavy customization, styling-focused
Example needs:
- Custom button variants matching brand hierarchy
- Specific spacing/sizing variants
- Custom animations/transitions
- Brand-specific slots (e.g., badge on buttons)
Who: Product engineers shipping features Goal: Unblock a specific functionality need Skill level: Medium — wants to ship fast Swizzle use: Functionality-focused, minimal styling changes
Example needs:
- Add click tracking to button
- Custom validation logic in input
- Different keyboard navigation behavior
- Integration with non-Astryx libraries
┌─────────────────────────────────────────────────────────────────┐
│ SWIZZLE PATH A: STYLING CUSTOMIZATION │
│ Target: DS teams, styling-focused │
│ │
│ npx astryx customize Button --theme=myTheme │
│ → Generates theme extension (AI-friendly) │
│ │
│ Format: Theme config (JSON-like) │
│ AI vibes: ✅ High (structured data) │
│ Contribution back: ✅ Possible (just config) │
├─────────────────────────────────────────────────────────────────┤
│ SWIZZLE PATH B: FUNCTIONAL OVERRIDE │
│ Target: Regular builders, functionality-focused │
│ │
│ npx astryx swizzle Button │
│ → Copies component source with AI-friendly annotations │
│ │
│ Format: Full source (StyleX + detailed comments) │
│ AI vibes: ⚠️ Medium (unfamiliar but documented) │
│ Contribution back: ❌ Unlikely (too use-case specific) │
└─────────────────────────────────────────────────────────────────┘
For DS teams that want to customize appearance without changing structure.
Command:
npx astryx customize Button --theme=corporate
Generated file (themes/corporate/button.ts):
// Auto-generated by Astryx customize // AI-friendly format: structured config, not code import { extendTheme } from '@astryxdesign/core'; export const corporateButtonTheme = extendTheme({ component: 'button', // Add new variants variants: { 'brand-primary': { background: '--corporate-blue', color: 'white', hoverBackground: '--corporate-blue-dark', }, 'brand-secondary': { background: '--corporate-gray', color: '--corporate-blue', } }, // Modify existing sizes sizes: { lg: { padding: '16px 32px', // Override default fontSize: '18px', } }, // Customize slots slots: { icon: { marginRight: '12px', // Override default } } });
Why this is AI-friendly:
- Structured config format (AI can generate JSON-like structures easily)
- Clear property names (
background,color,padding) - No StyleX syntax to learn
- Predictable schema
Usage:
import { Theme } from '@astryxdesign/core'; import { corporateButtonTheme } from './themes/corporate/button'; <Theme theme={corporateButtonTheme}> <Button variant="brand-primary">Corporate CTA</Button> </Theme>
For builders who need to change behavior or structure.
Command:
npx astryx swizzle Button
Generated file (components/astryx/Button/Button.tsx):
/** * 🎨 SWIZZLED COMPONENT: Button * * Source: @astryxdesign/core@2.1.0 * Swizzled: 2026年01月09日 * * ⚠️ This component is now your responsibility. * Changes here won't receive automatic updates from Astryx. * * COMMON CUSTOMIZATIONS: * * 1. Add custom behavior: * - Add onClick tracking: see line 45 * - Add loading state: see line 52 * - Custom keyboard handling: see line 67 * * 2. Modify structure: * - Add new slots: see line 89 * - Change DOM structure: see line 102 * * 3. Styling changes: * - Use semantic tokens: var(--astryx-color-primary) * - Available tokens documented at line 15 */ import * as stylex from '@stylexjs/stylex'; import { createVariants } from '@astryxdesign/variants'; // 👇 AVAILABLE SEMANTIC TOKENS // Colors: --astryx-color-primary, --astryx-color-secondary, --astryx-color-danger // Spacing: --astryx-spacing-sm, --astryx-spacing-md, --astryx-spacing-lg // Typography: --astryx-font-button, --astryx-font-weight-medium const button = createVariants({ // 👇 CUSTOMIZE: Base styles applied to all variants base: stylex.create({ root: { cursor: 'pointer', borderRadius: 4, transition: 'all 0.2s', // Add custom styles here: } }).root, slots: { // 👇 CUSTOMIZE: Icon slot icon: stylex.create({ root: { width: 16, height: 16, marginRight: 8 } }).root, // 👇 CUSTOMIZE: Label slot label: stylex.create({ root: { fontWeight: 500 } }).root, }, variants: { variant: { // 👇 CUSTOMIZE: Add or modify variants primary: stylex.create({ root: { backgroundColor: 'var(--astryx-color-primary)', color: 'var(--astryx-color-on-primary)', } }).root, } }, }); interface ButtonProps { variant?: 'primary' | 'secondary'; children: React.ReactNode; onClick?: () => void; // 👇 CUSTOMIZE: Add new props here } export function Button({ variant = 'primary', children, onClick }: ButtonProps) { const styles = button({ variant }); // 👇 CUSTOMIZE: Add custom behavior (tracking, analytics, etc.) const handleClick = () => { // Example: Add analytics // analytics.track('button_clicked', { variant }); onClick?.(); }; return ( <button {...stylex.props(styles.base())} onClick={handleClick}> {/* 👇 CUSTOMIZE: Modify structure, add slots */} <span {...stylex.props(styles.slots.label())}> {children} </span> </button> ); }
Why this works for AI despite StyleX:
- Extensive inline documentation — AI reads comments
- Clear customization points — 👇 CUSTOMIZE markers
- Examples in comments — "Add onClick tracking: see line 45"
- Semantic tokens only — no raw StyleX internals exposed
- Flat structure — not deeply nested
The pattern: Swizzled code is essentially AI context. Structure it so LLMs know exactly where and how to modify.
Build: npx astryx customize command
- Generates theme extension config files
- JSON-like structure, AI-friendly
- Type-safe with theme validation
DX improvements:
-
npx astryx customize Button --interactive— CLI prompts - Auto-generates TypeScript types from config
- Preview in Storybook
Build: Enhanced swizzle templates
- Heavy inline documentation
- Clear customization markers
- Example modifications in comments
- Semantic tokens only (no raw StyleX internals)
AI assistance:
- Templates designed as "AI context"
- Common modifications documented inline
- Codebase examples in comments
Build: AI-specific helpers
-
npx astryx customize Button --with-ai-context— generates extra context file - Component customization guide for AI consumption
- Cursor/Claude rules for Astryx patterns
For builders who prefer Tailwind patterns, we can offer a Tailwind-flavored swizzle that still uses Astryx tokens.
Command:
npx astryx customize Button --theme=corporate --format=tailwind
Generated file (themes/corporate/button.config.ts):
// Tailwind-style config format — AI-friendly, familiar to most builders import { defineButtonTheme } from '@astryxdesign/core'; export const corporateButton = defineButtonTheme({ variants: { 'brand-primary': { base: 'bg-primary text-on-primary rounded-md', hover: 'hover:bg-primary-dark', focus: 'focus:ring-2 focus:ring-primary/50', disabled: 'disabled:opacity-50 disabled:cursor-not-allowed', }, 'brand-secondary': { base: 'bg-secondary text-on-secondary rounded-md border border-border', hover: 'hover:bg-secondary-dark', }, }, sizes: { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-base', lg: 'px-6 py-3 text-lg', }, });
Key point: The Tailwind classes use Astryx token names (bg-primary, text-on-primary) from the Tailwind preset, not arbitrary values.
Why this works:
- Familiar Tailwind syntax
- AI can generate this confidently
- Still uses Astryx tokens (consistency)
- Linting prevents arbitrary values
Command:
npx astryx swizzle Button --format=tailwind
Generated file (components/astryx/Button.tsx):
/** * 🎨 SWIZZLED COMPONENT: Button * * Format: Tailwind Variants (familiar to most builders) * Tokens: Astryx theme tokens (via Tailwind preset) * * RULES: * ✅ Use Astryx token classes: bg-primary, text-on-primary, p-md * ❌ No arbitrary values: bg-[#ff0000], mt-[13px] */ import { tv } from 'tailwind-variants'; const button = tv({ base: 'rounded-md cursor-pointer transition-colors', slots: { icon: 'w-4 h-4', label: 'font-medium', }, variants: { variant: { primary: { base: 'bg-primary text-on-primary hover:bg-primary-dark', icon: 'text-on-primary', }, secondary: { base: 'bg-secondary text-on-secondary hover:bg-secondary-dark', icon: 'text-on-secondary', }, danger: { base: 'bg-danger text-on-danger hover:bg-danger-dark', }, // 👇 CUSTOMIZE: Add new variants here }, size: { sm: { base: 'px-sm py-xs text-sm', icon: 'w-3 h-3' }, md: { base: 'px-md py-sm text-base', icon: 'w-4 h-4' }, lg: { base: 'px-lg py-md text-lg', icon: 'w-5 h-5' }, }, }, defaultVariants: { variant: 'primary', size: 'md', }, }); interface ButtonProps { variant?: 'primary' | 'secondary' | 'danger'; size?: 'sm' | 'md' | 'lg'; children: React.ReactNode; onClick?: () => void; // 👇 CUSTOMIZE: Add new props } export function Button({ variant, size, children, onClick }: ButtonProps) { const styles = button({ variant, size }); // 👇 CUSTOMIZE: Add behavior (tracking, etc.) const handleClick = () => { onClick?.(); }; return ( <button className={styles.base()} onClick={handleClick}> {/* 👇 CUSTOMIZE: Modify structure, add slots */} <span className={styles.label()}> {children} </span> </button> ); }
| Builder knows... | Recommended format |
|---|---|
| Tailwind | --format=tailwind |
| StyleX / CSS-in-JS |
--format=stylex (default) |
| Neither |
--format=tailwind (more training data) |
Command with format:
# Default (StyleX) npx astryx swizzle Button # Tailwind format npx astryx swizzle Button --format=tailwind
Both formats enforce Astryx tokens:
// ✅ Uses Astryx token (via Tailwind preset) base: 'bg-primary text-on-primary p-md', // ❌ Linting error: arbitrary value not allowed base: 'bg-[#0066cc] text-white p-4',
The Tailwind preset maps Astryx tokens:
-
bg-primary→var(--astryx-color-primary) -
text-on-primary→var(--astryx-color-on-primary) -
p-md→var(--astryx-spacing-md)
| Benefit | Cost |
|---|---|
| Familiar syntax, AI-friendly | Classes in DOM (less encapsulation) |
| More training data for LLMs | Can't prevent all arbitrary values |
| Faster onboarding | Diverges from core Astryx patterns |
| Team can maintain without learning StyleX | Harder to contribute back |
Recommendation: Offer both formats. Let builders choose based on their team's skills. Default to Tailwind for better AI vibes.
| Approach | Path A (Theme Extension) | Path B (Full Swizzle) |
|---|---|---|
| Complexity | Low | High |
| AI vibes | ✅ Excellent | |
| Covers use cases | Styling only | Everything |
| Contribution back | ✅ Possible | ❌ Unlikely |
| Maintenance | ✅ Easy | |
| Learning curve | None | StyleX required |
Implement both paths with clear guidance on which to use when:
| Builder needs... | Recommended path |
|---|---|
| Different colors/spacing | Path A (customize) |
| New button variant | Path A (customize) |
| Custom click tracking | Path B (swizzle) |
| Different validation logic | Path B (swizzle) |
| Custom keyboard behavior | Path B (swizzle) |
| Structural changes | Path B (swizzle) |
Key insight: Most swizzles (80%?) are styling customizations. Serve those with the simple path. The 20% who need functional changes accept the complexity.
- Should Path A generate Tailwind config instead of theme extension for Tailwind users?
- Can we auto-detect which path a builder needs based on their prompt?
- Should swizzled components include a "sync from upstream" command?
- How do we version swizzled templates vs core components?
- AI and Design Systems — Why constraints beat suggestions
Start here Astryx Philosophy Contributing with AI Assistants Contributing
Architecture System Architecture Architecture Cheat Sheet Theming Infrastructure Distribution
Building a component Component Lifecycle Component Authoring Guide API Conventions Design Conventions
Quality Component Audit Rubric Accessibility Checklist AST-009 assistive-technology verification
Operations Release Process Night Watch Overview