feat: add Meta Theme - #692
Conversation
PR Analysis Report📚 Storybook PreviewView Storybook for this PR 🧪 Sandbox PreviewView Sandbox for this PR No new or modified components detected. Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
4b33f96 to
cced001
Compare
Updated: Theme Override Issues — Summary for @cixzhangWhat we were trying to doBuild a Meta Theme with color tokens sourced from CDS XMDS 3.0 ( What works ✅
What doesn't work ❌
What threw us offThe Storybook token panel (left sidebar) always shows the base This means:
Root cause (unchanged)StyleX
Suggested core fixes
This is a systemic issue affecting all XDS themes that override color tokens, not just Meta. |
PR #692 Analysis — Theme Infrastructure EvaluationThis PR creates Approach & learningsThe initial approach was to derive the Meta theme directly from the CDS (Core Design System) source files in WWW ( The next step was to manually set theme tokens (colors, radius, typography) through This meant the bulk of the work wasn't token overrides — it was per-component visual tweaks that the theme system sometimes couldn't express without touching core. By the numbers
What the theme system handled well ✅Token overrides — 70+ color tokens, radius scale, and typography all defined purely in Component style overrides — 12 components customized through
However, the button radius override required Where we had to touch core (the gaps)
|
| Category | Count | % | Properties |
|---|---|---|---|
| Color | 10 | 63% | radio checked border/bg/dot, unchecked border; checkbox unchecked border; switch off bg; slider track bg; calendar selected bg; dialog bg; banner icon color |
| Sizing | 4 | 25% | radio border width, radio dot size, header border width, footer border width |
| Spacing | 2 | 12% | header bottom padding, footer top padding |
Detail per component
| Component | Vars Added | Why the theme system couldn't handle it |
|---|---|---|
| RadioListItem | 6 | Checked/unchecked colors, dot size, border width all hardcoded in StyleX |
| LayoutHeader | 2 | Divider border width and padding hardcoded |
| LayoutFooter | 2 | Same as header |
| Calendar | 1 | Selected date bg hardcoded to --color-accent |
| CheckboxInput | 1 | Unchecked border color not overridable |
| Dialog | 1 | Background hardcoded to --color-surface |
| Slider | 1 | Track bg hardcoded to --color-muted |
| Switch | 1 | Off track bg hardcoded |
Root cause in every case: a component hardcodes a specific token reference (e.g., colorVars['--color-accent']) in StyleX, and the theme needed a different value in that spot. The theme system can change --color-accent globally, but can't say "use --color-surface for the radio background when checked."
2 structural changes (all themes)
- Banner: title
<p>→<h3>, default status icon always rendered - Button:
XDSButtonVariantProptype (XDSButtonVariant | string & {}) so themes can define custom variants (e.g.,primary-muted,primary-outline) without modifying core
Key infrastructure observations
-
The theme system can't target sub-elements. Component overrides set CSS properties on
.xds-componentbut can't reach inner elements (radio dot, slider track, switch track). Every sub-element customization required adding a CSS custom property to core. This was the Adding Code of Conduct file #1 source of core changes. -
No escape hatch for extending variants. Adding
primary-muted/primary-outline/ etc. required a type change and ensuring the component gracefully skips unknown StyleX variants. A first-class mechanism for theme-defined variants would eliminate this. -
Hardcoded token references in StyleX aren't per-component overridable. When a component uses
colorVars['--color-accent']directly, the theme can only change--color-accentglobally — not per-component. Thevar(--xds-X, fallback)pattern works but must be manually added per property. -
StyleX specificity beats theme overrides. Component overrides use class-based selectors (
.xds-button { borderRadius: ... }), but StyleX applies styles at inline-level specificity. This forces!importantfor property overrides like button radius. A cleaner specificity model between StyleX and the theme layer would eliminate this. -
hasDivideris a prop, not a style. Removing borders via CSS (border-width: 0) works, but the padding collapse logic is prop-driven. This disconnect forced us to add padding custom properties too. -
Font loading is the consumer's responsibility. The theme declares Figtree but can't load it — we had to add a Google Fonts
<link>to Storybook manually. -
Designers think in components, not tokens. The initial plan was global token mapping from CDS. In practice, most requests were component-specific: "radio should be ring-style," "dialog header should be semibold," "badge text should be medium." The theme system needs to support this mental model natively.
Recommendations for theme infra improvements
| Priority | Recommendation | Impact |
|---|---|---|
| 🔴 High | Auto-expose sub-element CSS custom properties — components with visual sub-elements should generate var(--xds-X, default) for key visual properties |
Eliminates ~10 of the 16 core changes |
| 🔴 High | First-class custom variants — let themes register new variant definitions that components accept without core type changes | Eliminates the XDSButtonVariantProp workaround |
| 🔴 High | Resolve StyleX vs theme specificity — ensure theme component overrides can win over StyleX without !important |
Eliminates !important hacks in theme overrides |
| 🟡 Medium | Component-scoped token overrides — allow --color-accent to be overridden only within a specific component scope |
Cleaner than adding component-specific custom properties |
| 🟡 Medium | Font loading integration — themes should declare font URLs that <XDSTheme> loads automatically |
Prevents the "font declared but not loaded" gotcha |
| 🟡 Medium | Support CDS/external design system import — tooling to map external token systems (like CDS XMDS 3.0) to XDS tokens, even if imperfect | Reduces manual token translation work |
| 🟢 Nice | Style-driven divider control — make border/padding behavior respond to CSS rather than requiring a prop | Removes the prop-vs-style disconnect |
Theme infrastructure follow-ups from this PRThanks for the thorough analysis, @rubyycheung — the breakdown of what the theme system handled vs where core changes were needed shaped the theming standards. Shipped ✅
In Progress
Your findings → resolutions
All 17 of your CSS var needs from the Meta theme are now addressable via xdsClassName class targeting + state classes — no core CSS vars required for sub-element theming. |
b037fb5 to
39dc3bb
Compare
Impact of theming infrastructure on this PRFollowing up on @cixzhang's theming infrastructure work — here's how those changes reduced the core footprint of this PR. Before → After
4 core files fully reverted (zero diff from main)These no longer need any changes thanks to xdsClassName class targeting:
10 CSS vars removed from coreThese were introduced as escape hatches but are now handled by xdsClassName sub-element targeting in
7 CSS vars still needed in coreThese participate in
Theme improvements
Remaining core changes (7 files)
|
Adds layering tests to brutalist theme that verify: - Component overrides (borderRadius, borderWidth, textTransform) work without !important when wrapped in @layer xds.theme - Reproduces the exact pattern from Ruby's Meta theme (#692) where borderRadius on button base needed !important to win over StyleX - Zero !important in entire theme CSS output - All component overrides present in generated CSS 10 tests covering layer-test theme + full brutalist theme.
Adds borderRadius: var(--radius-rounded) to brutalist button base — the exact pattern that needed !important in Meta theme (#692). Switch to Brutalist in Storybook and check Button stories: - Buttons should be pill-shaped (0px radius from --radius-rounded) - If layers aren't working, buttons will keep their default radius
2cf1509 to
af47c28
Compare
Updated analysis — after #802 / #803 / #804This reflects the latest state of the PR after adopting @cixzhang's unified typography config (#804), variants-carry-styles (#803), and additional visual changes (floating label input, banner/dialog/switch refinements). Current PR state
What the theme handles purely through
|
| Concern | Mechanism |
|---|---|
| 85 color token overrides | tokens |
| Typography (fonts, scale, weights) | typography config (#804) |
| Motion timing | motion (#802) |
| Heading 1 → 28px, link color = accent | Token overrides |
| Button pill shape + core variant color overrides | components.button |
| 5 custom button variants (muted/outline) | variants.button with inline styles (#803) |
| Card/Section 20px padding, 32px radius | Pre-existing --layout-padding-*, --card-radius vars |
| Dialog 32px radius + bg | Pre-existing --dialog-radius var + class targeting |
| Banner card bg + 32px radius + primary icon | --banner-status-bg, --banner-radius + banner-icon class target |
| Radio dot, slider track, calendar selected | xdsClassName sub-element targets (#763) |
| Popover/dropdown/menu/tooltip 16px radius | Pre-existing --popover-radius, --dropdown-radius + class targeting |
| Dialog: no header/footer borders, no body padding, large footer buttons | layout-header, layout-content, layout-footer class targeting |
| Empty state heading 1, field status plain text, search input pill | xdsClassName class targeting |
| Badge pastels, checkbox 4px radius, switch thumb + shadow | Class targeting |
16 xdsClassName targets used: banner-icon, radio-dot, slider-track, calendar-day, switch-thumb, emptystate-title, field-status, layout-header, layout-content, layout-footer, dropdown-menu, more-menu, popover, tooltip, text-input, checkbox
Core changes still required (9 files)
New features (2 files, +271/-31 lines):
| File | Change | Lines |
|---|---|---|
| XDSTextInput.tsx | labelPlacement="inset" floating label |
+166/-31 |
| XDSButton.tsx | 5 new variant StyleX objects | +105 |
CSS custom properties for theming (4 files, 12 new vars):
| Component | Vars | Reason |
|---|---|---|
| Radio (4) | --xds-radio-border-width, --xds-radio-unchecked-border, --xds-radio-checked-border, --xds-radio-checked-bg |
Used inside color-mix() hover |
| Checkbox (2) | --xds-checkbox-unchecked-border, --xds-checkbox-border-width |
Used inside color-mix() hover |
| Switch (5) | --xds-switch-off-bg, --switch-thumb-size-off/on, --switch-thumb-off-x/on-x |
Hover color-mix() + sizing |
| Banner (1) | --banner-status-bg |
StyleX specificity |
Bug fixes + minor (3 files, +25/-7 lines):
- Radio dot centering fix,
radiusVarstoken usage - Slider thumb
boxShadow - Banner
hasIconprop,<h3>title, per-status icon colors - EmptyState xdsClassName on title
Assessment
What worked:
- Token-level theming (colors, radius, spacing, typography) requires zero core changes. The
typographyunification from #804 removed ~60 lines of manual config and 3 raw font token strings. - xdsClassName sub-element targeting from #763 replaced 10 CSS custom properties from a previous iteration of this PR. Direct property overrides on targets like
banner-icon,radio-dot,slider-trackwork well. - The
variantsfield from #803 cleanly handles custom button variant declaration + styling together, with generated TypeScript augmentation.
Concerns:
-
12 new CSS custom properties in core. 7 exist because
color-mix()hover expressions reference them — xdsClassName can't override a value consumed inside a computed expression. The remaining 5 (banner-status-bg, switch thumb sizing) exist because StyleX's atomic class specificity beats the theme's@scope'd selectors. Each is individually reasonable, but the pattern of adding one-off CSS vars per theme need is worth watching. If every new theme requires similar escape hatches, the approach doesn't scale. -
Button variants in core (+105 lines). The 5 new variants are full StyleX objects with hover/focus/active states. The theme overrides colors via
variants.button, but interaction states (:hover > @media (hover: hover)nesting) can't be replicated in plain CSS. For: these variants use universal tokens and work across all themes with sensible defaults. Against: they're currently Meta-specific; if other themes don't use them, they're 105 lines of unused StyleX definitions. Thevariantsfield was designed to let themes add variants without core changes, but it can't provide proper interaction states — this is a gap in the theming model. -
Floating label input (+166/-31 lines). The largest single change. It adds
useStatefor focus tracking, animated transitions, status-colored labels, and a new layout mode toXDSTextInput. This is genuinely new functionality, not theming — any theme could use it. But it's a significant addition bundled into a theming PR. Could be its own PR. -
8 core components modified for one theme. Even with the infrastructure, the first real theme exercise needed patches to Banner, Button, CheckboxInput, EmptyState, RadioList, Slider, Switch, and TextInput. This is expected for a first theme — future themes benefit from the escape hatches and xdsClassName targets added here. But it means the "theme-only, no core changes" promise isn't fully realized yet.
-
xdsClassName targets added ad-hoc.
emptystate-titlewas added specifically because the theme needed it. The CLI'sKNOWN_COMPONENTSregistry grew by 20+ entries. These additions were driven by one theme's needs rather than a systematic audit. A broader pass to add xdsClassName to all themeable sub-elements would prevent future themes from needing similar one-off core patches.
Bottom line: The theming infrastructure substantially reduced core changes compared to what a naive approach would require. But 9 core files, 12 CSS vars, and +364 lines for a single theme is still significant. The main question going forward: are the core changes made here universally useful (floating label, button variants, xdsClassName targets), or are they primarily serving the Meta theme? If the former, this PR is building shared infrastructure. If the latter, the theming model has room to grow.
Recommendations for reducing core changes in future themes
Three structural changes to the theming infrastructure that would eliminate most of the core modifications this PR needed.
1. Extract shared button interaction styles to base
Every button variant repeats identical hover/active/focus boilerplate — the backgroundImage overlay, outline ring, and outlineOffset are the same across all 9 variants. Only backgroundColor, color, border, and focus ring color differ.
If the shared interaction behavior moved into styles.base:
base: {
backgroundImage: {
':hover': { '@media (hover: hover)': overlay },
':active': pressed,
},
outline: { ':focus-visible': '2px solid var(--button-focus-ring, var(--color-ring-focus))' },
}Then each variant only sets static color properties + --button-focus-ring. The 5 custom Meta variants would drop from ~105 lines of core StyleX to ~15 lines in the theme. More importantly, defineTheme variants would work for hover/focus automatically because the base provides interaction behavior and the variant just provides colors.
2. Components own their base colors as CSS custom properties
The 7 color-mix() CSS vars (--xds-radio-*, --xds-checkbox-*, --xds-switch-*) exist because hover expressions reference them. But the var(--xds-*, fallback) escape hatch pattern is ad-hoc.
Instead, components could define the base color as part of their own StyleX styles:
radio: {
'--radio-border': colorVars['--color-border-emphasized'],
borderColor: {
default: 'var(--radio-border)',
':hover': 'color-mix(in srgb, var(--radio-border), var(--color-hover-tint) 20%)',
},
}The theme overrides --radio-border via xdsClassName class targeting — no special --xds-* naming, no fallback wrapping. The component owns the var. Hover auto-derives. This is functionally the same mechanism but makes the custom property part of the component's CSS layer instead of a theming escape hatch.
This would apply to all components that use color-mix() for hover states (Radio, Checkbox, Switch), reducing the 7 escape-hatch vars to 0 while keeping the same behavior.
3. Support pseudo-selectors in defineTheme component overrides
XDSStyleOverrides is currently Record<string, string> — flat properties only. But CSS @scope fully supports pseudo-selectors:
@scope ([data-xds-theme="meta"]) {
.xds-button.primary-muted:hover { background-image: linear-gradient(...); }
.xds-button.primary-muted:focus-visible { outline: 2px solid ...; }
}Extending the type to allow nested pseudo keys:
type XDSStyleOverrides = Record<string, string | Record<string, string>>;
// Usage in defineTheme:
variants: {
button: {
'primary-muted': {
backgroundColor: 'light-dark(#ECF5FF, #182849)',
':hover': { backgroundImage: 'linear-gradient(...)' },
':focus-visible': { outline: '2px solid var(--color-ring-focus)' },
},
},
}This would let themes define fully interactive variants without any core changes. Combined with recommendation #1, it could eliminate the need for button variant StyleX definitions in core entirely.
Combined impact
If all three were implemented:
| Current | After | Reduction |
|---|---|---|
| 12 CSS custom properties in core | ~2 (banner-status-bg, switch sizing) | -10 vars |
| 105 lines of button variant StyleX | 0 (theme handles variants + interactions) | -105 lines |
| 9 core files changed | ~5 (TextInput floating label, Banner hasIcon/h3, EmptyState xdsClassName, Slider thumb shadow, Bug fixes) | -4 files |
The remaining core changes would be genuinely new features (floating label, hasIcon prop) and bug fixes — not theming escape hatches.
Updated analysis — CSS var cleanup + @layer specificity issueFollow-up to the previous analysis. Two structural changes since then. 1. Eliminated all
|
| Before (escape hatch) | After (component-owned) |
|---|---|
var(--xds-radio-unchecked-border, FALLBACK) |
Component: '--radio-border': TOKEN + var(--radio-border) |
Theme sets --xds-radio-unchecked-border: #8F9296 |
Theme targets .xds-radio { --radio-border: #8F9296 } |
Hover: color-mix(var(--xds-radio-unchecked-border, FALLBACK), ...) |
Hover: color-mix(var(--radio-border), ...) — auto-derives |
7 component-owned vars remain (down from 12 --xds-* escape hatches):
| Component | Vars | Purpose |
|---|---|---|
| Radio | --radio-border-width, --radio-border, --radio-checked-border, --radio-checked-bg |
Border/bg with hover color-mix |
| Checkbox | --checkbox-border-width, --checkbox-border |
Border with hover color-mix |
| Switch | --switch-off-bg |
Track bg with hover color-mix |
Zero --xds-* vars remain in core.
2. Discovered @layer specificity issue — requires !important
Problem: The theme CSS is generated inside @layer xds.theme. StyleX output is unlayered. Per the CSS cascade, unlayered styles beat layered styles for normal declarations. This means every theme property override that competes with a StyleX atomic class silently fails.
This affects:
paddingon popover, dropdown-menu, more-menu (StyleX sets padding directly)--container-paddingand--layout-padding-*on card, section, dialog (StyleX sets these viacontainer())borderRadius,border-block-end-width, etc. on layout sub-elements
Current workaround: !important inside @layer. Per the CSS spec, !important declarations in a layer have higher priority than unlayered !important — the cascade order is reversed for important declarations. So @layer { .x { padding: 16px !important } } beats unlayered .y { padding: 12px }.
19 properties currently use !important in the Meta theme:
- 12×
--layout-padding-*/--container-padding(card, section, dialog) - 3×
padding(popover, dropdown, more-menu) - Plus border-width on layout-header/footer
This is a systemic issue, not specific to this theme. Any theme that overrides properties also set by StyleX will need !important. The root cause is that generateThemeCSS wraps output in @layer xds.theme, which was intended for cascade ordering but creates an unintended specificity disadvantage against StyleX's unlayered output.
Possible fixes:
- Remove
@layerfrom theme CSS — theme rules would be unlayered like StyleX, competing on selector specificity alone (which@scopeprovides) stylex.defaults()API (stylex#1539) — would let StyleX values participate in the theme cascade properly- Document that theme overrides need
!important— pragmatic but not ideal
Current PR state (updated)
| Area | Files | Insertions | Deletions |
|---|---|---|---|
| Core | 9 | +371 | -74 |
| Theme | 6 | +644 | — (new) |
| CLI | 1 | +33 | -6 |
Core CSS vars: 7 component-owned (down from 12 --xds-* escape hatches)
Theme !important usage: 19 properties (due to @layer specificity)
--xds-* escape hatches: 0 (all eliminated)
Extends XDSStyleOverrides to accept pseudo-class keys (e.g. ':hover',
':focus-visible', ':active') mapping to nested property objects. This
lets themes override interaction states directly via defineTheme without
CSS custom property escape hatches.
Before: themes needed per-component CSS vars like --xds-radio-unchecked-border
to indirectly swap values inside color-mix() hover expressions.
After: themes can override hover/focus/active states directly:
components: {
radio: {
base: {
borderColor: '#8F9296',
':hover': { borderColor: 'color-mix(in srgb, #8F9296, black 20%)' },
},
},
}
Changes:
- XDSStyleOverrides type: Record<string, string | Record<string, string>>
- generateThemeRules (runtime): separates pseudo entries, emits as
.xds-component:pseudo { ... } rules
- build-theme.mjs (CLI): same logic, pseudo rules skip prose co-selection
- 4 new test cases covering base+pseudo, variant+pseudo, pseudo-only,
and no-pseudo regression
Ref: PR #692 identified 7 CSS vars needed solely because color-mix()
hover expressions couldn't be overridden via class targeting.
Response to updated analysis — reducing core changesThanks for the thorough breakdown @rubyycheung. Here's how we're addressing the remaining core footprint. Banner status var → eliminatedBanner's -1 CSS var. Also added this pattern ("visual prop drives styling but isn't in xdsClassName") to the Theme Auditor checklist so it gets caught automatically. 7 color-mix() hover vars → pseudo-class overridesThe 7 vars ( components: {
radio: {
base: {
borderColor: '#8F9296',
':hover': { borderColor: 'color-mix(in srgb, #8F9296, black 20%)' },
},
},
}Themes own the full interaction behavior. No escape hatches in core. -7 CSS vars. 4 switch thumb vars → already handledThe switch thumb already has Net impact on this PR
Remaining core changes are genuine new features and bug fixes — no theming escape hatches. PRs to land before rebasing this branch
|
|
Correction on my comment above — the override keys should be components: {
banner: {
'status:info': { backgroundColor: 'var(--color-card)' },
'status:warning': { backgroundColor: 'var(--color-card)' },
'status:error': { backgroundColor: 'var(--color-card)' },
'status:success': { backgroundColor: 'var(--color-card)' },
},
}
|
4a166f6 to
586683e
Compare
8051a38 to
d2db868
Compare
- Add @xds/theme-meta package with updated token names matching main's naming convention - Wire Meta theme into sandbox: providers, layout CSS, SandboxNav selector - Use --xds-card-padding / --xds-section-padding instead of raw --container-padding vars - Meta theme is sandbox-only (not in Storybook)
Updates based on Cindy's PR review comments: - Use status:* keys for banner per-status overrides (#810) - Use pseudo-class overrides for radio/checkbox/switch hover (#811) instead of component-owned CSS vars (--radio-border, etc.) - Remove all duplicate token declarations - Remove --banner-status-bg var, use status:* class targeting instead - Remove --radio/checkbox/switch CSS vars not present on main - Add fonts field for Figtree font loading via XDSTheme - Add @xds/theme-meta to transpilePackages in next.config.mjs - Add @xds/theme-meta to root build script - Clean up component overrides to be more concise
Illustrations are consumed by the sandbox, not by the theme itself. Keeps the theme package lean (tokens + icons + component overrides). Sandbox imports them directly from src/components/MetaIllustrations.
…mponents - fonts field doesn't exist on XDSDefineThemeInput; use url on typography roles instead (body.url) for font loading - variants field was removed from defineTheme; declare custom variants as variant:name keys inside components.button instead
d2db868 to
40709a7
Compare
* feat: add Meta Theme to sandbox (rebased on main) - Add @xds/theme-meta package with updated token names matching main's naming convention - Wire Meta theme into sandbox: providers, layout CSS, SandboxNav selector - Use --xds-card-padding / --xds-section-padding instead of raw --container-padding vars - Meta theme is sandbox-only (not in Storybook) * refactor: align Meta theme with latest theming infrastructure Updates based on Cindy's PR review comments: - Use status:* keys for banner per-status overrides (#810) - Use pseudo-class overrides for radio/checkbox/switch hover (#811) instead of component-owned CSS vars (--radio-border, etc.) - Remove all duplicate token declarations - Remove --banner-status-bg var, use status:* class targeting instead - Remove --radio/checkbox/switch CSS vars not present on main - Add fonts field for Figtree font loading via XDSTheme - Add @xds/theme-meta to transpilePackages in next.config.mjs - Add @xds/theme-meta to root build script - Clean up component overrides to be more concise * refactor: move illustrations from theme package to sandbox app Illustrations are consumed by the sandbox, not by the theme itself. Keeps the theme package lean (tokens + icons + component overrides). Sandbox imports them directly from src/components/MetaIllustrations. * fix: resolve build errors — remove fonts field, move variants into components - fonts field doesn't exist on XDSDefineThemeInput; use url on typography roles instead (body.url) for font loading - variants field was removed from defineTheme; declare custom variants as variant:name keys inside components.button instead
* feat: add Meta Theme to sandbox (rebased on main) - Add @xds/theme-meta package with updated token names matching main's naming convention - Wire Meta theme into sandbox: providers, layout CSS, SandboxNav selector - Use --xds-card-padding / --xds-section-padding instead of raw --container-padding vars - Meta theme is sandbox-only (not in Storybook) * refactor: align Meta theme with latest theming infrastructure Updates based on Cindy's PR review comments: - Use status:* keys for banner per-status overrides (#810) - Use pseudo-class overrides for radio/checkbox/switch hover (#811) instead of component-owned CSS vars (--radio-border, etc.) - Remove all duplicate token declarations - Remove --banner-status-bg var, use status:* class targeting instead - Remove --radio/checkbox/switch CSS vars not present on main - Add fonts field for Figtree font loading via XDSTheme - Add @xds/theme-meta to transpilePackages in next.config.mjs - Add @xds/theme-meta to root build script - Clean up component overrides to be more concise * refactor: move illustrations from theme package to sandbox app Illustrations are consumed by the sandbox, not by the theme itself. Keeps the theme package lean (tokens + icons + component overrides). Sandbox imports them directly from src/components/MetaIllustrations. * fix: resolve build errors — remove fonts field, move variants into components - fonts field doesn't exist on XDSDefineThemeInput; use url on typography roles instead (body.url) for font loading - variants field was removed from defineTheme; declare custom variants as variant:name keys inside components.button instead
* feat: add Meta Theme to sandbox (rebased on main) - Add @xds/theme-meta package with updated token names matching main's naming convention - Wire Meta theme into sandbox: providers, layout CSS, SandboxNav selector - Use --xds-card-padding / --xds-section-padding instead of raw --container-padding vars - Meta theme is sandbox-only (not in Storybook) * refactor: align Meta theme with latest theming infrastructure Updates based on Cindy's PR review comments: - Use status:* keys for banner per-status overrides (#810) - Use pseudo-class overrides for radio/checkbox/switch hover (#811) instead of component-owned CSS vars (--radio-border, etc.) - Remove all duplicate token declarations - Remove --banner-status-bg var, use status:* class targeting instead - Remove --radio/checkbox/switch CSS vars not present on main - Add fonts field for Figtree font loading via XDSTheme - Add @xds/theme-meta to transpilePackages in next.config.mjs - Add @xds/theme-meta to root build script - Clean up component overrides to be more concise * refactor: move illustrations from theme package to sandbox app Illustrations are consumed by the sandbox, not by the theme itself. Keeps the theme package lean (tokens + icons + component overrides). Sandbox imports them directly from src/components/MetaIllustrations. * fix: resolve build errors — remove fonts field, move variants into components - fonts field doesn't exist on XDSDefineThemeInput; use url on typography roles instead (body.url) for font loading - variants field was removed from defineTheme; declare custom variants as variant:name keys inside components.button instead

Meta Theme for XDS
New theme package
@xds/theme-meta— Meta's brand identity for XDS, with all color tokens sourced from CDS XMDS 3.0.Token Comparison: XDS Default → Meta (CDS)
Core
--color-accent#0064E0#0044A3#2694FE#0058CD--color-accent-deemphasized#0082FB33rgba(0,68,163,0.1)#0082FB3Frgba(0,88,205,0.15)--color-accent-text#0143B5#FFFFFF#4BA9FE#FFFFFFSurfaces
--color-surface#FFFFFF#FFFFFF#1F1F22#1F1F22--color-wash#F1F4F7#F2F4F6#111112#111112--color-card#FFFFFF#FFFFFF#1F1F22#1F1F22--color-popover#FFFFFF#FFFFFF#28292C#28292C--color-deemphasized#0536590C#F2F4F6#1111127F#28292C--color-overlay#01122866rgba(0,0,0,0.6)#11111299rgba(0,0,0,0.6)--color-hover-overlay#0536590Crgba(17,17,18,0.05)#FFFFFF0Crgba(242,244,246,0.05)--color-pressed-overlay#05365919rgba(17,17,18,0.08)#FFFFFF19rgba(242,244,246,0.08)Text
--color-text-primary#0A1317#111112#DFE2E5#F2F4F6--color-text-secondary#4E606F#666A72#AAAFB5#9FA4AB--color-text-disabled#A4B0BC#9FA4AB#6F747C#666A72--color-text-link#0064E0#0044A3#3E9EFB#3087FF--color-text-placeholder#4E606F#666A72#AAAFB5#9FA4AB--color-text-on-media#FFFFFF#FFFFFF#FFFFFF#FFFFFFIcons
--color-icon-primary#0A1317#111112#DFE2E5#F2F4F6--color-icon-secondary#4E606F#666A72#AAAFB5#9FA4AB--color-icon-tertiary#748695#666A72#8C939B#9FA4AB--color-icon-disabled#A4B0BC#9FA4AB#6F747C#666A72--color-icon-on-media#FFFFFF#FFFFFF#FFFFFF#FFFFFFFocus
--color-focus-outline#0171E3#0044A3#2694FE#0058CD--color-focus-outline-error#E3193B#D31130#F5394F#FB7D87--color-focus-outline-success#0D8626#147B29#0D8626#3CBC22--color-focus-outline-warning#F2C00B#965E03#E9AF08#D69804Dividers
--color-divider#05365919#DFE2E5#F2F4F619#494D53--color-divider-high-contrast#647685#666A72#6F747C#9FA4AB--color-divider-emphasized#CCD3DB#D0D3D6#494D53#494D53Status
--color-positive#0D8626#147B29#0D8626#3CBC22--color-negative#E3193B#D31130#F5394F#FB7D87--color-warning#E9AF08#965E03#F2C00B#D69804--color-educational#5B08D8#0044A3#6B1EFD#3087FFUtility
--color-glimmer#CCD3DB#E7EAED#5A5E66#28292C--color-glimmer-high-contrast#A4B0BC#DFE2E5#8C939B#494D53--color-shadow-elevation—rgba(17,17,18,0.12)—rgba(17,17,18,0.12)--color-hover-tintblackblackwhitewhiteNamed Palette
#0171E333#D7E9FF#0171E333#001F4C#0171E3#0044A3#4BA9FE#3087FF#0064E0#0044A3#2694FE#3087FF#042F97#003B8E#AFD7FF#4D99FF#0A131733#F2F4F6#666A724C#28292C#647685#DFE2E5#8C939B#494D53#4E606F#666A72#AAAFB5#9FA4AB#0A1317#111112#E7EAED#F2F4F6#24BB5E33#C4F8B9#24BB5E33#053018#24BB5E#147B29#4CD964#3CBC22#0D8626#147B29#26A756#3CBC22#09441F#076D29#A5F690#4EC72A#E3193B33#FEE4E6#E3193B33#5A0107#E3193B#D31130#F5394F#FB7D87#D31130#D31130#E3193B#FB7D87#7B0210#BE0424#FFB2B8#FD8E99#F2790233#FFE6CF#F2790233#4E1608#F27902#B34A01#FFA040#F88617#E9690B#B34A01#FB8C00#F88617#6B2203#A13F04#FDB876#FD9537#FFEB3B33#FCEC85#FFEB3B33#451E03#FFEB3B#965E03#FFF176#D69804#FBC02D#965E03#FFEE58#D69804#753F07#8A5001#FBCE03#E2A400#7952FF33#ECE2FF#7952FF33#140036#7952FF#5828CA#9575CD#9B73FF#5B08D8#5828CA#7952FF#9B73FF#3E0697#4D1EB6#B3B0FE#A985FF#E91E6333#FFE1ED#E91E6333#520019#E91E63#C71050#F48FB1#FE73A1#C2185B#C71050#EC407A#FE73A1#880E4F#B30543#F8BBD0#FF85B0#00BCD433#C7F1FF#00BCD433#001B2A#00BCD4#006BA3#4DD0E1#00AFFA#00ACC1#006BA3#26C6DA#00AFFA#006064#005F91#B2EBF2#21BDFF#0DB7AF33#BCF5F0#0DB7AF33#062D38#0DB7AF#08767D#4DB6AC#0DB7AF#009688#08767D#26A69A#0DB7AF#083943#0F686F#40DCCD#1DC3B9Methodology: How tokens were mapped
Token values were derived through semantic mapping — not closest visual match.
Step 1: CDS semantic token assignments
The source of truth is
CDSDefaultThemeSource.php, which explicitly maps every CDS semantic token to XMDS 3.0 color names with light/dark pairs. For example:Step 2: CDS semantic → XDS semantic (by meaning)
Each XDS token was matched to its CDS equivalent by what it means, not what it looks like:
--color-accentBLUE_BADGE--color-text-primaryPRIMARY_TEXT--color-text-secondarySECONDARY_TEXT--color-text-disabledDISABLED_TEXT--color-text-linkBLUE_LINK--color-surfaceSURFACE_BACKGROUND--color-washBACKGROUND_DEEMPHASIZED--color-cardCARD_BACKGROUND--color-popoverPOPOVER_BACKGROUND--color-negativeNEGATIVE--color-positivePOSITIVE--color-warningWARNING--color-dividerDIVIDER--color-icon-primaryPRIMARY_ICON--color-focus-outlineBLUE_BADGEStep 3: Resolve to hex via CDSBaseColorDefinition
XMDS 3.0 color names were resolved to RGB values from
CDSBaseColorDefinition.php:Step 4: Named palette — consistent ramp pattern
CDS doesn't have direct equivalents for XDS's
--color-{hue}-background/border/icon/texttokens. These were filled using a consistent ramp position pattern derived from how CDS uses its scales for semantic tokens:_100_1000_650_400_700_350This pattern is consistent across all 10 hue families (blue, gray, green, red, orange, yellow, purple, pink, cyan, teal).
What's included
Color palette — sourced from CDS XMDS 3.0:
CDSBaseColorDefinition.php(XMDS 3.0 ramp)CDSDefaultThemeSource.phpmappings#0044A3) / BLUE_550 (#0058CD)Typography:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, ...)Radius:
Components:
Icons:
metaIconRegistry)CDS source files
internal monorepo/.../cds/CDSBaseColorDefinition.php— raw XMDS 3.0 RGB valuesinternal monorepo/.../cds/themes/CDSDefaultThemeSource.php— semantic token → color mappingsinternal monorepo/.../cds/CDSThemeColors.php— semantic token enum