# Installation

Get StyleX compiling, then paste the token layer every component reads.

You install carbon by copying component source into your project. Two things
have to exist there before any component works: the StyleX compiler and the
token layer the components read.

Both fail quietly. A missing compiler leaves `stylex.props()` returning class
names with no stylesheet behind them. A misconfigured one resolves a token to
an empty variable and the declaration drops.

## Setup

These steps assume a Vite + React + TypeScript project. Every file path below
is relative to `src/`. Delete the starter's `src/index.css` first: it sets
`--border` and `color-scheme` on `:root` outside any layer, and that beats
carbon's values.

1. Install the runtime, Base UI, the fonts and the compiler.

   ```bash
   npm install @stylexjs/stylex @base-ui/react clsx @fontsource/geist-sans @fontsource/geist-mono
   ```

   ```bash
   npm install -D @stylexjs/unplugin
   ```

2. Add the plugin to your Vite config. It goes first in the array, before the React
   plugin, so it sees the source before anything else rewrites it.

   ```ts title="vite.config.ts"
   import path from "node:path"
   import { defineConfig } from "vite"
   import react from "@vitejs/plugin-react"
   import stylex from "@stylexjs/unplugin/vite"

   export default defineConfig({
     plugins: [
       stylex({
         aliases: { "@/*": [path.join(import.meta.dirname, "src/*")] },
         useCSSLayers: { before: ["carbon.base"] },
       }),
       react(),
     ],
     resolve: { tsconfigPaths: true },
     build: { cssTarget: ["chrome123", "edge123", "firefox120", "safari17.5"] },
   })
   ```

   Each option is required:

   - `aliases` lets the compiler resolve `@/theme/tokens.stylex`. Without it the
     build fails with "Could not resolve the path to the imported file".
   - `useCSSLayers` puts the reset layer that `theme.css` opens under every atomic
     class. Without it the reset's `* { margin: 0 }` beats every StyleX
     declaration.
   - `resolve.tsconfigPaths` lets Vite resolve the same `@/` imports in dev.
   - `build.cssTarget` stops Vite's CSS minifier from rewriting `light-dark()`,
     which every themed token uses.

   The plugin appends the compiled stylesheet to your CSS bundle. You do not need
   an `@import` or a virtual module.

3. Declare your browser floor. The compiler runs its CSS through lightningcss with
   your `browserslist` as the target, and the default targets are old enough that
   `light-dark()` gets downleveled into a polyfill that only works in production
   bundles. Baseline 2024 keeps it native, and matches `cssTarget` above.

   ```json title="package.json"
   {
     "browserslist": ["chrome >= 123", "edge >= 123", "firefox >= 120", "safari >= 17.5"]
   }
   ```

4. Point `@/*` at `src/`. In a Vite project the compiler options live in
   `tsconfig.app.json`; the root `tsconfig.json` only lists references.

   ```json title="tsconfig.app.json"
   {
     "compilerOptions": {
       "paths": { "@/*": ["./src/*"] }
     }
   }
   ```

   In `tsconfig.node.json`, which type-checks `vite.config.ts`, set
   `"module": "esnext"` and `"moduleResolution": "bundler"`. Under the template's
   `nodenext`, TypeScript reads the StyleX plugin's default export as a namespace
   and `tsc -b` fails with "This expression is not callable".

5. Copy `theme.css`. It holds the element reset, the `color-scheme` switch that
   drives dark mode, and the font stacks.

   ```css title="styles/theme.css"
   @import "@fontsource/geist-sans/latin.css";
   @import "@fontsource/geist-mono/latin.css";

   :root {
     color-scheme: light dark;

     --font-sans: "Geist Sans", ui-sans-serif, system-ui, sans-serif;
     --font-mono: "Geist Mono", ui-monospace, monospace;
   }

   .light {
     color-scheme: light;
   }

   .dark {
     color-scheme: dark;
   }

   @layer carbon.base {
     *,
     ::after,
     ::before,
     ::backdrop,
     ::file-selector-button {
       box-sizing: border-box;
       margin: 0;
       padding: 0;
       border: 0 solid var(--border);
       outline-color: var(--ring-50);
     }

     html {
       line-height: 1.5;
       tab-size: 4;
       -webkit-text-size-adjust: 100%;
       -webkit-tap-highlight-color: transparent;
       font-family: var(--font-sans);
     }

     body {
       background-color: var(--background);
       color: var(--foreground);
       -webkit-font-smoothing: antialiased;
       -moz-osx-font-smoothing: grayscale;
     }

     h1,
     h2,
     h3,
     h4,
     h5,
     h6 {
       font-size: inherit;
       font-weight: inherit;
     }

     a {
       color: inherit;
       text-decoration: inherit;
     }

     b,
     strong {
       font-weight: bolder;
     }

     code,
     kbd,
     samp,
     pre {
       font-family: var(--font-mono);
       font-size: 1em;
     }

     small {
       font-size: 80%;
     }

     sub,
     sup {
       font-size: 75%;
       line-height: 0;
       position: relative;
       vertical-align: baseline;
     }

     sub {
       bottom: -0.25em;
     }

     sup {
       top: -0.5em;
     }

     table {
       text-indent: 0;
       border-color: inherit;
       border-collapse: collapse;
     }

     hr {
       height: 0;
       color: inherit;
       border-top-width: 1px;
     }

     ol,
     ul,
     menu {
       list-style: none;
     }

     summary {
       display: list-item;
     }

     img,
     svg,
     video,
     canvas,
     audio,
     iframe,
     embed,
     object {
       display: block;
       vertical-align: middle;
     }

     img,
     video {
       max-width: 100%;
       height: auto;
     }

     button,
     input,
     select,
     optgroup,
     textarea,
     ::file-selector-button {
       font: inherit;
       letter-spacing: inherit;
       color: inherit;
       border-radius: 0;
       background-color: transparent;
       opacity: 1;
     }

     button,
     input:where([type="button"], [type="reset"], [type="submit"]),
     ::file-selector-button {
       appearance: button;
     }

     textarea {
       resize: vertical;
     }

     ::placeholder {
       opacity: 1;
       color: color-mix(in oklab, currentcolor 50%, transparent);
     }

     :-moz-focusring:where(:not(iframe)) {
       outline: auto;
     }

     [hidden]:where(:not([hidden="until-found"])) {
       display: none !important;
     }
   }

   ```

