+ This panel reads the new Phase 1 skin variables directly. It is the proof that
+ root or nested `data-skin` scopes are now a stable runtime contract.
+
+ Runtime skin switching is now a first-class docs contract, even before
+ component recipes are extracted.
+
+
+ Phase 1 introduces `data-skin`, root helpers, Storybook toolbar wiring, and a
+ dedicated skin CSS entrypoint. Phase 2 will move component recipes onto this
+ contract.
+
+
+
+
+
+
+
+
+
+
+
+
What Phase 1 includes
+
+ {[
+ "A new runtime attribute: `data-skin`",
+ "Public helpers from `@ai-ui/ui` for skin names, defaults, and root updates",
+ "A dedicated `@ai-ui/ui/skins.css` entrypoint imported by the docs app",
+ "Storybook globals that apply theme, skin, and motion together"
+ ].map((item) => (
+
+
{item}
+
+ ))}
+
+
+
+
+
What still waits for Phase 2
+
+ {[
+ "Button, card, input, dialog, switch, and skeleton recipe extraction",
+ "Skin-specific component semantic variables such as `--button-*` and `--panel-*`",
+ "A docs comparison page where existing components fully restyle under each skin"
+ ].map((item) => (
+
+
{item}
+
+ ))}
+
+
+
+
+
+ {skinNames.map((name) => (
+
+ ))}
+
+
+
+ );
+}
+
+const meta = {
+ title: "Foundation/Style Contract",
+ component: StyleContractShowcase,
+ args: {
+ motion: defaultMotionMode,
+ skin: defaultSkin,
+ theme: defaultTheme
+ },
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Phase 1 adds the runtime style contract. Use the Storybook toolbar to switch the active `theme`, `skin`, and `motion` values globally, or inspect the side-by-side nested `data-skin` panels below."
+ }
+ }
+ },
+ render: (_args, context) => (
+
+ )
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Overview: Story = {};
diff --git a/docs/rfcs/multi-style-architecture.md b/docs/rfcs/multi-style-architecture.md
new file mode 100644
index 0000000..abdc062
--- /dev/null
+++ b/docs/rfcs/multi-style-architecture.md
@@ -0,0 +1,610 @@
+# 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 MotionName = "system" | "reduced" | "micro" | "spring";
+```
+
+Likely helpers:
+
+- `setTheme(theme, root?)`
+- `setSkin(skin, root?)`
+- `setMotionMode(mode, root?)`
+
+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:
+
+- not started
+
+### 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:
+
+- not started
+
+### 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:
+
+- not started
+
+### 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:
+
+- not started
+
+## 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 motion packs beyond `system` and `reduced` ship in the first skin milestone, or
+ should additional motion packs wait until after the pilot components land?
+- 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`
+- no component extraction started
+- no broad skin-aware component recipes implemented yet
+
+The next implementation task should be Phase 2: pilot skin extraction for
+`Button`, `Card`, `Input`, `Dialog`, `Switch`, and `Skeleton`.
diff --git a/packages/ui/package.json b/packages/ui/package.json
index f7861a5..871c308 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -3,8 +3,12 @@
"version": "0.0.0",
"private": true,
"type": "module",
+ "sideEffects": [
+ "**/*.css"
+ ],
"exports": {
- ".": "./src/index.ts"
+ ".": "./src/index.ts",
+ "./skins.css": "./src/skins.css"
},
"files": [
"dist",
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 6db3d8b..f2d4395 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -401,3 +401,10 @@ export {
motionScales,
type MotionRecipeName
} from "./lib/motion";
+export {
+ defaultSkin,
+ setSkin,
+ skinDetails,
+ skinNames,
+ type SkinName
+} from "./lib/skin";
diff --git a/packages/ui/src/lib/skin.test.ts b/packages/ui/src/lib/skin.test.ts
new file mode 100644
index 0000000..6dd1d5c
--- /dev/null
+++ b/packages/ui/src/lib/skin.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from "vitest";
+
+import { defaultSkin, setSkin, skinDetails, skinNames } from "./skin";
+
+describe("skin contract", () => {
+ it("exposes a default skin that exists in the public name set", () => {
+ expect(skinNames).toContain(defaultSkin);
+ expect(skinDetails[defaultSkin].label).toBeTruthy();
+ });
+
+ it("sets the document root skin when no target element is provided", () => {
+ setSkin("glass");
+
+ expect(document.documentElement.dataset.skin).toBe("glass");
+ });
+
+ it("sets the provided target element instead of the document root", () => {
+ const target = document.createElement("div");
+
+ setSkin("pixel", target);
+
+ expect(target.dataset.skin).toBe("pixel");
+ });
+});
diff --git a/packages/ui/src/lib/skin.ts b/packages/ui/src/lib/skin.ts
new file mode 100644
index 0000000..6b462bd
--- /dev/null
+++ b/packages/ui/src/lib/skin.ts
@@ -0,0 +1,41 @@
+export const skinNames = ["minimal", "glass", "pixel"] as const;
+export type SkinName = (typeof skinNames)[number];
+
+export const defaultSkin: SkinName = "minimal";
+
+export const skinDetails = {
+ minimal: {
+ label: "Minimal",
+ note: "Restrained surfaces and low-ornament defaults"
+ },
+ glass: {
+ label: "Glass",
+ note: "Translucent layers, brighter edges, and blurred panels"
+ },
+ pixel: {
+ label: "Pixel",
+ note: "Hard edges, crisp borders, and stepped shadows"
+ }
+} as const satisfies Record;
+
+function getTargetElement(root?: HTMLElement) {
+ if (root) {
+ return root;
+ }
+
+ if (typeof document === "undefined") {
+ return undefined;
+ }
+
+ return document.documentElement;
+}
+
+export function setSkin(skin: SkinName, root?: HTMLElement) {
+ const target = getTargetElement(root);
+
+ if (!target) {
+ return;
+ }
+
+ target.dataset.skin = skin;
+}
diff --git a/packages/ui/src/skins.css b/packages/ui/src/skins.css
new file mode 100644
index 0000000..4cf982d
--- /dev/null
+++ b/packages/ui/src/skins.css
@@ -0,0 +1,82 @@
+:root,
+[data-skin="minimal"] {
+ --ui-canvas-image: radial-gradient(
+ circle at top,
+ color-mix(in oklch, var(--color-primary) 8%, transparent),
+ transparent 58%
+ );
+ --ui-canvas-size: auto;
+ --ui-surface-bg: color-mix(in oklch, var(--color-card) 88%, white 12%);
+ --ui-surface-border: color-mix(in oklch, var(--color-border) 92%, white 8%);
+ --ui-surface-shadow: var(--shadow-sm);
+ --ui-surface-radius: var(--radius-lg);
+ --ui-surface-backdrop-blur: 0px;
+ --ui-control-bg: color-mix(in oklch, var(--color-background) 92%, white 8%);
+ --ui-control-border: var(--color-border);
+ --ui-control-shadow: var(--shadow-xs);
+ --ui-control-radius: var(--radius-md);
+ --ui-ornament-opacity: 0.1;
+ --ui-ornament-mix: normal;
+}
+
+[data-skin="glass"] {
+ --ui-canvas-image:
+ radial-gradient(
+ circle at top left,
+ color-mix(in oklch, var(--color-primary) 22%, transparent),
+ transparent 42%
+ ),
+ radial-gradient(
+ circle at top right,
+ color-mix(in oklch, var(--color-accent) 18%, transparent),
+ transparent 48%
+ ),
+ linear-gradient(
+ 180deg,
+ color-mix(in oklch, var(--color-background) 64%, white 36%),
+ var(--color-background)
+ );
+ --ui-canvas-size: auto;
+ --ui-surface-bg: color-mix(in oklch, var(--color-card) 58%, transparent);
+ --ui-surface-border: color-mix(in oklch, white 46%, var(--color-border));
+ --ui-surface-shadow: 0 24px 64px oklch(0.18 0.03 255 / 0.18);
+ --ui-surface-radius: var(--radius-xl);
+ --ui-surface-backdrop-blur: 20px;
+ --ui-control-bg: color-mix(in oklch, var(--color-card) 52%, transparent);
+ --ui-control-border: color-mix(in oklch, white 36%, var(--color-border-strong));
+ --ui-control-shadow: 0 14px 38px oklch(0.2 0.03 255 / 0.14);
+ --ui-control-radius: var(--radius-lg);
+ --ui-ornament-opacity: 0.36;
+ --ui-ornament-mix: screen;
+}
+
+[data-skin="pixel"] {
+ --ui-canvas-image:
+ linear-gradient(
+ 90deg,
+ color-mix(in oklch, var(--color-foreground) 7%, transparent) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ 180deg,
+ color-mix(in oklch, var(--color-foreground) 7%, transparent) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ 180deg,
+ color-mix(in oklch, var(--color-background) 92%, black 8%),
+ var(--color-background)
+ );
+ --ui-canvas-size: 12px 12px, 12px 12px, auto;
+ --ui-surface-bg: color-mix(in oklch, var(--color-card) 96%, white 4%);
+ --ui-surface-border: var(--color-foreground);
+ --ui-surface-shadow: 6px 6px 0 color-mix(in oklch, var(--color-foreground) 38%, transparent);
+ --ui-surface-radius: 0px;
+ --ui-surface-backdrop-blur: 0px;
+ --ui-control-bg: var(--color-background);
+ --ui-control-border: var(--color-foreground);
+ --ui-control-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 30%, transparent);
+ --ui-control-radius: 0px;
+ --ui-ornament-opacity: 0.2;
+ --ui-ornament-mix: multiply;
+}