# Styling How styling works in `@luke-ui/react`, for contributors who maintain it. Public usage docs live in the docs app MDX. Luke UI uses Vanilla Extract for styling. Public component APIs, utility props, cascade layers, and token custom properties are the stable contracts. Vanilla Extract types are not part of the published TypeScript surface. ## Setup Luke UI ships one static stylesheet for its reset, theme root, recipes, and utilities. 1. Import `@luke-ui/react/stylesheet.css`. 2. Apply `rootClassName` from `@luke-ui/react/theme` to ``, `
`, or an app shell. 3. Import one bundled theme stylesheet, for example `@luke-ui/react/themes/tactile/stylesheet.css`. The theme stylesheet themes the document from `:root`. It needs no class and no JS. None of these steps inject styles at runtime. ## Structure Paths below are relative to `packages/@luke-ui/react/src/`. | Area | Role | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `core/styles/` | Stylesheet graph, layers, reset, theme root, recipe engine, modules registry, utilities, and shared helpers that emit no CSS on their own | | Component and primitive folders under `core/` | Colocate `recipe.css.ts` (public) and `styles.css.ts` (private) beside the owner | | `theme/` | Token contract, `defineTheme`, foundations, bundles, and the build pipeline | | `scripts/build-themes.ts` | Writes `.generated/themes//stylesheet.css`, which `vp pack` copies into `dist/` | Stable entry points: - Stylesheet graph: `core/styles/index.css.ts` - Modules registry: `core/styles/modules.css.ts` - Recipe engine: `core/styles/recipe.ts` for authoring, `core/styles/recipe-engine.ts` for the runtime that components import - Layer helpers: `core/styles/layered-style.css.ts` - Token contract: `theme/contract.ts`, `theme/contract.css.ts`, and `theme/type-styles.ts` - Theme authoring: `theme/define-theme.ts` - Colour pipeline: [THEME_COLOUR_GENERATION.md](THEME_COLOUR_GENERATION.md) `modules.css.ts` imports every shipped `recipe.css.ts` and `styles.css.ts`, plus primitive and overlay modules. Named layers set cross-layer priority. Within a layer, later equal-specificity rules win. Put overridden modules first — for example `text/recipe.css` before `code/recipe.css` and `kbd/recipe.css`. Generators append imports and do not sort this list. Inherited custom properties ignore source order. ## Themes `defineTheme(input)` from `@luke-ui/react/theme` is the sole public theme-authoring surface. It is pure and Node-compatible. It normalises a curated `ThemeInput` into static CSS and throws `ThemeContrastError` when a pair misses WCAG 2.2 AA contrast. The raw `ThemeFoundation` object and `buildTheme` are internal. Colour tokens come from private 12-step scales and elevation surfaces, then map onto the semantic contract. See [THEME_COLOUR_GENERATION.md](THEME_COLOUR_GENERATION.md) for that pipeline. Type styles are grouped from `font.caption` through `font.display`. Keep family, size, weight, line height, letter spacing, and Capsize trims together. `font.family.code` is a fixed monospace stack. Icon sizes run from `xsmall` to `large` and are emitted as `rem`. `ICON_SIZE_VALUES` in `src/theme/token-values.ts` sets their dimensions. Author `depth.*` and `actionControlFinish.*` per mode as final CSS values. Components pick semantic tokens. They do not branch on theme identity. Bundled themes (`tactile`, `paper`) ship precompiled. Each stylesheet pairs `:where(:root)` with a `.luke-ui-theme-` identity class. Apply the bundled `themeClassName` export only when a document needs more than one theme at once. Without `data-color-mode`, a themed subtree follows `prefers-color-scheme`. Set `data-color-mode="light"` or `data-color-mode="dark"` to force a mode. Nested scopes can override it. Every scope also sets native `color-scheme`. A colour mode scoped below `` does not reach a body-level portal. Set `data-color-mode` on `` when a portalled surface must follow an explicit mode. ## Cascade layers All styles live in named CSS cascade layers. Layer order sets cross-layer priority. Specificity and source order still decide conflicts within a layer. | Layer | Purpose | | ----------- | ------------------------------------------------------------------------------------------- | | `reset` | Browser defaults, box sizing, root base colour, body typography, focus, and reduced-motion | | `base` | Reserved for the consuming app (for example Tailwind Preflight). Luke UI emits nothing here | | `recipes` | All Luke UI component styling, including descendant and combinator selectors | | `utilities` | One-off layout and override escape hatches | The public stylesheet starts with one combined order statement: ```css @layer reset, base, recipes, utilities; ``` The package declares `base` but never writes to it. That pins its rank between `reset` and `recipes`. If a consumer's first `@layer base` write creates the layer instead, the browser places it last and it beats every component recipe. The docs app uses `@vanilla-extract/css` for docs-owned `.css.ts` files. Its Vite and Vitest configs run the Vanilla Extract plugin. Import a generated class from the component or route that applies it. The root route applies a `base`-layer class from `src/styles/docs-root.css.ts` to `` for the page's flex layout. Docs-owned shell, navigation, search, and theme controls use `recipes`-layer classes. Keep docs-owned base styles in `base`, and put intentional component overrides in a higher layer. The layer order is declared before imports in `apps/docs/src/styles/app.css`. Author component CSS with one of: - `recipe()` — component visuals with selection and variants. Styles go in the `recipes` layer. - `style()` — one private `recipes` class with no selection (scopes, markers, implementation classes). - `globalStyleInLayer()` — a global selector in a chosen layer. Use for reset/root rules and for component-owned descendant or combinator selectors that still belong in `recipes`. Authors never name the `recipes` layer. Only `globalStyleInLayer()` takes a layer, and it rejects `base`. Put overrides that must beat recipes in the `utilities` layer. Use `!important` only to beat un-layered or inline styles. Under `!important`, lower layers win over higher layers. Loading skeleton masks that must stick on wrapped children use `!important` in `recipes` for that reason. Reduced-motion handling belongs near the animation. The global `prefers-reduced-motion` rule lives in `reset`, so it cannot disable animations in `recipes` or `utilities`. Add a local `@media (prefers-reduced-motion: reduce)` override in any animated recipe. ## Recipes Public recipes export from the owning component or primitive entrypoint. Keep them separate from layout utilities. Colocate recipe files beside their owner: - `recipe.css.ts` — public recipe contract - `styles.css.ts` — private implementation styling Build every recipe with the internal `recipe()` engine from `core/styles/recipe.ts`. It is not a public package entry. `recipe()` wraps every base, variant, and compound-variant style in the `recipes` layer. Pass optional `className` into the recipe. It appends after the recipe's own classes: ```tsx