6. Copy the token module. Values two components share live here: themed colors
   are `defineVars` with `light-dark()` pairs, fixed scales are `defineConsts`
   (`space`, `radius`, `text`, `leading`, `tracking`, `shadow`, `ease`,
   `duration`, `layer`), and the `interop` group publishes the flat custom
   properties the reset reads. To retheme, edit this file or scope a
   `stylex.createTheme(color, {…})` over a subtree.

   ```ts title="theme/tokens.stylex.ts"
   import * as stylex from "@stylexjs/stylex"

   export const color = stylex.defineVars({
     background: "light-dark(oklch(1 0 0), oklch(0.145 0 0))",
     foreground: "light-dark(oklch(0.145 0 0), oklch(0.985 0 0))",
     card: "light-dark(oklch(1 0 0), oklch(0.205 0 0))",
     cardForeground: "light-dark(oklch(0.145 0 0), oklch(0.985 0 0))",
     popover: "light-dark(oklch(1 0 0), oklch(0.205 0 0))",
     popoverForeground: "light-dark(oklch(0.145 0 0), oklch(0.985 0 0))",
     primary: "light-dark(oklch(0.205 0 0), oklch(0.922 0 0))",
     primaryForeground: "light-dark(oklch(0.985 0 0), oklch(0.205 0 0))",
     secondary: "light-dark(oklch(0.97 0 0), oklch(0.269 0 0))",
     secondaryForeground: "light-dark(oklch(0.205 0 0), oklch(0.985 0 0))",
     muted: "light-dark(oklch(0.97 0 0), oklch(0.269 0 0))",
     mutedForeground: "light-dark(oklch(0.556 0 0), oklch(0.708 0 0))",
     destructive: "light-dark(oklch(0.577 0.245 27.325), oklch(0.704 0.191 22.216))",
     border: "light-dark(oklch(0.922 0 0), oklch(1 0 0 / 10%))",
     input: "light-dark(oklch(0.922 0 0), oklch(1 0 0 / 15%))",
     ring: "light-dark(oklch(0.708 0 0), oklch(0.556 0 0))",

     overlay: "light-dark(oklch(0 0 0 / 10%), oklch(0 0 0 / 40%))",
     sidebar: "light-dark(oklch(0.985 0 0), oklch(0.205 0 0))",

     buttonDestructiveBg: (): string =>
       `light-dark(color-mix(in oklab, ${color.destructive} 10%, transparent), color-mix(in oklab, ${color.destructive} 20%, transparent))`,

     buttonPrimaryEdge: "light-dark(oklch(0 0 0 / 40%), oklch(0 0 0 / 10%))",
     buttonPrimaryHighlight: "light-dark(oklch(1 0 0 / 16%), oklch(1 0 0 / 60%))",
     buttonPrimarySheen: "light-dark(oklch(1 0 0 / 14%), oklch(1 0 0 / 40%))",
     buttonDestructiveHighlight: "oklch(1 0 0 / 16%)",
     buttonDrop: "light-dark(oklch(0 0 0 / 20%), oklch(0 0 0 / 50%))",
     buttonSecondaryBg: "light-dark(oklch(0.98 0 0), oklch(0.21 0 0))",
     buttonSecondaryTop: "light-dark(oklch(1 0 0), oklch(1 0 0 / 5%))",
     buttonSecondaryEdge: "light-dark(oklch(0 0 0 / 6%), oklch(0 0 0 / 36%))",
     buttonSecondaryHighlight: "light-dark(oklch(1 0 0 / 90%), oklch(1 0 0 / 7%))",
     buttonSecondaryDrop: "light-dark(oklch(0 0 0 / 4%), oklch(0 0 0 / 30%))",

     tray: "light-dark(oklch(0.985 0 0), oklch(0.17 0 0))",
     raisedHighlight: "light-dark(transparent, oklch(1 0 0 / 4%))",
     raisedShadow: "light-dark(oklch(0.2 0 0 / 6%), oklch(0 0 0 / 16%))",
     raisedRing: "light-dark(oklch(0 0 0 / 4%), oklch(0 0 0 / 40%))",
     popupRing: "light-dark(oklch(0 0 0 / 8%), oklch(0 0 0 / 50%))",
     popupShadowNear: "light-dark(oklch(0 0 0 / 6%), oklch(0 0 0 / 24%))",
     popupShadowFar: "light-dark(oklch(0 0 0 / 8%), oklch(0 0 0 / 40%))",
     itemHighlightBg: "light-dark(oklch(0 0 0 / 4%), oklch(1 0 0 / 7%))",
     itemHighlightRing: "light-dark(oklch(0 0 0 / 6%), oklch(1 0 0 / 6%))",
     invalidRing: (): string =>
       `light-dark(color-mix(in oklab, ${color.destructive} 20%, transparent), color-mix(in oklab, ${color.destructive} 40%, transparent))`,
     inputDisabledBg: (): string =>
       `light-dark(color-mix(in oklab, ${color.input} 50%, transparent), color-mix(in oklab, ${color.input} 80%, transparent))`,
     grooveShadow: "light-dark(oklch(0 0 0 / 3.5%), oklch(0 0 0 / 18%))",
     thumbShade: "oklch(0 0 0 / 3%)",
     thumbRing: "oklch(0 0 0 / 6%)",
     raisedEdge: "oklch(0 0 0 / 8%)",
     fieldHoverEdge: "light-dark(oklch(0 0 0 / 16%), oklch(1 0 0 / 14%))",
     fieldFocusEdge: "light-dark(oklch(0 0 0 / 20%), oklch(1 0 0 / 20%))",
     fieldHalo: "light-dark(oklch(0 0 0 / 6%), oklch(1 0 0 / 12%))",
     fieldDrop: "light-dark(oklch(0 0 0 / 6%), oklch(0 0 0 / 24%))",
     fieldBg: (): string =>
       `light-dark(${color.background}, color-mix(in oklab, ${color.input} 30%, transparent))`,
     well: "light-dark(oklch(0.965 0 0), oklch(0.17 0 0))",
     wellKey: "light-dark(oklch(1 0 0), oklch(0.3 0 0))",

     calloutInfoBorder:
       "light-dark(color-mix(in oklab, oklch(68.5% 0.169 237.323) 25%, transparent), color-mix(in oklab, oklch(74.6% 0.16 232.661) 25%, transparent))",
     calloutInfoBg:
       "light-dark(color-mix(in oklab, oklch(68.5% 0.169 237.323) 6%, transparent), color-mix(in oklab, oklch(74.6% 0.16 232.661) 6%, transparent))",
     calloutInfoAccent: "light-dark(oklch(58.8% 0.158 241.966), oklch(74.6% 0.16 232.661))",
     calloutTipBorder:
       "light-dark(color-mix(in oklab, oklch(60.6% 0.25 292.717) 25%, transparent), color-mix(in oklab, oklch(70.2% 0.183 293.541) 25%, transparent))",
     calloutTipBg:
       "light-dark(color-mix(in oklab, oklch(60.6% 0.25 292.717) 6%, transparent), color-mix(in oklab, oklch(70.2% 0.183 293.541) 6%, transparent))",
     calloutTipAccent: "light-dark(oklch(54.1% 0.281 293.009), oklch(70.2% 0.183 293.541))",
     calloutWarnBorder:
       "light-dark(color-mix(in oklab, oklch(76.9% 0.188 70.08) 30%, transparent), color-mix(in oklab, oklch(82.8% 0.189 84.429) 25%, transparent))",
     calloutWarnBg:
       "light-dark(color-mix(in oklab, oklch(76.9% 0.188 70.08) 7%, transparent), color-mix(in oklab, oklch(82.8% 0.189 84.429) 6%, transparent))",
     calloutWarnAccent: "light-dark(oklch(66.6% 0.179 58.318), oklch(82.8% 0.189 84.429))",
     calloutSuccessBorder:
       "light-dark(color-mix(in oklab, oklch(69.6% 0.17 162.48) 25%, transparent), color-mix(in oklab, oklch(76.5% 0.177 163.223) 25%, transparent))",
     calloutSuccessBg:
       "light-dark(color-mix(in oklab, oklch(69.6% 0.17 162.48) 6%, transparent), color-mix(in oklab, oklch(76.5% 0.177 163.223) 6%, transparent))",
     calloutSuccessAccent: "light-dark(oklch(59.6% 0.145 163.225), oklch(76.5% 0.177 163.223))",

     transparent: "transparent",
     current: "currentColor",
   })

   export const interop = stylex.defineVars({
     "--background": color.background,
     "--foreground": color.foreground,
     "--border": color.border,
     "--card": color.card,
     "--muted": color.muted,
     "--muted-foreground": color.mutedForeground,
     "--primary": color.primary,
     "--ring": color.ring,
     "--ring-50": `color-mix(in oklab, ${color.ring} 50%, transparent)`,
   })

   export const radius = stylex.defineConsts({
     xs: "0.125rem",
     sm: "0.25rem",
     md: "0.375rem",
     lg: "0.5rem",
     xl: "0.75rem",
     xl2: "1rem",
     xl4: "1.5rem",
     full: "9999px",
   })

   export const shadow = stylex.defineConsts({
     xs: "0 1px 2px 0 rgb(0 0 0 / 0.05)",

     ring: `0 0 0 2px color-mix(in oklab, ${color.ring} 30%, transparent)`,
     ringInvalid: `0 0 0 2px ${color.invalidRing}`,
     rowRing: `0 0 0 2px ${color.ring}`,

     buttonPrimary: `inset 0 1px 0 ${color.buttonPrimaryHighlight}, 0 1px 2px ${color.buttonDrop}`,
     buttonDestructive: `inset 0 1px 0 ${color.buttonDestructiveHighlight}, 0 1px 2px ${color.buttonDrop}`,
     buttonSecondary: `inset 0 1px 0 ${color.buttonSecondaryHighlight}, 0 1px 1px ${color.buttonSecondaryDrop}`,

     popup: `inset 0 0 1px 1px ${color.raisedHighlight}, 0 0 0 1px ${color.popupRing}, 0 1px 2px ${color.popupShadowNear}, 0 4px 12px -2px ${color.popupShadowFar}`,
     popupLifted: `inset 0 0 1px 1px ${color.raisedHighlight}, 0 0 0 1px ${color.popupRing}, 0 2px 4px ${color.popupShadowNear}, 0 16px 40px -8px ${color.popupShadowFar}`,

     thumb: `0 0 0 0.5px ${color.thumbRing}, 0 1px 1px ${color.buttonSecondaryDrop}`,

     peekLeft: `4px 0 8px -4px ${color.popupShadowNear}, 12px 0 24px -12px ${color.popupShadowFar}`,
     peekRight: `-4px 0 8px -4px ${color.popupShadowNear}, -12px 0 24px -12px ${color.popupShadowFar}`,

     raised: `inset 0 0 1px 1px ${color.raisedHighlight}, 0 1px 2px ${color.raisedShadow}, 0 0 2px ${color.raisedEdge}, 0 0 0 1px ${color.raisedRing}`,

     field: `0 2px 2px -1px ${color.fieldDrop}`,
     fieldFocus: `0 0 0 3px ${color.fieldHalo}`,

     groove: `inset 0 1px 1px ${color.grooveShadow}`,
     well: "inset 0 1px 1.5px light-dark(oklch(0 0 0 / 6%), oklch(0 0 0 / 35%))",
     wellKey:
       "inset 0 1px 0 light-dark(transparent, oklch(1 0 0 / 8%)), 0 0 0 0.5px light-dark(oklch(0 0 0 / 10%), oklch(0 0 0 / 60%)), 0 1px 2px light-dark(oklch(0 0 0 / 8%), oklch(0 0 0 / 45%)), 0 2px 4px -1px light-dark(oklch(0 0 0 / 5%), transparent)",
   })

   export const ease = stylex.defineConsts({
     out: "cubic-bezier(0, 0, 0.2, 1)",
     standard: "cubic-bezier(0.4, 0, 0.2, 1)",
     decelerate: "cubic-bezier(0.32, 0.72, 0, 1)",
     tactile: "cubic-bezier(0.4, 0.36, 0, 1)",
     spring:
       "linear(0, 0.0575, 0.1802, 0.3206, 0.4549, 0.5727, 0.6709, 0.7499, 0.812, 0.86, 0.8965, 0.9239, 0.9444, 0.9596, 0.9707, 0.9789, 0.9848, 0.9891, 0.9922, 0.9944, 0.996, 0.9972, 0.998, 0.9986, 1)",
   })

   export const duration = stylex.defineConsts({
     fade: "100ms",
     color: "150ms",
     modal: "150ms",
     sheen: "300ms",
     move: "200ms",
     slide: "250ms",
     stackFade: "250ms",
     panel: "300ms",
     sheet: "400ms",
     drawer: "450ms",
     stack: "500ms",
   })

   export const layer = stylex.defineConsts({
     raised: 1,
     overlay: 10,
     sticky: 20,
     popup: 50,
   })

   export const font = stylex.defineConsts({
     sans: "var(--font-sans)",
     mono: "var(--font-mono)",
   })

   export const space = stylex.defineConsts({
     s0: "0",
     s0_5: "0.125rem",
     s1: "0.25rem",
     s1_5: "0.375rem",
     s2: "0.5rem",
     s2_5: "0.625rem",
     s3: "0.75rem",
     s3_5: "0.875rem",
     s4: "1rem",
     s5: "1.25rem",
     s6: "1.5rem",
     s7: "1.75rem",
     s8: "2rem",
     s9: "2.25rem",
     s10: "2.5rem",
     s12: "3rem",
     s16: "4rem",
     s24: "6rem",
   })

   export const text = stylex.defineConsts({
     xs: "0.75rem",
     compact: "0.8rem",
     code: "0.8125rem",
     sm: "0.875rem",
     base: "1rem",
     lg: "1.125rem",
     xl2: "1.5rem",
     xl4: "2.25rem",
   })

   export const leading = stylex.defineConsts({
     xs: "calc(1 / 0.75)",
     compact: "calc(1.125 / 0.8)",
     code: "1.75",
     sm: "calc(1.25 / 0.875)",
     base: "1.5",
     lg: "calc(1.75 / 1.125)",
     xl2: "calc(2 / 1.5)",
     xl4: "calc(2.5 / 2.25)",
   })

   export const tracking = stylex.defineConsts({
     tight: "-0.025em",
     widest: "0.1em",
   })
   ```

