Skip to content

Swizzle Ergonomics

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

Swizzle Layer 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

Context

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

Decision Criteria (from product discussions)

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

Personas Using Swizzle

Persona 1: Design System Team

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)

Persona 2: Regular Builder

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

Proposed Architecture: Dual-Path Swizzle

┌─────────────────────────────────────────────────────────────────┐
│  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)        │
└─────────────────────────────────────────────────────────────────┘

Path A: Styling Customization (Theme Extension)

For DS teams that want to customize appearance without changing structure.

Command:

npx xds 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 '@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>

Path B: Functional Override (Full Swizzle)

For builders who need to change behavior or structure.

Command:

npx xds swizzle Button

Generated 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:

  1. Extensive inline documentation — AI reads comments
  2. Clear customization points — 👇 CUSTOMIZE markers
  3. Examples in comments — "Add onClick tracking: see line 45"
  4. Semantic tokens only — no raw StyleX internals exposed
  5. Flat structure — not deeply nested

The pattern: Swizzled code is essentially AI context. Structure it so LLMs know exactly where and how to modify.


Implementation Plan

Phase 1: Path A (Theme Extension)

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

Phase 2: Path B (Functional Swizzle)

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

Phase 3: AI Tooling

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

Tailwind Ergonomics Option

For builders who prefer Tailwind patterns, we can offer a Tailwind-flavored swizzle that still uses Astryx tokens.

Path A with Tailwind Syntax

Command:

npx xds 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 '@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

Path B with Tailwind Variants

Command:

npx xds swizzle Button --format=tailwind

Generated 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>
  );
}

Format Selection

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=tailwind

Constraints Still Apply

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-primaryvar(--astryx-color-primary)
  • text-on-primaryvar(--astryx-color-on-primary)
  • p-mdvar(--astryx-spacing-md)

Tradeoff: Tailwind Format

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.


Tradeoffs

Approach Path A (Theme Extension) Path B (Full Swizzle)
Complexity Low High
AI vibes ✅ Excellent ⚠️ Medium
Covers use cases Styling only Everything
Contribution back ✅ Possible ❌ Unlikely
Maintenance ✅ Easy ⚠️ Your responsibility
Learning curve None StyleX required

Decision

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.


Open Questions

  • 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?

Related


Sources

Clone this wiki locally