-
Notifications
You must be signed in to change notification settings - Fork 27
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 xds 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 xds 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 xds customize Button --theme=corporateGenerated file (themes/corporate/button.ts):
// Auto-generated by Astryx customize
// AI-friendly format: structured config, not code
import { extendTheme } from '@xds/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 '@xds/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 xds swizzle ButtonGenerated file (components/xds/Button/Button.tsx):
/**
* 🎨 SWIZZLED COMPONENT: Button
*
* Source: @xds/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 '@xds/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 xds customize command
- Generates theme extension config files
- JSON-like structure, AI-friendly
- Type-safe with theme validation
DX improvements:
-
npx xds 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 xds 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 xds customize Button --theme=corporate --format=tailwindGenerated file (themes/corporate/button.config.ts):
// Tailwind-style config format — AI-friendly, familiar to most builders
import { defineButtonTheme } from '@xds/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 xds swizzle Button --format=tailwindGenerated file (components/xds/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 xds swizzle Button
# Tailwind format
npx xds swizzle Button --format=tailwindBoth 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