7. Copy the breakpoints. StyleX puts a media query inside a property's value
   rather than around the rule, so breakpoints are constants you import.

   ```ts title="theme/breakpoints.stylex.ts"
   import * as stylex from "@stylexjs/stylex"

   export const breakpoints = stylex.defineConsts({
     sm: "@media (width >= 40rem)",
     md: "@media (width >= 48rem)",
     lg: "@media (width >= 64rem)",
     xl: "@media (width >= 80rem)",
     maxMd: "@media (width < 48rem)",
     maxLg: "@media (width < 64rem)",
   })
   ```

8. Copy the markers. A part that reacts to its ancestor's state, such as an
   accordion's chevron when the trigger is hovered or a sidebar row's action when
   the row is, reads that state through a marker. StyleX only compiles markers
   inside a `.stylex.ts` file.

   ```ts title="theme/markers.stylex.ts"
   import * as stylex from "@stylexjs/stylex"

   export const codeBlock = stylex.defineMarker()

   export const drawerPopup = stylex.defineMarker()

   export const sidebarMenuItem = stylex.defineMarker()

   export const accordionTrigger = stylex.defineMarker()
   ```

9. Copy the shared state styles. Every control shows the same focus ring, the same
   destructive ring when it is invalid and the same dim when it is disabled, and
   every popup sits on one of two surfaces, so those are written here and merged
   into a component's own styles.

   ```ts title="theme/states.ts"
   import * as stylex from "@stylexjs/stylex"

   import { color, layer, radius, shadow, space } from "./tokens.stylex"

   const invalidBorder = `light-dark(${color.destructive}, color-mix(in oklab, ${color.destructive} 50%, transparent))`
   const inputBg = `light-dark(transparent, color-mix(in oklab, ${color.input} 30%, transparent))`
   const fieldEdge = "light-dark(oklch(0 0 0 / 10%), oklch(1 0 0 / 8%))"

   export const states = stylex.create({
     focusRing: {
       borderColor: {
         default: `var(--carbon-border-color, ${color.transparent})`,
         ":focus-visible": color.ring,
       },
       boxShadow: { default: null, ":focus-visible": shadow.ring },
       outlineStyle: "none",
     },

     focusWithinRing: {
       borderColor: {
         default: `var(--carbon-border-color, ${color.transparent})`,
         ":focus-within": color.ring,
       },
       boxShadow: { default: null, ":focus-within": shadow.ring },
       outlineStyle: "none",
     },

     ring: { borderColor: color.ring, boxShadow: shadow.ring },

     rowFocusRing: {
       boxShadow: { default: null, ":focus-visible": shadow.rowRing },
       outlineStyle: "none",
     },

     invalidRing: {
       borderColor: invalidBorder,
       boxShadow: { default: shadow.ringInvalid, ":focus-visible": shadow.ringInvalid },
     },

     disabled: { opacity: 0.5, pointerEvents: "none" },

     popupSurface: {
       backgroundColor: color.popover,
       color: color.popoverForeground,
       boxShadow: shadow.popup,
       outlineStyle: "none",
     },

     itemHighlight: {
       backgroundColor: color.itemHighlightBg,
       boxShadow: `inset 0 0 0 1px ${color.itemHighlightRing}`,
     },

     itemHighlightDestructive: {
       backgroundColor: `color-mix(in oklab, ${color.destructive} 6%, transparent)`,
       boxShadow: `inset 0 0 0 1px color-mix(in oklab, ${color.destructive} 10%, transparent)`,
     },

     key: {
       "--carbon-border-color": color.buttonSecondaryEdge,
       backgroundColor: color.buttonSecondaryBg,
       backgroundImage: `linear-gradient(to bottom, ${color.buttonSecondaryTop}, transparent)`,
       backgroundOrigin: "border-box",
       "--carbon-key-surface": shadow.buttonSecondary,
     },

     keyChoice: {
       "--carbon-border-color": color.input,
       backgroundColor: inputBg,
       backgroundImage: "none",
     },

     keyChecked: {
       "--carbon-border-color": color.buttonPrimaryEdge,
       backgroundColor: color.primary,
       backgroundImage: `linear-gradient(to bottom, ${color.buttonPrimarySheen}, transparent)`,
       color: color.primaryForeground,
       "--carbon-key-surface": shadow.buttonPrimary,
     },

     keySurface: {
       boxShadow: {
         default: "var(--carbon-key-surface)",
         ":focus-visible": `var(--carbon-key-surface), ${shadow.ring}`,
       },
     },

     fieldSurface: {
       "--carbon-border-color": fieldEdge,
       borderColor: {
         default: "var(--carbon-border-color)",
         ":hover": color.fieldHoverEdge,
         ":focus-visible": color.fieldFocusEdge,
       },
       backgroundColor: { default: color.fieldBg, ":disabled": color.inputDisabledBg },
       backgroundClip: "padding-box",
       boxShadow: { default: shadow.field, ":focus-visible": `${shadow.field}, ${shadow.fieldFocus}` },
     },

     fieldFocused: {
       borderColor: color.fieldFocusEdge,
       boxShadow: `${shadow.field}, ${shadow.fieldFocus}`,
     },

     well: {
       backgroundColor: color.well,
       boxShadow: shadow.well,
     },

     wellKey: {
       "--carbon-border-color": color.transparent,
       backgroundColor: color.wellKey,
       backgroundImage: "none",
       "--carbon-key-surface": shadow.wellKey,
     },

     popupItem: {
       borderRadius: `max(0px, calc(var(--carbon-popup-radius, ${radius.lg}) - ${space.s1}))`,
     },

     iconTile: {
       display: "inline-flex",
       alignItems: "center",
       justifyContent: "center",
       flexShrink: 0,
       borderRadius: radius.lg,
       backgroundColor: color.card,
       boxShadow: `0 0 0 1px ${color.border}`,
       color: color.foreground,
     },

     trayFooter: {
       position: "relative",
       padding: `var(--carbon-tray-spacing, ${space.s3})`,
       paddingBlockStart: `calc(${radius.xl} + var(--carbon-tray-spacing, ${space.s3}))`,
       backgroundColor: color.tray,
       "::before": {
         content: "''",
         position: "absolute",
         insetInline: 0,
         insetBlockStart: `calc(-1 * ${space.s1})`,
         height: `calc(${radius.xl} + ${space.s1})`,
         borderEndStartRadius: radius.xl,
         borderEndEndRadius: radius.xl,
         backgroundColor: `var(--carbon-tray-body-bg, ${color.popover})`,
         boxShadow: shadow.raised,
         clipPath: `inset(${space.s1} 0 calc(-1 * ${space.s2}) 0)`,
         pointerEvents: "none",
       },
     },

     srOnly: {
       position: "absolute",
       width: 1,
       height: 1,
       padding: 0,
       margin: -1,
       overflow: "hidden",
       clipPath: "inset(50%)",
       whiteSpace: "nowrap",
       borderWidth: 0,
     },

     positioner: { isolation: "isolate", zIndex: layer.popup, outlineStyle: "none" },
   })
   ```

