# RFC: Multi-Style Architecture ## Status Proposed ## Last Updated 2026-03-20 ## Why this document exists This document records the current plan for making Cadence UI support multiple visual styles without forking component behavior or losing the existing source-owned model. It is written as a handoff document. A different agent should be able to read this file, inspect the listed repo files, and continue the work without reconstructing the intent from chat history. ## Objective Support runtime switching across multiple visual styles such as: - `minimal` - `glass` - `pixel` while preserving: - the current public React component APIs - accessibility behavior - reduced-motion support - source ownership inside this repository ## Summary Decision Cadence UI should not treat "style" as a single token pack. The working model should be: 1. `theme`: color, typography, radius, shadow baseline 2. `skin`: component appearance recipe 3. `motion`: interaction timing and effect vocabulary 4. `layout`: page composition patterns, handled outside the base component package For the first milestone, this repo should implement `theme + skin + motion`. Layout patterns such as `bento`, `sidebar`, or `magazine` should remain a docs or blocks concern, not a base `@ai-ui/ui` concern. ## External Reference Synthesis The site `https://ui-gallery.codebanana.app/` is useful because it separates style into multiple dimensions: - `UI 风格` - `色调` - `字体` - `布局` - `动效` That separation matches the conclusion above: styles like `glassmorphism` or `pixel art` are not just color swaps. They change component surfaces, borders, decoration, motion, and sometimes page composition. ## Current Repo State ### What already exists - `packages/tokens` already defines semantic CSS variables for color, typography, radius, shadow, and motion. - `packages/tokens` already supports multiple themes through `data-theme`. - `packages/tokens` already supports reduced-motion behavior through `data-motion` and `prefers-reduced-motion`. - `packages/ui` already consumes semantic token variables in many component recipes. - `packages/ui` now exposes a public skin contract through `skinNames`, `defaultSkin`, `skinDetails`, and `setSkin`. - `packages/ui` now exports a dedicated `@ai-ui/ui/skins.css` entrypoint. - `apps/docs` already acts as the review surface and imports token CSS globally. - `apps/docs` Storybook globals now apply `theme`, `skin`, and `motion` together. - `apps/docs` now includes a `Foundation/Style Contract` page that documents the Phase 1 runtime contract. ### What does not exist yet - No component skin layer exists. - No style provider exists that treats `theme`, `skin`, and `motion` as separate axes. - No existing component family fully restyles across multiple skins yet. ### Why the repo is not "multi-style" yet The current system is multi-theme, not multi-style. Many visual decisions are still written directly inside `packages/ui`, for example: - button sheen gradient and animation - skeleton shimmer gradient - white switch thumb - fixed rounded forms like `rounded-full` - fixed spacing and size choices that encode a particular visual language That means changing `tokens` alone can shift tone, but it cannot reliably produce strongly different styles like `glass` or `pixel`. ## Read These Files First Another agent resuming this work should inspect these files first: - `roadmap.md` - `CONTRIBUTING.md` - `packages/tokens/src/index.ts` - `packages/tokens/src/tokens.css` - `packages/tokens/src/motion.css` - `packages/ui/src/lib/skin.ts` - `packages/ui/src/skins.css` - `apps/docs/src/preview.css` - `apps/docs/.storybook/preview.ts` - `apps/docs/src/style-contract.stories.tsx` - `packages/ui/src/lib/motion.ts` - `packages/ui/src/components/button.tsx` - `packages/ui/src/components/button.variants.ts` - `packages/ui/src/components/skeleton.tsx` - `packages/ui/src/components/switch.variants.ts` These files capture the current token contract, motion contract, docs wiring, and the best concrete examples of hardcoded visual decisions that block style switching. ## Working Definitions ### Theme Theme sets the foundational visual baseline: - color roles - font families - font scales - radius scale - shadow scale Examples: - `editorial` - `minimal` - `dark-luxury` ### Skin Skin defines how components look while keeping structure and behavior stable. Skin can change: - surface treatment - border style - blur and transparency - decoration layers - contrast level - component-specific gradients - special effects such as sheen or pixelated shadows Examples: - `minimal` - `glass` - `pixel` ### Motion Pack Motion pack defines interaction feel: - durations - easing - hover feedback - entrance and exit behavior - loading behavior Examples: - `calm` - `micro` - `spring` ### Layout Pattern Layout pattern is page composition, not component skin. Examples: - `bento-grid` - `sidebar-app` - `magazine` - `single-column-longform` These should live in docs, blocks, or app-level patterns. They should not be mixed into the core style-switching milestone. ## Proposed Architecture ### Layer 1: Semantic Foundation Tokens Keep `packages/tokens` as the owner of global semantic variables. Do not split multiple token packages yet. Keep a single package with multiple themes until the contract is stable. The existing token groups remain the baseline: - `--color-*` - `--font-*` - `--text-*` - `--radius-*` - `--shadow-*` - `--dur-*` - `--ease-*` - `--distance-*` - `--scale-*` ### Layer 2: Skin Contract Add a new root attribute: ```html ``` `data-skin` should be the runtime contract for component appearance. Recommended initial values: - `minimal` - `glass` - `pixel` ### Layer 3: Component Semantic Style Variables The current repo uses global semantic tokens directly in many components. That is good for theme switching, but it is not enough for skin switching. The next step should be introducing component-semantic variables or skin hooks for the highest-value components first. Examples: - button: - `--button-bg` - `--button-border` - `--button-fg` - `--button-shadow` - `--button-sheen-opacity` - card: - `--card-bg` - `--card-border` - `--card-shadow` - input: - `--input-bg` - `--input-border` - `--input-shadow` - dialog or popover: - `--panel-bg` - `--panel-border` - `--panel-shadow` - `--panel-backdrop-blur` - switch: - `--switch-track-bg` - `--switch-thumb-bg` The rule is: - global semantic tokens define product-wide roles - component semantic variables define component appearance - skins map global tokens to component variables ### Layer 4: Skin CSS Ownership The recommended ownership split is: - `packages/tokens` owns global theme and motion variables - `packages/ui` owns component skin CSS Reason: - `theme` is a design-system baseline concern - `skin` is a component recipe concern That means the likely future CSS imports are: ```css @import "@ai-ui/tokens/styles.css"; @import "@ai-ui/ui/skins.css"; ``` Consumer ergonomics can later be improved by providing a convenience entry such as: ```css @import "@ai-ui/ui/styles.css"; ``` where `@ai-ui/ui/styles.css` re-exports both token and skin styles. ## Implementation Rules ### Rule 1: Preserve behavior and API Do not fork component logic per skin. Skins should change appearance, not: - props - ARIA behavior - controlled or uncontrolled patterns - slot naming - `data-*` state contract ### Rule 2: Separate structure from appearance Leave these in component code: - layout structure - sizing variants - slot and state attributes - accessibility and interaction logic Move these toward skin hooks: - surface fill - border treatment - shadow treatment - blur - decoration layers - visual effect toggles ### Rule 3: Do not put layout patterns into the skin milestone Page layouts are a separate concern and should not block the component skin work. ### Rule 4: Default gracefully If a consumer does not specify `data-skin`, the system should default to the current closest appearance, likely `minimal`. ### Rule 5: Reduced motion remains authoritative No skin may bypass reduced-motion expectations. `data-motion` and `prefers-reduced-motion` still govern the final motion behavior. ## Recommended First Slice Do not start by updating every component. Start with a focused pilot set that proves the architecture: - `Button` - `Card` - `Input` - `Dialog` - `Switch` - `Skeleton` These are enough to validate: - basic surfaces - overlays - focus treatments - loading affordances - special effects ## Suggested Skin Definitions ### Minimal Intent: - remove ornamental effects - keep surfaces solid - keep shadows subtle - prefer clarity over decoration Expected traits: - no sheen - no blur - low-contrast borders - restrained shadows ### Glass Intent: - translucent layers - blurred panels - floating surfaces - higher use of overlays and edge highlights Expected traits: - semi-transparent surfaces - stronger border highlights - backdrop blur on floating panels - controlled glow or sheen on selected components ### Pixel Intent: - square geometry - hard edges - crisp borders - step-like shadow language Expected traits: - zero or near-zero radius - no blur - hard shadows - no soft sheen - sharper, less fluid motion ## Suggested Runtime API The end state should support runtime switching without remounting components. Minimum contract: ```ts type ThemeName = "light" | "dark" | "brand" | "minimal"; type SkinName = "minimal" | "glass" | "pixel"; type MotionPackName = "calm" | "snappy" | "spring"; type MotionAccessibilityName = "system" | "full" | "reduced"; ``` Likely helpers: - `setTheme(theme, root?)` - `setSkin(skin, root?)` - `setMotionPack(pack, root?)` - `setMotionAccessibility(mode, root?)` - `setMotionMode(mode, root?)` as a backward-compatible alias for accessibility mode Provider shape if needed: ```tsx ``` This provider is optional. Direct root attributes are acceptable if they keep the system simple. ## Delivery Plan ### Phase 0: Analysis and Contract Draft Goal: - document the problem clearly - separate `theme`, `skin`, `motion`, and `layout` - identify which components prove the architecture Deliverables: - this RFC - explicit initial skin names - initial pilot component list Status: - completed ### Phase 1: Runtime Skin Contract Goal: - introduce `data-skin` - define `skinNames` and related helpers - decide final CSS file ownership and export path Deliverables: - root skin attribute contract - helper API - docs wiring for skin switching Status: - completed ### Phase 2: Pilot Skin Extraction Goal: - extract hardcoded visual decisions from the pilot components into skin-aware hooks Deliverables: - `Button`, `Card`, `Input`, `Dialog`, `Switch`, and `Skeleton` render distinctly under `minimal`, `glass`, and `pixel` - no public component API breakage Status: - completed ### Phase 3: Docs Validation Surface Goal: - create a docs page or stories that compare the same components across all supported skins and motion modes Deliverables: - a stable review surface in `apps/docs` - screenshot-friendly comparisons - clear regression target for future work Status: - completed ### Phase 4: Expand Coverage Goal: - apply the same pattern to the rest of the component library Priority order: - form controls - overlays - navigation and menus - feedback components - advanced patterns such as `DataTable` Status: - completed ### Phase 5: Packaging and Consumer Ergonomics Goal: - make skin CSS consumable outside this repo without forcing consumers to understand the internal file graph Deliverables: - final CSS import path - final package exports - install guidance in release and registry docs Status: - completed ## Acceptance Criteria For The First Real Milestone The first milestone should be considered complete only when all of these are true: - the same `Button`, `Card`, `Input`, `Dialog`, `Switch`, and `Skeleton` instances can switch across `minimal`, `glass`, and `pixel` - switching uses root attributes, not per-component prop forks - reduced motion still works correctly - Storybook or docs demonstrate the comparison clearly - no public component APIs were broken to achieve the visual switching ## Risks ### Risk 1: Tailwind classes currently mix structure and appearance This will make extraction repetitive. The work must stay disciplined and not turn into ad hoc component rewrites. ### Risk 2: Some skins may require more than variables `glass` and `pixel` may need extra selectors, pseudo-elements, or special wrapper classes. That is acceptable, but those decisions must remain in the skin layer. ### Risk 3: Layout can derail the effort The external reference site includes layout categories. If layout gets pulled into the first milestone, the implementation scope will expand too quickly. ### Risk 4: Packaging decisions can arrive too early Do not split multiple npm packages for separate skins until the internal contract is proven inside this repo. ## Open Questions - Should `skin` helpers live in `@ai-ui/tokens` or `@ai-ui/ui`? The current recommendation is `@ai-ui/ui`. - Should the runtime API expose a `StyleProvider`, or should root attributes remain the only public contract at first? - Should the current `calm / snappy / spring` set remain the long-term pack list, or should product-specific packs be introduced later? - Should `minimal` become the new default appearance, or should the current warm editorial look remain the default and be renamed explicitly? ## Current Completion Snapshot As of 2026-03-20, the project is at this point: - problem analysis completed - architecture direction chosen - scope boundaries chosen - phase 1 runtime skin contract completed - `@ai-ui/ui/skins.css` export added - Storybook globals now switch `skin` - docs switching surface added in `Foundation/Style Contract` - pilot recipe extraction completed for `Button`, `Card`, `Input`, `Dialog`, `Switch`, and `Skeleton` - screenshot-friendly validation surface added in `Foundation/Style Matrix` - scoped `data-motion="reduced"` now works for nested docs wrappers - motion now uses real packs through `data-motion-pack` - reduced motion remains available as a separate accessibility override layer - shared skin-aware treatment now extends across the broader component library surface, including controls, menus, overlays, feedback, and data-heavy patterns - package consumers can now import a single combined stylesheet from `@ai-ui/ui/styles.css` The original Phase 0-5 implementation sequence is now complete. Further work should be treated as iteration: refining skins, expanding motion packs, or hardening package and registry distribution.