10. Copy the state-props helper. A Base UI part hands its state to a `className`
    callback, and `stylex.props()` returns a `style` as well as a class. The helper
    returns both callbacks from one list of styles, so a dynamic style keeps its
    inline variable.

    ```ts title="lib/state-styles.ts"
    import type { CSSProperties } from "react"
    import * as stylex from "@stylexjs/stylex"
    import type { StyleXStyles } from "@stylexjs/stylex"
    import { clsx } from "clsx"

    type VarStyles = StyleXStyles<{ [key: `--${string}`]: string | number }>

    type ExpressionStyles = StyleXStyles<{ zIndex: string }>

    type Marker = ReturnType<typeof stylex.defineMarker>

    type StyleList = ReadonlyArray<
      StyleXStyles | VarStyles | ExpressionStyles | Marker | false | null | undefined
    >

    type StateProps<State> = {
      className: (state: State) => string
      style: (state: State) => CSSProperties | undefined
    }

    type Overrides = {
      className?: string
      style?: CSSProperties
      hook?: string
    }

    export function stateStyles<State>(
      resolve: (state: State) => StyleList,
      { className, style, hook }: Overrides = {},
    ): StateProps<State> {
      return {
        className: (state) => clsx(hook, stylex.props(...resolve(state)).className, className),
        style: (state) => {
          const merged: CSSProperties = { ...stylex.props(...resolve(state)).style, ...style }
          return Object.keys(merged).length > 0 ? merged : undefined
        },
      }
    }
    ```

11. Copy `interop.css`. It holds the rules StyleX cannot author: `@property`
    registrations, the scroll-driven edge masks, icon sizing inside components, and
    drawer gesture rules.

    ```css title="styles/interop.css"
    .no-scrollbar {
      -ms-overflow-style: none;
      scrollbar-width: none;
    }

    .no-scrollbar::-webkit-scrollbar {
      display: none;
    }

    @property --scroll-fade-t {
      syntax: "<length-percentage>";
      inherits: false;
      initial-value: 0px;
    }

    @property --scroll-fade-b {
      syntax: "<length-percentage>";
      inherits: false;
      initial-value: 0px;
    }

    @property --scroll-fade-s {
      syntax: "<length-percentage>";
      inherits: false;
      initial-value: 0px;
    }

    @property --scroll-fade-e {
      syntax: "<length-percentage>";
      inherits: false;
      initial-value: 0px;
    }

    @property --scroll-fade-mask {
      syntax: "*";
      inherits: false;
    }

    @keyframes scroll-fade-reveal-t {
      from {
        --scroll-fade-t: 0px;
      }
      to {
        --scroll-fade-t: var(--_scroll-fade-size-t, min(12%, 2.5rem));
      }
    }

    @keyframes scroll-fade-reveal-b {
      from {
        --scroll-fade-b: var(--_scroll-fade-size-b, min(12%, 2.5rem));
      }
      to {
        --scroll-fade-b: 0px;
      }
    }

    @keyframes scroll-fade-reveal-s {
      from {
        --scroll-fade-s: 0px;
      }
      to {
        --scroll-fade-s: var(--_scroll-fade-size-s, min(12%, 2.5rem));
      }
    }

    @keyframes scroll-fade-reveal-e {
      from {
        --scroll-fade-e: var(--_scroll-fade-size-e, min(12%, 2.5rem));
      }
      to {
        --scroll-fade-e: 0px;
      }
    }

    .scroll-fade-y {
      --_scroll-fade-size-t: var(--scroll-fade-t-size, var(--scroll-fade-size, min(12%, 2.5rem)));
      --_scroll-fade-size-b: var(--scroll-fade-b-size, var(--scroll-fade-size, min(12%, 2.5rem)));
      --scroll-fade-block: linear-gradient(
        to bottom,
        transparent 0,
        #000 var(--scroll-fade-t, 0px),
        #000 calc(100% - var(--scroll-fade-b, 0px)),
        transparent 100%
      );
      -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
      mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
      -webkit-mask-composite: source-in;
      mask-composite: intersect;
      -webkit-mask-repeat: no-repeat;
      mask-repeat: no-repeat;
    }

    .scroll-fade-x {
      --_scroll-fade-size-s: var(--scroll-fade-s-size, var(--scroll-fade-size, min(12%, 2.5rem)));
      --_scroll-fade-size-e: var(--scroll-fade-e-size, var(--scroll-fade-size, min(12%, 2.5rem)));
      --scroll-fade-inline: linear-gradient(
        to right,
        transparent 0,
        #000 var(--scroll-fade-s, 0px),
        #000 calc(100% - var(--scroll-fade-e, 0px)),
        transparent 100%
      );
      -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));
      mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));
      -webkit-mask-composite: source-in;
      mask-composite: intersect;
      -webkit-mask-repeat: no-repeat;
      mask-repeat: no-repeat;
    }

    .scroll-fade-x:where([dir="rtl"], [dir="rtl"] *) {
      --scroll-fade-inline: linear-gradient(
        to left,
        transparent 0,
        #000 var(--scroll-fade-s, 0px),
        #000 calc(100% - var(--scroll-fade-e, 0px)),
        transparent 100%
      );
    }

    @supports (animation-timeline: scroll()) {
      .scroll-fade-y {
        animation:
          scroll-fade-reveal-t 1ms ease-in-out,
          scroll-fade-reveal-b 1ms ease-in-out;
        animation-timeline: scroll(self y), scroll(self y);
        animation-range:
          0 var(--scroll-fade-reveal, 6rem),
          calc(100% - var(--scroll-fade-reveal, 6rem)) 100%;
        animation-fill-mode: both;
      }

      .scroll-fade-x {
        animation:
          scroll-fade-reveal-s 1ms ease-in-out,
          scroll-fade-reveal-e 1ms ease-in-out;
        animation-timeline: scroll(self inline), scroll(self inline);
        animation-range:
          0 var(--scroll-fade-reveal, 6rem),
          calc(100% - var(--scroll-fade-reveal, 6rem)) 100%;
        animation-fill-mode: both;
      }

      .scroll-fade-x.scroll-fade-y {
        animation:
          scroll-fade-reveal-t 1ms ease-in-out,
          scroll-fade-reveal-b 1ms ease-in-out,
          scroll-fade-reveal-s 1ms ease-in-out,
          scroll-fade-reveal-e 1ms ease-in-out;
        animation-timeline: scroll(self y), scroll(self y), scroll(self inline), scroll(self inline);
        animation-range:
          0 var(--scroll-fade-reveal, 6rem),
          calc(100% - var(--scroll-fade-reveal, 6rem)) 100%,
          0 var(--scroll-fade-reveal, 6rem),
          calc(100% - var(--scroll-fade-reveal, 6rem)) 100%;
        animation-fill-mode: both;
      }
    }

    @supports not (animation-timeline: scroll()) {
      .scroll-fade-y {
        --scroll-fade-t: var(--_scroll-fade-size-t);
        --scroll-fade-b: var(--_scroll-fade-size-b);
      }

      .scroll-fade-x {
        --scroll-fade-s: var(--_scroll-fade-size-s);
        --scroll-fade-e: var(--_scroll-fade-size-e);
      }
    }

    .scroll-fade-x.scroll-fade-y {
      -webkit-mask-image: var(--scroll-fade-block), var(--scroll-fade-inline);
      mask-image: var(--scroll-fade-block), var(--scroll-fade-inline);
    }

    [data-variant="outline"][aria-haspopup][aria-expanded="true"],
    [data-variant="ghost"][aria-haspopup][aria-expanded="true"],
    [data-variant="secondary"][aria-haspopup][aria-expanded="true"] {
      background-color: var(--carbon-menu-open-bg);
      color: var(--carbon-menu-open-fg);
    }

    [data-sidebar="menu-button"][aria-haspopup][aria-expanded="true"] {
      background-color: var(--carbon-menu-open-bg);
      color: var(--carbon-menu-open-fg);
    }

    [data-sidebar="menu-action"][aria-expanded="true"] {
      opacity: 1;
    }

    :where([data-slot="combobox-clear"] ~ [data-slot="combobox-trigger"]) {
      display: none;
    }

    [data-slot="drawer-overlay"][data-swiping] {
      transition-duration: 0s;
    }

    :is([data-slot="drawer-popup"], [data-slot="drawer-overlay"])[data-ending-style] {
      transition-duration: calc(var(--drawer-swipe-strength) * 400ms);
    }

    @media (prefers-reduced-motion: reduce) {
      :is([data-slot="drawer-popup"], [data-slot="drawer-overlay"])[data-ending-style] {
        transition-duration: 0s;
      }
    }

    :where([data-slot="alert"]) > svg {
      grid-column: 1;
      grid-row: 1 / span 2;
      margin-inline-end: 0.5rem;
      translate: 0 0.125rem;
    }

    :where([data-slot="alert-title"], [data-slot="alert-description"]) a {
      text-decoration-line: underline;
      text-underline-offset: 3px;
    }

    :where([data-slot="alert-title"], [data-slot="alert-description"]) a:hover {
      color: var(--foreground);
    }

    :where([data-slot="alert-description"]) p:not(:last-child) {
      margin-block-end: 1rem;
    }

    :where([data-slot="card"]) > img:first-child {
      margin-block-start: calc(-1 * var(--carbon-card-spacing));
    }

    :where(.carbon-icons) svg {
      width: var(--carbon-icon-size, 1rem);
      height: var(--carbon-icon-size, 1rem);
      flex-shrink: 0;
      pointer-events: none;
    }

    :where([data-slot="field-description"]) a {
      text-decoration-line: underline;
      text-underline-offset: 4px;
    }

    :where([data-slot="field-description"]) a:hover {
      color: var(--primary);
    }

    :where([data-slot="resizable-handle"]:is([data-separator="hover"], [data-separator="active"])) {
      color: var(--ring);
    }

    ```

12. Import both stylesheets at the top of your entry file, `theme.css` first.

    ```tsx title="main.tsx"
    import "./styles/theme.css"
    import "./styles/interop.css"
    ```

    Render a component to check the setup. A failed token resolution drops the
    declaration without failing the build, so a successful build proves nothing.

    > **Note**
    >
    > The first `vite` start after installing can show an "Invalid empty selector"
    > overlay from the StyleX plugin. Reload the page once. The production build is
    > not affected.

## Dark mode

`color-scheme` controls dark mode. Every themed token is a `light-dark()` pair, and
`theme.css` sets `color-scheme: light dark` on `:root`, so the page follows the
OS with no JS. Anything that differs between light and dark is a `light-dark()`
pair, written once, in `tokens.stylex.ts` or in the one component that uses it.

A theme *toggle* only has to force the scheme. `.light` and `.dark` in
`theme.css` pin `color-scheme`, and every token follows. Set the class before
first paint or a forced theme flashes:

```html title="index.html"
<script>
  ;(function () {
    try {
      var t = localStorage.getItem("theme")
      if (t === "dark" || t === "light") document.documentElement.classList.add(t)
    } catch (e) {}
  })()
</script>
```

> **Note**
>
> The storage key is yours to choose, but it must match the one your theme
> switcher writes. A mismatch means the pre-paint script and the toggle disagree
> until the first click.

`light-dark()` only works on colors. A non-color value that differs by theme,
such as a transform or a `font-style`, needs two rules: a `.dark` override for
the forced scheme, plus a `@media (prefers-color-scheme: dark) { :root:not(.light) … }`
twin for system mode.

## Conventions

Every component takes overrides the same way:

```tsx
<Button xstyle={styles.pill} />
```

`xstyle` takes StyleX styles, and the component merges it last, so it wins.
`style` is still real inline CSS, for values only the runtime knows. `className`
is a hook for plain CSS. It is concatenated and nothing resolves conflicts, so
anything that must beat a component's own declaration goes through `xstyle`.

### Field state

`Field` publishes its resolved state for plain layout parts through `field-state.ts`.

```ts title="lib/field-state.ts"
"use client"

import { createContext } from "react"
import type { Field } from "@base-ui/react/field"

export const FieldStateContext = createContext<Field.Root.State | null>(null)
```
