# Use with AI tools Query typestyles documentation from LLM crawlers, coding agents, and chat assistants TypeStyles documentation is built for humans **and** AI tools. Every page has a clean Markdown sibling, a machine-readable index, and a remote MCP server you can add to your editor. ## Quick links | Resource | URL | | ------------------- | ------------------------------------------------------------------- | | Documentation index | [llms.txt](https://typestyles.dev/llms.txt) | | Full doc bundle | [llms-full.txt](https://typestyles.dev/llms-full.txt) | | JSON index | [docs-index.json](https://typestyles.dev/docs-index.json) | | MCP server | [https://typestyles.dev/mcp](https://typestyles.dev/mcp) | | MCP discovery | [.well-known/mcp.json](https://typestyles.dev/.well-known/mcp.json) | Any doc page is also available as Markdown: append `.md` to the HTML path — for example [getting-started.md](https://typestyles.dev/docs/getting-started.md). ## MCP server tools The MCP server at `https://typestyles.dev/mcp` exposes read-only tools: - **`search_docs`** — keyword search across all pages - **`get_doc`** — full clean markdown for one page by slug - **`list_docs`** — all pages grouped by sidebar category - **`lookup_api`** — API reference sections by symbol (e.g. `styles.component`) - **`get_examples`** — code blocks from the docs by topic - **`get_changelog`** — package changelogs, optionally one version Doc pages are also available as MCP resources at `typestyles://docs/`. ## Add to your editor ### Claude Code ```bash claude mcp add --transport http typestyles https://typestyles.dev/mcp ``` ### Cursor Add to your MCP config (`.cursor/mcp.json` in your project or global Cursor settings): ```json { "mcpServers": { "typestyles": { "url": "https://typestyles.dev/mcp" } } } ``` ### VS Code Add to your user or workspace MCP settings: ```json { "servers": { "typestyles": { "type": "http", "url": "https://typestyles.dev/mcp" } } } ``` ### ChatGPT Use **Connectors** or custom GPT actions with the MCP endpoint `https://typestyles.dev/mcp` (Streamable HTTP transport). ## On-page actions Every documentation page includes **Copy as Markdown**, **View as Markdown**, and **Open in AI** actions next to the title. Use them to paste a page into Claude or ChatGPT with one click. ## In your project The `typestyles` npm package ships an [`llms.txt`](https://www.npmjs.com/package/typestyles) with API patterns and links to the full docs. Agents working inside consumer repos can read it from `node_modules/typestyles/llms.txt`. The [typestyles monorepo](https://github.com/type-styles/typestyles) includes an `AGENTS.md` at the repo root with monorepo layout and build commands. Source: https://typestyles.dev/docs/ai-tools --- # Animation Patterns Common animation patterns and techniques with typestyles TypeStyles supports CSS animations through the `keyframes` API. This guide covers common animation patterns and best practices. ## Basic animations ### Fade in ```ts import { keyframes, styles } from 'typestyles'; const fadeIn = keyframes.create('fadeIn', { from: { opacity: 0 }, to: { opacity: 1 }, }); const card = styles.component('card', { base: { animation: `${fadeIn} 300ms ease`, }, }); ``` ### Slide in ```ts const slideInRight = keyframes.create('slideInRight', { from: { opacity: 0, transform: 'translateX(20px)', }, to: { opacity: 1, transform: 'translateX(0)', }, }); const slideInLeft = keyframes.create('slideInLeft', { from: { opacity: 0, transform: 'translateX(-20px)', }, to: { opacity: 1, transform: 'translateX(0)', }, }); const slideInUp = keyframes.create('slideInUp', { from: { opacity: 0, transform: 'translateY(20px)', }, to: { opacity: 1, transform: 'translateY(0)', }, }); const slideInDown = keyframes.create('slideInDown', { from: { opacity: 0, transform: 'translateY(-20px)', }, to: { opacity: 1, transform: 'translateY(0)', }, }); ``` ### Scale animations ```ts const scaleIn = keyframes.create('scaleIn', { from: { opacity: 0, transform: 'scale(0.9)', }, to: { opacity: 1, transform: 'scale(1)', }, }); const scaleOut = keyframes.create('scaleOut', { from: { opacity: 1, transform: 'scale(1)', }, to: { opacity: 0, transform: 'scale(0.9)', }, }); const popIn = keyframes.create('popIn', { '0%': { transform: 'scale(0)' }, '70%': { transform: 'scale(1.1)' }, '100%': { transform: 'scale(1)' }, }); ``` ## Common UI animations ### Loading spinner ```ts const spin = keyframes.create('spin', { from: { transform: 'rotate(0deg)' }, to: { transform: 'rotate(360deg)' }, }); const spinner = styles.component('spinner', { base: { display: 'inline-block', width: '24px', height: '24px', border: '3px solid #e5e7eb', borderTopColor: '#3b82f6', borderRadius: '50%', animation: `${spin} 800ms linear infinite`, }, sm: { width: '16px', height: '16px', borderWidth: '2px', }, lg: { width: '32px', height: '32px', borderWidth: '4px', }, }); ``` ### Pulse animation ```ts const pulse = keyframes.create('pulse', { '0%, 100%': { opacity: 1 }, '50%': { opacity: 0.5 }, }); const skeleton = styles.component('skeleton', { base: { backgroundColor: '#e5e7eb', borderRadius: '4px', animation: `${pulse} 2s ease-in-out infinite`, }, }); ``` ### Shake animation (error state) ```ts const shake = keyframes.create('shake', { '0%, 100%': { transform: 'translateX(0)' }, '10%, 30%, 50%, 70%, 90%': { transform: 'translateX(-4px)' }, '20%, 40%, 60%, 80%': { transform: 'translateX(4px)' }, }); const input = styles.component('input', { base: { ... }, error: { borderColor: '#ef4444', animation: `${shake} 500ms ease-in-out`, }, }); ``` ### Bounce animation ```ts const bounce = keyframes.create('bounce', { '0%, 100%': { transform: 'translateY(0)', animationTimingFunction: 'cubic-bezier(0.8, 0, 1, 1)', }, '50%': { transform: 'translateY(-25%)', animationTimingFunction: 'cubic-bezier(0, 0, 0.2, 1)', }, }); const notification = styles.component('notification', { badge: { animation: `${bounce} 1s infinite`, }, }); ``` ## Page transitions ### Page fade ```ts const pageFadeIn = keyframes.create('pageFadeIn', { from: { opacity: 0 }, to: { opacity: 1 }, }); const pageLayout = styles.component('page', { container: { animation: `${pageFadeIn} 300ms ease`, }, }); ``` ### Staggered list items ```ts // Individual item animation const listItemFadeIn = keyframes.create('listItemFadeIn', { from: { opacity: 0, transform: 'translateY(10px)', }, to: { opacity: 1, transform: 'translateY(0)', }, }); // Delay classes for stagger effect const list = styles.component('list', { item: { opacity: 0, animation: `${listItemFadeIn} 300ms ease forwards`, }, itemDelay1: { animationDelay: '50ms' }, itemDelay2: { animationDelay: '100ms' }, itemDelay3: { animationDelay: '150ms' }, itemDelay4: { animationDelay: '200ms' }, itemDelay5: { animationDelay: '250ms' }, }); // React usage with index function List({ items }) { return ( ); } ``` ## Micro-interactions ### Button press ```ts const buttonPress = keyframes.create('buttonPress', { '0%': { transform: 'scale(1)' }, '50%': { transform: 'scale(0.95)' }, '100%': { transform: 'scale(1)' }, }); const button = styles.component('button', { base: { transition: 'transform 100ms ease', '&:active': { transform: 'scale(0.95)', }, }, // Or with keyframe for more control press: { animation: `${buttonPress} 150ms ease`, }, }); ``` ### Checkbox check ```ts const checkmark = keyframes.create('checkmark', { '0%': { strokeDashoffset: 100, }, '100%': { strokeDashoffset: 0, }, }); const checkbox = styles.component('checkbox', { base: { ... }, checkmark: { strokeDasharray: 100, strokeDashoffset: 100, }, checked: { '& .checkmark': { animation: `${checkmark} 200ms ease forwards`, }, }, }); ``` ### Hover lift effect ```ts const card = styles.component('card', { base: { transition: 'transform 200ms ease, box-shadow 200ms ease', }, interactive: { cursor: 'pointer', '&:hover': { transform: 'translateY(-4px)', boxShadow: '0 12px 24px rgba(0, 0, 0, 0.15)', }, }, }); ``` ## Complex animations ### Modal enter/exit ```ts const modalBackdropFadeIn = keyframes.create('modalBackdropFadeIn', { from: { opacity: 0 }, to: { opacity: 1 }, }); const modalBackdropFadeOut = keyframes.create('modalBackdropFadeOut', { from: { opacity: 1 }, to: { opacity: 0 }, }); const modalContentSlideIn = keyframes.create('modalContentSlideIn', { from: { opacity: 0, transform: 'scale(0.95) translateY(10px)', }, to: { opacity: 1, transform: 'scale(1) translateY(0)', }, }); const modalContentSlideOut = keyframes.create('modalContentSlideOut', { from: { opacity: 1, transform: 'scale(1) translateY(0)', }, to: { opacity: 0, transform: 'scale(0.95) translateY(10px)', }, }); const modal = styles.component('modal', { backdrop: { position: 'fixed', inset: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', }, backdropEnter: { animation: `${modalBackdropFadeIn} 200ms ease`, }, backdropExit: { animation: `${modalBackdropFadeOut} 200ms ease forwards`, }, content: { backgroundColor: 'white', borderRadius: '8px', padding: '24px', }, contentEnter: { animation: `${modalContentSlideIn} 300ms ease`, }, contentExit: { animation: `${modalContentSlideOut} 200ms ease forwards`, }, }); ``` ### Toast notifications ```ts const toastSlideIn = keyframes.create('toastSlideIn', { from: { opacity: 0, transform: 'translateX(100%)', }, to: { opacity: 1, transform: 'translateX(0)', }, }); const toastSlideOut = keyframes.create('toastSlideOut', { from: { opacity: 1, transform: 'translateX(0)', }, to: { opacity: 0, transform: 'translateX(100%)', }, }); const toastProgress = keyframes.create('toastProgress', { from: { transform: 'scaleX(1)' }, to: { transform: 'scaleX(0)' }, }); const toast = styles.component('toast', { base: { padding: '16px 20px', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', }, enter: { animation: `${toastSlideIn} 300ms ease`, }, exit: { animation: `${toastSlideOut} 300ms ease forwards`, }, progressBar: { height: '3px', backgroundColor: 'rgba(255, 255, 255, 0.3)', marginTop: '12px', transformOrigin: 'left', animation: `${toastProgress} 5000ms linear forwards`, }, success: { backgroundColor: '#10b981', color: 'white', }, error: { backgroundColor: '#ef4444', color: 'white', }, info: { backgroundColor: '#3b82f6', color: 'white', }, }); ``` ### Skeleton loading ```ts const shimmer = keyframes.create('shimmer', { '0%': { backgroundPosition: '-200% 0' }, '100%': { backgroundPosition: '200% 0' }, }); const skeleton = styles.component('skeleton', { base: { backgroundColor: '#e5e7eb', borderRadius: '4px', }, animated: { background: 'linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%)', backgroundSize: '200% 100%', animation: `${shimmer} 1.5s infinite`, }, text: { height: '1em', marginBottom: '0.5em', }, textLarge: { height: '1.5em', }, circle: { borderRadius: '50%', }, avatar: { width: '40px', height: '40px', }, card: { height: '200px', }, }); ``` ## Scroll animations ### Infinite scroll indicator ```ts const scrollIndicator = keyframes.create('scrollIndicator', { '0%, 100%': { opacity: 1, transform: 'translateY(0)', }, '50%': { opacity: 0.5, transform: 'translateY(6px)', }, }); const scrollCue = styles.component('scroll-cue', { base: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px', }, arrow: { animation: `${scrollIndicator} 1.5s ease-in-out infinite`, }, arrowDelay1: { animationDelay: '0ms' }, arrowDelay2: { animationDelay: '150ms' }, arrowDelay3: { animationDelay: '300ms' }, }); ``` ## Best practices ### Performance ```ts // ✅ Use transform and opacity for smooth animations const goodAnimation = keyframes.create('goodAnimation', { from: { opacity: 0, transform: 'translateY(20px)', // GPU accelerated }, to: { opacity: 1, transform: 'translateY(0)', }, }); // ❌ Avoid animating layout properties const badAnimation = keyframes.create('badAnimation', { from: { height: 0, // Triggers layout marginTop: 0, // Triggers layout }, to: { height: '100px', marginTop: '20px', }, }); ``` ### Accessibility ```ts // Respect reduced motion preference const animated = styles.component('animated', { base: { '@media (prefers-reduced-motion: reduce)': { animation: 'none', transition: 'none', }, }, }); ``` ### Timing functions ```ts // Standard curves const ease = 'ease'; // Standard const easeIn = 'ease-in'; // Accelerate const easeOut = 'ease-out'; // Decelerate const easeInOut = 'ease-in-out'; // Both // Custom cubic-bezier for specific feels const springy = 'cubic-bezier(0.68, -0.55, 0.265, 1.55)'; const smooth = 'cubic-bezier(0.4, 0, 0.2, 1)'; const enter = 'cubic-bezier(0, 0, 0.2, 1)'; const exit = 'cubic-bezier(0.4, 0, 1, 1)'; // Usage const button = styles.component('button', { base: { transition: `transform 200ms ${springy}`, }, }); ``` ### Duration guidelines | Animation Type | Duration | Use Case | | ----------------- | --------- | --------------------------- | | Micro-interaction | 100-200ms | Button press, hover states | | Standard | 200-300ms | Modals, dropdowns, tooltips | | Complex | 300-500ms | Page transitions, reveals | | Ambient | 1-10s | Loading states, skeletons | ### Naming conventions ```ts // Use descriptive names const fadeIn = keyframes.create('fadeIn', { ... }); const slideUp = keyframes.create('slideUp', { ... }); const modalEnter = keyframes.create('modalEnter', { ... }); const modalExit = keyframes.create('modalExit', { ... }); // Include direction when applicable const slideInRight = keyframes.create('slideInRight', { ... }); const slideInLeft = keyframes.create('slideInLeft', { ... }); const slideOutUp = keyframes.create('slideOutUp', { ... }); ``` ## Animation utilities ### Animation delay helper ```ts function createStaggeredAnimation( baseName: string, itemCount: number, baseDelay: number ): Record { const delays: Record = {}; for (let i = 0; i < itemCount; i++) { delays[`delay${i + 1}`] = `${baseDelay * (i + 1)}ms`; } return delays; } // Usage in React function StaggeredList({ items }) { const delayClass = (index: number) => { const delays = ['delay1', 'delay2', 'delay3', 'delay4', 'delay5']; return delays[index] || 'delay5'; }; return (
    {items.map((item, index) => (
  • {item.name}
  • ))}
); } ``` ### Animation presets ```ts // animations/presets.ts export const presets = { fade: { in: 'fadeIn 300ms ease', out: 'fadeOut 200ms ease', }, slide: { up: 'slideUp 300ms ease', down: 'slideDown 300ms ease', left: 'slideLeft 300ms ease', right: 'slideRight 300ms ease', }, scale: { in: 'scaleIn 200ms ease', out: 'scaleOut 200ms ease', }, bounce: 'bounce 500ms ease', pulse: 'pulse 2s ease-in-out infinite', spin: 'spin 1s linear infinite', shake: 'shake 500ms ease-in-out', } as const; // Usage const modal = styles.component('modal', { enter: { animation: presets.fade.in, }, }); ``` ## Summary 1. **Use transform and opacity** - Best performance 2. **Respect reduced motion** - Accessibility first 3. **Keep animations purposeful** - Don't animate just because you can 4. **Use appropriate timing** - Match animation to importance 5. **Consider physics** - Natural motion feels better 6. **Test on real devices** - Performance varies 7. **Name descriptively** - Make intent clear 8. **Reuse animations** - Create a preset library 9. **Document timing** - Help other developers 10. **Test with users** - Get feedback on feel Source: https://typestyles.dev/docs/animation-patterns --- # API Reference Complete API reference for typestyles Auto-generated documentation for all typestyles APIs. ## Core Exports ### `styles` Default style API (semantic class names, empty `scopeId`). Prefer `createStyles({ scopeId, mode, prefix })` per package or micro-frontend for isolation. **Methods:** - `styles.component(namespace, config)`: Create multi-variant component styles (CVA-style) - `styles.class(name, properties)`: Create a single class - `styles.container(…)`: Build typed `@container` keys for nested styles (also exported as `container`). Object/two-arg forms infer a **literal** `@container …` string, so `[container({ minWidth: 400 })]: { … }` works next to longhands without casting; use `atRuleBlock` when the key is only known as a generic `string`. - `styles.has(…)`, `styles.is(…)`, `styles.where(…)`: Build nested `&`-keys for `:has()`, `:is()`, and `:where()` (same as the `has` / `is` / `where` exports). Literal arguments narrow to a concrete `&:…` key so you can mix them with longhands as `[has('.x')]: { … }` without `as CSSProperties`. `:where()` keeps **zero specificity**; raw `'&:has(…)'` strings still work. - `styles.atRuleBlock(key, nested)`: Spreadable `{ [@key]: nested }` so `@…` keys type-check (also exported as `atRuleBlock`) - `styles.containerRef(label)`: Readable `{scopeId}-{label}` or `{prefix}-{label}` `container-name` (see `createContainerRef`) - `styles.hashClass(properties, label?)`: Create a deterministic hashed class - `styles.property(id, options?)`: Register a standalone CSS custom property (optional `@property` when `syntax` is set); returns `{ name, var, toString }` - `styles.compose(...fns)`: Compose multiple style functions - `styles.withUtils(utils)`: Create a utility-aware styles API (prefer `createStyles({ utils })` for a single instance) - `styles.classNaming`: Read-only resolved naming config for the default `styles` instance **Named exports (same behavior as `styles.*`):** `container`, `createContainerRef`, `atRuleBlock`, `has`, `is`, `where`. **Related types:** `ContainerQueryKey`, `ContainerObjectKey`, `HasNestedKey`, `IsNestedKey`, `WhereNestedKey`, `IsPseudoArg`. See [Custom selectors & at-rules](/docs/custom-at-rules) and [TypeScript tips](/docs/typescript-tips). ### `createStyles(options?)` Returns a new style API (same shape as `styles`) with its own class naming config. Pass `Partial`: `mode` (`'semantic' | 'hashed' | 'compact' | 'atomic'`), `prefix`, `scopeId`. Optionally pass **`utils`** — a map of shorthand expanders — to get a utility-aware API in one step (same typing as `styles.withUtils(…)`; see [Styles](/docs/styles#utility-shortcuts)). Optionally pass **`layers`** (tuple or `{ order, prependFrameworkLayers? }`) to enable **`@layer`** output; then every **`class`**, **`hashClass`**, and **`component`** call must include a third argument **`{ layer: '…' }`** (see [Cascade layers](/docs/cascade-layers)). The default `import { styles } from 'typestyles'` is `createStyles()` with default options. ### `tokens` Default token API (unscoped custom properties). Prefer `createTokens({ scopeId })` when multiple bundles share a page. **Methods:** - `tokens.create(namespace, values)`: Creates CSS custom properties; returns a branded `CreatedTokenRef`. Leaf values may use `{ value, syntax?, inherits? }` to register `@property` and return `{ name, var, toString }` refs instead of plain `var(...)` strings. - `tokens.use(namespace | createdRef)`: References existing tokens; infers types from a `tokens.create()` return value or a `createTokens()` generic - `tokens.createTheme(name, config)`: Registers a theme class that overrides token custom properties - `tokens.createDarkMode(name, darkOverrides)`: Shorthand theme with a single dark `@media` branch - `tokens.when` / `tokens.colorMode`: Condition helpers for themes - `tokens.scopeId`: The scope passed to `createTokens`, if any ### `createTokens(options?)` Returns a token + theme API bound to an optional `scopeId`. When set, `tokens.create('color', …)` emits `--{scopeId}-color-*` variables and `tokens.createTheme('dark', …)` registers `.theme-{scopeId}-dark` (sanitized segments). With **`layers`**, **`tokenLayer`** is required and token/theme CSS is wrapped in that layer. Optional **`nameTemplate`** on the instance or per `tokens.create` call controls emitted `--*` names (see [Tokens](/docs/tokens#custom-css-variable-names-nametemplate)). The default `import { tokens } from 'typestyles'` is `createTokens()` (no scope). Exported types: **`TokenNameContext`**, **`TokenNameTemplate`**, **`FlatTokenPathEntry`**. Helper: **`flattenTokenPaths`** (segment-preserving flatten for custom templates). ### `keyframes` Keyframe animation API. **Methods:** - `keyframes.create(name, stops)`: Creates @keyframes animation ### `color` (`typestyles/color`) Type-safe CSS color function helpers, on a **separate subpath** so the main `typestyles` entry stays lean. Import `color` (namespace) or named functions from `typestyles/color`. Each function returns a plain CSS color string — no runtime color math. Composes naturally with token references. **Functions:** - `color.rgb(r, g, b, alpha?)`: RGB color - `color.hsl(h, s, l, alpha?)`: HSL color - `color.oklch(l, c, h, alpha?)`: OKLCH color - `color.oklab(l, a, b, alpha?)`: OKLAB color - `color.lab(l, a, b, alpha?)`: LAB color - `color.lch(l, c, h, alpha?)`: LCH color - `color.hwb(h, w, b, alpha?)`: HWB color - `color.mix(c1, c2, p?, space?)`: Mix two colors - `color.lightDark(light, dark)`: Light/dark mode color - `color.alpha(color, opacity, space?)`: Adjust opacity See [Color](/docs/color). ### `color-scale` (`typestyles/color-scale`) OKLCH ramp generation and WCAG contrast math, on a **separate subpath** (zero impact on main bundle size). **Functions:** - `parseColor(hex)`: Hex → `{ l, c, h }` in OKLCH units (hex only in v1) - `generateRamp({ hue, chroma, steps?, lightnessRange? })`: Perceptual OKLCH ramp (lightest → darkest) - `contrastRatio(colorA, colorB)`: WCAG relative luminance ratio (hex or `oklch()` strings) See [Theming Patterns — Generating a theme from one accent color](/docs/theming-patterns#generating-a-theme-from-one-accent-color). ### `token-scale` (`typestyles/token-scale`) Generic numeric ramp generators for type/motion/radius ladders, on a **separate subpath** (zero impact on main bundle size). Pure numbers in, pure numbers out — naming steps is a design-system concern. **Functions:** - `generateGeometricScale({ base, ratio, steps, round? })`: `base * ratio ** offset` for each signed integer offset in `steps` (font-size-style ladders) - `generateLinearScale({ base, multiplier, steps, round? })`: `base * step * multiplier` for each ordinal in `steps` (radius-style ladders) - `expandDurationBand({ base, ratio, roundTo? })`: `{ min, base, max }` motion band (`min = base * ratio`, `max = base / ratio`, rounded to nearest 5 by default) See [Theming Patterns — Generating type, motion, and radius scales](/docs/theming-patterns#generating-type-motion-and-radius-scales). ### `calc` and `clamp` Helpers for CSS `calc()` and `clamp()` that always emit balanced outer parentheses: - **`calc`** — tagged template: `` calc`100vh - ${token}` `` → `calc(100vh - …)` - **`clamp(min, preferred, max)`** — three arguments → `clamp(min, preferred, max)` See [TypeScript Tips — Complex CSS values](/docs/typescript-tips). ### `createTypeStyles(options)` Returns **`{ styles, tokens, global }`** with one shared **`scopeId`** (and optional **`mode`**, **`prefix`**, **`layers`**, **`tokenLayer`**). When **`layers`** is omitted, behavior matches separate **`createStyles()`** + **`createTokens()`** (no `@layer` in output). When **`layers`** is set, **`tokenLayer`** is required and both APIs use the same cascade-layer stack. See [Cascade layers](/docs/cascade-layers). **Default singleton:** `import { styles, tokens } from 'typestyles'` is the same as calling `createStyles()` and `createTokens()` with **no** `scopeId`. That is fine for throwaway demos, but **prefer `createTypeStyles({ scopeId })`** in real apps and libraries so tokens and themes stay namespaced. Add **`global`** from the same constructor when you register [cascade layers](/docs/cascade-layers) or shared `@layer` stacks. ### Cascade layers (types) Exported types include **`CascadeLayersInput`**, **`CascadeLayersObjectInput`**, **`ResolvedCascadeLayers`**, and **`ThemeEmitLayerContext`** (theme emission with layers). ### `global` Global CSS helpers (not scoped to a component class): - `global.style(selector, styles)`: Insert rules for an arbitrary selector. Rules dedupe by an internal key (`scopeId` + selector + layer when layered). A second call with the **same** key and **different** CSS is skipped; in non-`production` builds, TypeStyles logs a **console warning** so overlapping selectors (for example reset `body` plus your own `body`) are not silent failures. Reuse one call, merge properties, or use a more specific selector (e.g. `html body`). - `global.fontFace(family, props)`: Register `@font-face` (supports `src` as a string or array of fragments, variable font weight ranges, `font-display`, `unicode-range`, and metric overrides — see [Fonts](/docs/fonts)) ### `cx(...parts)` Joins class name parts into a single string, filtering out falsy values (`false`, `undefined`, `null`, `0`, `''`). Use `cx` to combine TypeStyles classes with external class strings and conditional expressions. ```ts import { cx, styles } from 'typestyles'; const card = styles.component('card', { base: { padding: '16px' }, elevated: { boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }, }); cx(card(), isElevated && card.elevated, externalClassName); ``` ### CSS variables (dynamic styling) - [`createVar(name?, fallback?)`](/docs/dynamic-styles), [`assignVars(vars)`](/docs/dynamic-styles): Typed custom property helpers for per-instance dynamic values. Pass a debug name (e.g. `createVar('cardBg')`) for readable DevTools property names. ### Sheet and testing utilities - `getRegisteredCss()`: Returns all CSS registered so far (useful with SSR or diagnostics) - `subscribeRegisteredCss(listener)`: Subscribe to CSS registration changes; returns an unsubscribe function. Compatible with React `useSyncExternalStore` (used by `@typestyles/next` `useTypestyles`) - `reset()`, `flushSync()`, `ensureDocumentStylesAttached()`: Primarily for tests and advanced setup; see [Testing](/docs/testing) - `insertRules(rules)`: Low-level rule insertion (mainly for library authors) ### SSR helpers (`typestyles/server`) - `collectStyles(renderFn)`: Wrap a sync or async render; returns `{ html, css }`. Request-isolated on Node via `AsyncLocalStorage`. See [SSR](/docs/ssr). - `TYPESTYLES_STYLE_ID`: Stable `"typestyles"` id for the managed `` (empty string when `css` is empty) - `injectStylesIntoHtml(html, css)`: Insert collected CSS before `` - `streamingDocumentShell(css)`: Open doctype + `` + `` for `renderToPipeableStream` (pair with `collectStyles` for the CSS pass) ### Class naming helpers - `mergeClassNaming(partial?)`: Build a full `ClassNamingConfig` from partial options - `defaultClassNamingConfig`: Default `mode`, `prefix`, and `scopeId` - `scopedTokenNamespace(scopeId, logicalNamespace)`: CSS variable namespace segment for scoped token instances See [Class naming](/docs/class-naming). ## Usage Examples ### Creating Styles ```ts import { styles } from 'typestyles'; const button = styles.component('button', { base: { padding: '8px 16px' }, variants: { intent: { primary: { backgroundColor: '#0066ff' } }, }, defaultVariants: { intent: 'primary' }, }); button(); // "button-base button-intent-primary" button({ intent: 'primary' }); // same const { base } = button; // destructure class strings ``` ### Creating Tokens ```ts import { tokens } from 'typestyles'; const color = tokens.create('color', { primary: '#0066ff', secondary: '#6b7280', }); color.primary; // "var(--color-primary)" ``` ### Scoped instances (libraries / micro-frontends) ```ts import { createStyles, createTokens } from 'typestyles'; export const styles = createStyles({ scopeId: 'my-ds', mode: 'hashed', prefix: 'ds' }); export const tokens = createTokens({ scopeId: 'my-ds' }); ``` ### `:has()`, `:is()`, `:where()` (nested selectors) Use the helpers as **computed keys** so you keep normal CSS semantics (including `:where`’s zero specificity) with the same “small builder” ergonomics as `container()`: ```ts import { styles } from 'typestyles'; const nav = styles.class('nav', { display: 'flex', [styles.where('.nav')]: { gap: '8px' }, [styles.has('.active')]: { borderBottom: '2px solid blue' }, [styles.is(':hover', ':focus-visible')]: { outline: '2px solid blue' }, }); ``` The named exports `has`, `is`, and `where` are identical to `styles.has` / `styles.is` / `styles.where`. The `IsPseudoArg` type documents common pseudos for `:is()` groups. ### Creating Animations ```ts import { keyframes } from 'typestyles'; const fadeIn = keyframes.create('fadeIn', { from: { opacity: 0 }, to: { opacity: 1 }, }); // Use in styles animation: `${fadeIn} 300ms ease`; ``` --- _This API reference was auto-generated from source code._ _Last updated: 2026-04-06_ Source: https://typestyles.dev/docs/api-reference --- # Atomic CSS Utilities Type-safe atomic CSS utilities with @typestyles/props The `@typestyles/props` package provides a type-safe way to generate atomic CSS utility classes, similar to Tailwind CSS but with full TypeScript inference and zero runtime overhead. Runtime APIs such as `styles.class`, `styles.component`, and `styles.hashClass` use a separate naming system; to change those class strings (semantic vs hashed), see [Class naming](/docs/class-naming). **Compare approaches:** this package (`defineProperties` + `createProps`) powers layout utilities on the docs site ([`docs/src/atoms.ts`](https://github.com/type-styles/typestyles/blob/main/docs/src/atoms.ts)). For hand-written `styles.class` utilities (Tailwind-style), see [examples/typewind](https://github.com/type-styles/typestyles/blob/main/examples/typewind/README.md). ## Installation ```bash npm install @typestyles/props ``` ## Quick Start ```ts import { defineProperties, createProps } from '@typestyles/props'; // Define your atomic utilities const atoms = createProps( 'atom', defineProperties({ properties: { display: ['flex', 'block', 'grid', 'none'], padding: { 0: '0', 1: '4px', 2: '8px', 3: '16px' }, gap: { 0: '0', 1: '4px', 2: '8px', 3: '16px' }, }, }), ); // Use them with full type safety atoms({ display: 'flex', padding: 2, gap: 1, }); // Returns: "atom-display-flex atom-padding-2 atom-gap-1" ``` ## Defining Properties Use `defineProperties()` to define a collection of CSS properties with their allowed values: ### Array-Based Values For enumerated values like display modes: ```ts const utilities = defineProperties({ properties: { display: ['flex', 'block', 'grid', 'none', 'inline-flex'], flexDirection: ['row', 'column', 'row-reverse', 'column-reverse'], alignItems: ['start', 'center', 'end', 'stretch', 'baseline'], }, }); ``` ### Object-Based Values For scale-based values like spacing or font sizes: ```ts const utilities = defineProperties({ properties: { padding: { 0: '0', 1: '0.25rem', 2: '0.5rem', 3: '0.75rem', 4: '1rem', 8: '2rem', }, fontSize: { xs: '0.75rem', sm: '0.875rem', base: '1rem', lg: '1.125rem', xl: '1.25rem', }, }, }); ``` ## Responsive Conditions Add breakpoints and other conditional styles: ```ts const responsive = defineProperties({ conditions: { mobile: { '@media': '(min-width: 640px)' }, tablet: { '@media': '(min-width: 768px)' }, desktop: { '@media': '(min-width: 1024px)' }, }, properties: { display: ['flex', 'block', 'grid'], flexDirection: ['row', 'column'], }, }); const atoms = createProps('atom', responsive); // Apply responsive values atoms({ display: 'block', // Base value flexDirection: { mobile: 'column', desktop: 'row' }, // Responsive }); // Returns: "atom-display-block atom-flexDirection-mobile-column atom-flexDirection-desktop-row" ``` ### Condition Types **Media Queries:** ```ts { mobile: { '@media': '(min-width: 768px)' } } ``` **Container Queries:** ```ts { wide: { '@container': '(min-width: 400px)' } } ``` **Feature Queries:** ```ts { supportsGrid: { '@supports': '(display: grid)' } } ``` **Custom Selectors:** ```ts { hover: { selector: '&:hover'; } } { dark: { selector: '[data-theme="dark"] &'; } } ``` ## Shorthands Define shorthands that expand to multiple properties: ```ts const spacing = defineProperties({ properties: { paddingTop: { 0: '0', 1: '4px', 2: '8px' }, paddingRight: { 0: '0', 1: '4px', 2: '8px' }, paddingBottom: { 0: '0', 1: '4px', 2: '8px' }, paddingLeft: { 0: '0', 1: '4px', 2: '8px' }, }, shorthands: { padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'], paddingX: ['paddingLeft', 'paddingRight'], paddingY: ['paddingTop', 'paddingBottom'], }, }); const atoms = createProps('atom', spacing); atoms({ padding: 2 }); // Expands to: paddingTop, paddingRight, paddingBottom, paddingLeft all set to 2 // Returns: "atom-paddingTop-2 atom-paddingRight-2 atom-paddingBottom-2 atom-paddingLeft-2" atoms({ paddingX: 1, paddingY: 2 }); // Returns: "atom-paddingLeft-1 atom-paddingRight-1 atom-paddingTop-2 atom-paddingBottom-2" ``` ## Combining Property Collections Merge multiple property collections into one props function: ```ts const layout = defineProperties({ properties: { display: ['flex', 'block', 'grid'], position: ['relative', 'absolute', 'fixed', 'sticky'], }, }); const spacing = defineProperties({ properties: { padding: { 0: '0', 1: '4px', 2: '8px' }, margin: { 0: '0', 1: '4px', 2: '8px', auto: 'auto' }, }, }); const colors = defineProperties({ properties: { color: { primary: '#0066ff', secondary: '#6b7280' }, backgroundColor: { white: '#ffffff', gray: '#f3f4f6' }, }, }); // Combine all collections const atoms = createProps('atom', layout, spacing, colors); // Use properties from any collection atoms({ display: 'flex', padding: 2, color: 'primary', }); ``` ## Default Conditions Set a default condition for all property values: ```ts const mobileFirst = defineProperties({ conditions: { tablet: { '@media': '(min-width: 768px)' }, desktop: { '@media': '(min-width: 1024px)' }, }, defaultCondition: false, // No condition by default (mobile-first) properties: { fontSize: { sm: '14px', base: '16px', lg: '18px' }, }, }); const desktopFirst = defineProperties({ conditions: { mobile: { '@media': '(max-width: 768px)' }, }, defaultCondition: 'mobile', // All values get mobile condition by default properties: { fontSize: { sm: '14px', base: '16px', lg: '18px' }, }, }); ``` ## SSR Support CSS is automatically injected and available for SSR: ```ts import { getRegisteredCss } from 'typestyles'; // Define and create your atoms const atoms = createProps( 'atom', defineProperties({ /* ... */ }), ); // CSS is automatically registered const css = getRegisteredCss(); // Contains all atomic utility classes ``` ## Composing with Component Styles Combine atomic utilities with component-specific styles using `styles.compose()`: ```ts import { styles } from 'typestyles'; import { createProps, defineProperties } from '@typestyles/props'; const atoms = createProps( 'atom', defineProperties({ properties: { display: ['flex', 'grid'], gap: { 0: '0', 1: '4px', 2: '8px' }, padding: { 0: '0', 1: '4px', 2: '8px', 3: '16px' }, }, }), ); const card = styles.component('card', { base: { borderRadius: '8px', border: '1px solid #e5e5e5', background: 'white', }, }); // Compose together const flexCard = styles.compose(card, atoms({ display: 'flex', gap: 2, padding: 3 })); flexCard(); // "card-base atom-display-flex atom-gap-2 atom-padding-3" ``` ## Real-World Example Building a complete design system: ```ts import { defineProperties, createProps } from '@typestyles/props'; // Define responsive breakpoints and utilities const atoms = createProps( 'ds', defineProperties({ conditions: { sm: { '@media': '(min-width: 640px)' }, md: { '@media': '(min-width: 768px)' }, lg: { '@media': '(min-width: 1024px)' }, xl: { '@media': '(min-width: 1280px)' }, dark: { selector: '[data-theme="dark"] &' }, hover: { selector: '&:hover' }, }, properties: { // Layout display: ['none', 'flex', 'block', 'grid', 'inline-flex'], flexDirection: ['row', 'column'], alignItems: ['start', 'center', 'end', 'stretch'], justifyContent: ['start', 'center', 'end', 'between', 'around'], // Spacing gap: { 0: '0', 1: '0.25rem', 2: '0.5rem', 3: '0.75rem', 4: '1rem' }, padding: { 0: '0', 1: '0.25rem', 2: '0.5rem', 4: '1rem', 8: '2rem' }, margin: { 0: '0', auto: 'auto', 1: '0.25rem', 2: '0.5rem' }, // Typography fontSize: { xs: '0.75rem', sm: '0.875rem', base: '1rem', lg: '1.125rem' }, fontWeight: { normal: '400', medium: '500', semibold: '600', bold: '700' }, // Colors color: { primary: '#3b82f6', secondary: '#64748b', muted: '#94a3b8' }, backgroundColor: { white: '#fff', gray: '#f1f5f9', transparent: 'transparent' }, }, shorthands: { p: ['padding'], px: ['paddingLeft', 'paddingRight'], py: ['paddingTop', 'paddingBottom'], m: ['margin'], }, }) ); // Use in components export function Button() { return ( ); } ``` ## Class Naming Classes follow a predictable pattern: ``` {namespace}-{property}-{value} {namespace}-{property}-{condition}-{value} ``` Examples: - `atom-display-flex` - `atom-display-mobile-block` - `atom-padding-2` - `atom-padding-desktop-4` ## TypeScript Full type inference and autocomplete for: - Property names - Property values - Condition names - Shorthand names ```ts const atoms = createProps( 'atom', defineProperties({ properties: { display: ['flex', 'block'], }, }), ); atoms({ display: 'flex' }); // ✅ atoms({ display: 'grid' }); // ❌ Type error: 'grid' not in ['flex', 'block'] atoms({ fontSize: '16px' }); // ❌ Type error: 'fontSize' not defined ``` ## Inspecting Properties Check which properties are available: ```ts const atoms = createProps( 'atom', defineProperties({ properties: { display: ['flex', 'block'] }, shorthands: { d: ['display'] }, }), ); atoms.properties.has('display'); // true atoms.properties.has('d'); // true (shorthand) atoms.properties.has('fontSize'); // false ``` ## Performance - **Zero runtime overhead** - All CSS is pre-generated at module load time - **Automatic deduplication** - Classes are only generated once - **Small bundle size** - Only the props function and a lookup map are included - **Fast runtime** - Just string concatenation, no CSS generation at runtime Source: https://typestyles.dev/docs/atomic-css --- # Benchmarks Reproducible performance measurements for TypeStyles — CSS generation, file size, runtime injection, SSR collection, and cross-framework comparison Measured numbers, reproducible methodology, and honest caveats. These benchmarks run in CI on every pull request — regressions beyond thresholds block the build. ## Reference app Both TypeStyles and Vanilla Extract benchmarks use equivalent self-contained reference design systems: - **50 components** — buttons, cards, inputs, modals, layout primitives, etc. - **8 token namespaces** — color, space, radius, typography, shadow, duration, easing, z-index - **3 themes** — dark, high-contrast, warm - **2 keyframe animations**, **4 global styles** This covers dimensioned variants, flat variants, compound variants, nested selectors, media queries, and pseudo-classes — a realistic production design system. The Vanilla Extract version uses `recipe()`, `style()`, `createThemeContract()`, and `createGlobalTheme()` — their closest equivalents to TypeStyles' APIs. ## TypeStyles results ### CSS generation time Time to register all styles, tokens, themes, keyframes, and globals. | Metric | Median | p95 | | ---------------------------------------------------- | ------------ | -------- | | Style registration (50 components + tokens + themes) | **~1.0 ms** | ~1.5 ms | | Selector calls (~90 component function invocations) | **~0.10 ms** | ~0.13 ms | Style registration happens once at module load. Selector calls (e.g. `button({ intent: 'primary', size: 'lg' })`) happen on every render — they're string concatenation, not CSS generation. ### Extracted CSS file size The same 50-component reference app extracted in each naming mode: | Mode | Raw | Gzip | Rules | | ------------ | ------- | ------ | ----- | | **Semantic** | 31.4 KB | 4.7 KB | 232 | | **Hashed** | 30.7 KB | 5.2 KB | 232 | | **Compact** | 29.0 KB | 4.9 KB | 232 | | **Atomic** | 19.0 KB | 4.7 KB | 296 | Atomic mode produces the smallest raw CSS because identical declarations are deduplicated across components. Gzip sizes converge because compression already exploits repetition. ### Runtime injection Full cycle: create all styles → serialize CSS → flush to sheet buffer. | Metric | Median | p95 | | --------------------------------- | ----------- | ------- | | Full cycle (registration + flush) | **~1.1 ms** | ~1.3 ms | ### SSR collection Per-request `collectStyles()` overhead, including `AsyncLocalStorage` isolation. | Metric | Median | p95 | | ----------------------------- | ----------- | ------- | | `collectStyles()` per request | **~1.1 ms** | ~1.4 ms | | Collected CSS size | 31.4 KB | — | ## Cross-framework comparison: Vanilla Extract The same 50-component design system implemented in Vanilla Extract's `recipe()` / `style()` / `createThemeContract()` APIs, measured under identical conditions (same machine, same Node version, same benchmark harness). ### Style registration Time to define all styles, tokens, and themes. For Vanilla Extract this is the build-time `.css.ts` execution cost; for TypeStyles this is module-load time. | Framework | Median | p95 | | ------------------- | ------- | ------- | | **TypeStyles** | ~1.0 ms | ~1.5 ms | | **Vanilla Extract** | ~0.7 ms | ~1.2 ms | VE is slightly faster at registration because it collects style objects without serializing CSS inline — serialization happens in their bundler plugin. ### Selector calls Time to invoke recipe/component functions and get class name strings back. Both frameworks do string concatenation at this stage. | Framework | Median | p95 | | ------------------- | -------- | -------- | | **TypeStyles** | ~0.10 ms | ~0.13 ms | | **Vanilla Extract** | ~0.03 ms | ~0.07 ms | VE's recipes are simpler string joins. TypeStyles does more work per call (variant resolution, compound variant matching), which trades a few microseconds for richer variant semantics. ### Extracted CSS file size | Framework | Raw | Gzip | Rules | | ------------------------- | ------- | ------ | ----- | | **TypeStyles (semantic)** | 31.4 KB | 4.7 KB | 232 | | **TypeStyles (atomic)** | 19.0 KB | 4.7 KB | 296 | | **Vanilla Extract** | 30.5 KB | 4.6 KB | 196 | CSS output sizes are nearly identical. TypeStyles' atomic mode produces 38% less raw CSS through cross-component declaration dedup, though gzip narrows the gap. ### What's not compared - **Runtime injection** — VE doesn't do runtime injection (CSS is always a build artifact). TypeStyles' runtime path is an additional capability, not a comparable dimension. - **SSR collection** — VE has no runtime CSS collection. Their CSS exists as static files at build time. - **Build pipeline overhead** — VE's bundler plugin (Vite, webpack, esbuild) adds build-time cost beyond `.css.ts` execution. We only measure the style definition cost, not the full build pipeline. ## Methodology Each timing benchmark: 1. Runs **5 warmup iterations** (discarded) 2. Runs **30–100 measured iterations** 3. Forces garbage collection between iterations (`--expose-gc`) 4. Reports **median, p95, min, max** File size measurements are deterministic — they run once per mode and measure exact byte counts (raw and gzip). The Vanilla Extract benchmark uses their `@vanilla-extract/css/adapter` and `@vanilla-extract/css/fileScope` APIs to run style definitions in Node without a bundler — the same mechanism VE's own test suite uses. CSS output is serialized from VE's structured format to measure comparable file sizes. ### Environment Numbers above were measured on Apple Silicon (M-series). CI runs on `ubuntu-latest` (GitHub Actions, Node 22) and may differ. File sizes are platform-independent. ## Reproduce locally From the monorepo root: ```bash # Build the core package first pnpm --filter typestyles build # Run benchmarks (prints results) pnpm --filter @typestyles/benchmarks bench # Run with regression check against baseline pnpm --filter @typestyles/benchmarks bench:ci # Update baseline after intentional changes pnpm --filter @typestyles/benchmarks bench:update ``` Results are written to `benchmarks/baseline.json`. The CI job compares each run against this baseline: - **File size metrics**: fail if raw or gzip size grows > 5% - **Timing metrics**: printed for visibility but not gated — timing varies too much across hardware (Apple Silicon vs CI VMs can differ 3x+) to use a checked-in baseline reliably VE results are tracked for comparison but never gate CI (we don't control VE's performance). ## Caveats - **Runtime injection is measured in Node**, not a real browser. The "full cycle" timing covers CSS serialization and sheet buffering, not actual CSSOM `insertRule` latency. - **Single-machine numbers.** These are not a cross-platform benchmark suite. Your production performance depends on your users' devices. - **VE comparison is honest but partial.** We measure what's directly comparable (style definition cost, CSS output size, selector call speed). We don't measure VE's full build pipeline, and we note where TypeStyles has capabilities VE doesn't (runtime injection, SSR collection) rather than treating those as advantages. - **CI VMs are noisy.** File sizes are the most reliable regression signal. ## Related - [Performance guide](/docs/performance) — best practices for keeping TypeStyles fast - [Class naming](/docs/class-naming) — how semantic, hashed, compact, and atomic modes work - [Zero-runtime extraction](/docs/zero-runtime) — eliminating runtime CSS injection entirely - [Framework comparison](/docs/framework-comparison) — feature-level comparison across ecosystems Source: https://typestyles.dev/docs/benchmarks --- # Best Practices Tips for organizing and maintaining typestyles in your project These guidelines will help you write maintainable, scalable code with typestyles. ## Naming conventions ### Namespaces Use kebab-case for namespaces. The namespace should describe the component or pattern: ```ts // Good - descriptive and consistent const button = styles.component('button', { ... }); const card = styles.component('card', { ... }); const navigationMenu = styles.component('navigation-menu', { ... }); // Avoid - generic or single-letter names const b = styles.component('b', { ... }); const x = styles.component('x', { ... }); const component = styles.component('component', { ... }); ``` ### Variants Use camelCase for variant dimension names. Be descriptive about the style purpose, not just the visual appearance: ```ts const button = styles.component('button', { base: { ... }, variants: { intent: { primary: { ... }, // The main action secondary: { ... }, // Alternative action danger: { ... }, // Destructive action ghost: { ... }, // Subtle action }, size: { small: { ... }, medium: { ... }, large: { ... }, }, }, defaultVariants: { intent: 'primary', size: 'medium', }, }); // Avoid - describes appearance only // blue: { ... }, big: { ... }, rounded: { ... } ``` For flat configs, variant names should still be descriptive: ```ts const card = styles.component('card', { base: { ... }, elevated: { ... }, interactive: { ... }, disabled: { ... }, }); ``` ### Tokens Group tokens by category. Use consistent naming within groups: ```ts // Good - clear categories and consistent naming export const color = tokens.create('color', { // Semantic colors primary: '#0066ff', secondary: '#6b7280', success: '#10b981', warning: '#f59e0b', danger: '#ef4444', // Neutral scale gray50: '#f9fafb', gray100: '#f3f4f6', gray200: '#e5e7eb', // ...etc // Text colors text: '#111827', textMuted: '#6b7280', textInverse: '#ffffff', // Surface colors surface: '#ffffff', surfaceRaised: '#f9fafb', surfaceSunken: '#f3f4f6', }); export const space = tokens.create('space', { // Scale-based 0: '0', 1: '4px', 2: '8px', 3: '12px', 4: '16px', 5: '20px', 6: '24px', 8: '32px', 10: '40px', 12: '48px', 16: '64px', 20: '80px', 24: '96px', // Semantic aliases (optional) xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', }); ``` ## File organization ### Monorepo/Package structure ``` src/ tokens/ index.ts # Re-exports all tokens colors.ts # Color definitions spacing.ts # Spacing scale typography.ts # Font sizes, line heights radii.ts # Border radius shadows.ts # Box shadows components/ Button/ index.ts # Component export Button.tsx # Component implementation Button.styles.ts # Style definitions Card/ index.ts Card.tsx Card.styles.ts ... layouts/ Layout.styles.ts ... utils/ styles.ts # Shared utility styles ``` ### Token files Keep tokens in dedicated files, grouped by category: ```ts // tokens/colors.ts import { tokens } from 'typestyles'; export const color = tokens.create('color', { // Brand colors brand50: '#eff6ff', brand100: '#dbeafe', brand500: '#3b82f6', brand600: '#2563eb', brand700: '#1d4ed8', // Neutral scale gray50: '#f9fafb', gray100: '#f3f4f6', // ... etc }); // tokens/index.ts export { color } from './colors'; export { space } from './spacing'; export { font } from './typography'; ``` ### Component style files Keep styles co-located with components or in a separate `.styles.ts` file: **Option 1: Same file (for simple components)** ```tsx // Button.tsx import { styles } from 'typestyles'; import { color, space } from '../tokens'; const button = styles.component('button', { base: { ... }, variants: { intent: { primary: { ... }, secondary: { ... } }, }, defaultVariants: { intent: 'primary' }, }); export function Button({ variant = 'primary', children }) { return ; } ``` **Option 2: Separate file (for complex components)** ```ts // Button.styles.ts import { styles } from 'typestyles'; import { color, space } from '../tokens'; export const button = styles.component('button', { base: { ... }, variants: { intent: { primary: { ... }, secondary: { ... } }, size: { small: { ... }, medium: { ... }, large: { ... } }, }, defaultVariants: { intent: 'primary', size: 'medium' }, }); // Button.tsx import { button } from './Button.styles'; export function Button({ variant = 'primary', size = 'medium', children }) { return ( ); } ``` ## Token architecture ### Semantic vs. literal tokens Use literal tokens for the design system foundation, semantic tokens for application use: ```ts // tokens/primitives.ts // Raw values from your design system export const primitives = { colors: { blue500: '#0066ff', blue600: '#0052cc', gray500: '#6b7280', red500: '#ef4444', }, space: { 1: '4px', 2: '8px', 3: '16px', 4: '24px', }, }; // tokens/semantic.ts // Semantic meanings mapped to primitives export const color = tokens.create('color', { primary: primitives.colors.blue500, primaryHover: primitives.colors.blue600, secondary: primitives.colors.gray500, danger: primitives.colors.red500, // ... etc }); export const space = tokens.create('space', primitives.space); ``` ### Component-specific tokens For complex components, you can scope tokens to just that component: ```ts // components/Calendar/Calendar.tokens.ts import { tokens } from 'typestyles'; export const calendar = tokens.create('calendar', { daySize: '40px', headerHeight: '48px', gridGap: '4px', }); // Usage in Calendar.styles.ts import { calendar } from './Calendar.tokens'; const calendarStyles = styles.component('calendar', { base: { width: calendar.daySize, height: calendar.daySize, }, }); ``` ### Token composition Reference other tokens when defining new ones: ```ts const space = tokens.create('space', { 1: '4px', 2: '8px', 3: '16px', 4: '24px', 5: '32px', }); const size = tokens.create('size', { // Reference space tokens for consistency sm: space[1], // 4px md: space[2], // 8px lg: space[3], // 16px icon: space[4], // 24px }); ``` ## Component patterns ### Compound components For related components (like Form + Input + Label), use consistent namespacing: ```ts const form = styles.component('form', { base: { ... }, }); const input = styles.component('input', { base: { ... }, variants: { state: { error: { borderColor: '#ef4444' }, success: { borderColor: '#10b981' }, }, }, }); const label = styles.component('label', { base: { ... }, variants: { required: { true: { '&::after': { content: '"*"', color: '#ef4444' } }, }, }, }); // Usage
``` ### Variant composition Use dimensioned variants for structured component APIs: ```ts const button = styles.component('button', { base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', border: 'none', fontWeight: 500, transition: 'all 150ms ease', }, variants: { intent: { primary: { backgroundColor: color.primary, color: '#fff' }, secondary: { backgroundColor: color.secondary, color: '#fff' }, ghost: { backgroundColor: 'transparent' }, }, size: { small: { padding: '4px 8px', fontSize: '12px' }, medium: { padding: '8px 16px', fontSize: '14px' }, large: { padding: '12px 24px', fontSize: '16px' }, }, }, defaultVariants: { intent: 'primary', size: 'medium', }, }); // Usage button(); // Primary medium button button({ intent: 'ghost', size: 'small' }); // Small ghost button ``` ### Layout components Use layout components with flexible spacing: ```ts const stack = styles.component('stack', { base: { display: 'flex', flexDirection: 'column', }, variants: { gap: { 1: { gap: space[1] }, 2: { gap: space[2] }, 3: { gap: space[3] }, 4: { gap: space[4] }, }, }, defaultVariants: { gap: 2 }, }); const row = styles.component('row', { base: { display: 'flex', flexDirection: 'row', alignItems: 'center', }, variants: { gap: { 1: { gap: space[1] }, 2: { gap: space[2] }, }, }, }); ``` ## State management ### Boolean variants For simple on/off states, use boolean variants or flat configs: ```ts const card = styles.component('card', { base: { ... }, elevated: { boxShadow: '0 4px 6px rgba(0,0,0,0.1)', }, interactive: { cursor: 'pointer', '&:hover': { boxShadow: '0 8px 12px rgba(0,0,0,0.15)', }, }, }); // Destructure and use cx() for conditional application: import { cx } from 'typestyles'; const { base, elevated, interactive } = card; cx(base, isElevated && elevated, isInteractive && interactive); ``` ### Complex state For more complex state combinations, use dimensioned variants: ```ts const button = styles.component('button', { base: { ... }, variants: { intent: { primary: { ... }, secondary: { ... }, ghost: { ... } }, size: { small: { ... }, medium: { ... }, large: { ... } }, disabled: { true: { opacity: 0.5, cursor: 'not-allowed' } }, loading: { true: { cursor: 'wait' } }, }, defaultVariants: { intent: 'primary', size: 'medium' }, }); function getButtonClasses({ variant, size, isDisabled, isLoading }: ButtonProps) { return button({ intent: variant, size, disabled: isDisabled || undefined, loading: isLoading || undefined, }); } ``` ## Theming patterns ### Dark mode with createTheme ```ts // tokens/index.ts export const color = tokens.create('color', { text: '#111827', textMuted: '#6b7280', surface: '#ffffff', surfaceRaised: '#f9fafb', }); export const darkTheme = tokens.createTheme('dark', { base: { color: { text: '#e0e0e0', textMuted: '#9ca3af', surface: '#1a1a2e', surfaceRaised: '#25253e', }, }, }); // Apply theme to document body or specific container document.body.classList.add(darkTheme.className); ``` ### Multiple themes ```ts const brandLight = tokens.createTheme('brand-light', { base: { /* … */ }, }); const brandDark = tokens.createTheme('brand-dark', { base: { /* … */ }, }); const highContrast = tokens.createTheme('high-contrast', { base: { /* … */ }, }); ``` ## Code style ### Consistent formatting ```ts // Good - consistent structure const card = styles.component('card', { base: { padding: space[4], borderRadius: '8px', backgroundColor: color.surface, }, elevated: { boxShadow: shadow.md, }, interactive: { cursor: 'pointer', transition: 'box-shadow 200ms ease', '&:hover': { boxShadow: shadow.lg, }, }, }); // Avoid - inconsistent formatting const card = styles.component('card', { base: { padding: space[4], borderRadius: '8px', backgroundColor: color.surface }, elevated: { boxShadow: shadow.md }, }); ``` ### Import organization ```ts // 1. External imports import React from 'react'; import { styles, tokens, cx } from 'typestyles'; // 2. Internal absolute imports import { color, space } from '@/tokens'; import { Button } from '@/components'; // 3. Internal relative imports import { card } from './Card.styles'; import { useCardState } from './useCardState'; ``` ## Performance tips 1. **Define styles at module level** - Never create styles inside components 2. **Reuse token references** - Define once, import many times 3. **Minimize variants** - Don't create a variant for every possible state 4. **Use [`createVar()` for dynamic values**](/docs/dynamic-styles) — Keep styles static; set per-instance values via CSS custom properties 5. **Lazy load component styles** - Use dynamic imports for code splitting ## Scope isolation ### Why scopeId matters In semantic mode (the default), `styles.component('button', …)` produces class names like `button-base`. If two packages — or two files in the same app — both register a `'button'` namespace without a `scopeId`, their CSS rules silently overwrite each other. TypeStyles warns about this in development, but the fix is simple: always set a `scopeId`. ### For applications Use `createTypeStyles` with a `scopeId` at the root of your styles: ```ts // src/styles.ts import { createTypeStyles } from 'typestyles'; export const { styles, tokens } = createTypeStyles({ scopeId: 'my-app', }); ``` Class names become `my-app-button-base` — unique across your dependency tree. ### For published packages Published packages **must** use a `scopeId` to avoid colliding with consumer code or other packages. Use the package name and `hashed` mode: ```ts import { createTypeStyles } from 'typestyles'; export const { styles, tokens } = createTypeStyles({ scopeId: '@acme/ui', mode: 'hashed', }); ``` See the full [Publishing Packages](/docs/publishing-packages) guide for ESLint enforcement, token scoping, and a complete library setup example. ### Per-file isolation (CSS Modules style) For maximum isolation without choosing a global scope, use `fileScopeId`: ```ts import { createStyles, fileScopeId } from 'typestyles'; const styles = createStyles({ scopeId: fileScopeId(import.meta) }); ``` Each file gets a unique hash-based scope, similar to CSS Modules. ## Common mistakes to avoid ### Creating styles inside components ```tsx // Bad - creates new styles on every render function Button({ children }) { const button = styles.component('button', { base: { ... } }); // Don't do this return ; } // Good - define at module level const button = styles.component('button', { base: { ... } }); function Button({ children }) { return ; } ``` ### Dynamic style values ```tsx import { styles, createVar, assignVars } from 'typestyles'; // Bad - creates styles for every possible value const button = styles.component('button', { base: { width: props.width }, // Don't do this }); // Good - wire dynamic values through CSS custom properties const widthVar = createVar('buttonWidth'); const button = styles.component('button', { base: { display: 'inline-block', width: widthVar }, }); function Button({ width, children }) { return ( ); } ``` ### Duplicate namespaces ```ts // File A const button = styles.component('button', { ... }); // File B const button = styles.component('button', { ... }); // Collision! // Fix 1 - use descriptive names const iconButton = styles.component('icon-button', { ... }); const textButton = styles.component('text-button', { ... }); // Fix 2 (recommended) - use scopeId to isolate const { styles } = createTypeStyles({ scopeId: 'my-app' }); const button = styles.component('button', { ... }); // "my-app-button-base" ``` ## Summary - Always set a `scopeId` via `createTypeStyles` or `createStyles` — especially in published packages - Use descriptive, consistent naming for namespaces and variants - Organize tokens by category in dedicated files - Define styles at module level, never in components - Use semantic tokens for application code - Keep variants focused on style purpose, not appearance - Use [`createVar()` + `assignVars()`](/docs/dynamic-styles) for per-instance dynamic values - Use `cx()` from typestyles for conditional class joining - Use a bundler plugin for [zero-runtime production builds](/docs/zero-runtime) Source: https://typestyles.dev/docs/best-practices --- # Cascade layers (@layer) Opt-in CSS cascade layers for predictable specificity against global CSS By default, TypeStyles emits **flat** rules (no `@layer`), matching legacy behavior and keeping the API surface small. When you opt in with a **`layers`** tuple on `createStyles`, `createTokens`, or the unified **`createTypeStyles`** factory, TypeStyles: 1. Registers a single **`@layer a, b, c;`** preamble (once per distinct stack) so order is deterministic. 2. Wraps each emitted rule block in **`@layer { … }`** for the `layer` you pass on each style call. 3. When **`createTokens({ layers, tokenLayer })`** is used, `:root` custom properties and **theme** rules go into `tokenLayer` so utilities and components can override tokens predictably. ## `createTypeStyles` (recommended for design systems) One config object gives you matching **`scopeId`**, **`layers`**, and **`tokenLayer`** for both class CSS and token/theme CSS: ```ts import { createTypeStyles } from 'typestyles'; const { styles, tokens } = createTypeStyles({ scopeId: 'ds', mode: 'semantic', layers: ['reset', 'tokens', 'components', 'utilities'] as const, tokenLayer: 'tokens', }); const reset = styles.class('reset', { margin: 0, padding: 0 }, { layer: 'reset' }); const button = styles.component( 'button', { base: { padding: '8px 16px' }, variants: { intent: { primary: { backgroundColor: '#0066ff' } }, }, defaultVariants: { intent: 'primary' }, }, { layer: 'components' }, ); tokens.create('color', { primary: '#0066ff' }); ``` Conceptual output: ```css @layer reset, tokens, components, utilities; @layer tokens { :root { /* --ds-color-* */ } } @layer reset { .reset { margin: 0; padding: 0; } } @layer components { .button-base { padding: 8px 16px; } } ``` ## `createStyles` only Pass **`layers`** as a `const` tuple (or as `{ order, prependFrameworkLayers? }`). Every **`styles.class`**, **`styles.hashClass`**, and **`styles.component`** call must include **`{ layer: '…' }`**. Layer names must appear in the tuple (not in `prependFrameworkLayers`, which only affects ordering against external frameworks). ```ts const styles = createStyles({ layers: { order: ['components'], prependFrameworkLayers: ['bootstrap'] }, }); styles.class('card', { padding: '1rem' }, { layer: 'components' }); ``` ## `createTokens` with layers When **`layers`** is set, **`tokenLayer`** is **required**. Token and theme CSS is wrapped in that layer. ```ts const tokens = createTokens({ scopeId: 'app', layers: ['tokens', 'components'] as const, tokenLayer: 'tokens', }); ``` ## Default (no layers) Omit **`layers`** everywhere: no `@layer` in output, and there is no `layer` option on style APIs. ## Notes - **Multiple instances:** Each factory that passes **`layers`** owns its own stack; preamble keys include **`scopeId`** and full order so different design systems on one page stay distinct. - **Build / SSR:** Preamble rules are inserted at the **front** of the virtual sheet so `getRegisteredCss()` and extraction see ordering before wrapped blocks. - **jsdom:** Older parsers may log warnings for `@layer`; real browsers support cascade layers. See also [Class naming](/docs/class-naming). Source: https://typestyles.dev/docs/cascade-layers --- # Class naming Per-instance semantic, hashed, or atomic class names via createStyles; scoped tokens via createTokens By default, typestyles emits **readable semantic** class names: `button-base`, `card-elevated`, `button-intent-primary`. You can switch to **hashed**, **compact** (hash-only whole-object), or **atomic** (one class per declaration) names for smaller strings, deduped CSS, or closer parity with CSS-in-JS tools that minify class names. Naming applies to: - [`styles.class`](/docs/styles) - [`styles.component`](/docs/components) (single-part components and [multipart `slots`](/docs/components)) It does **not** change [`@typestyles/props`](/docs/atomic-css) utility naming; that package uses its own `createProps` namespace pattern. ## Quick start **Class names** are configured per **`createStyles()`** instance (not with a global singleton). Create one instance per package, design system, or micro-frontend and import that everywhere in the package: ```ts import { createStyles } from 'typestyles'; export const styles = createStyles({ mode: 'hashed', prefix: 'ds', scopeId: '@acme/design-system', }); ``` Use `styles.component`, `styles.class`, and `styles.hashClass` from that object. For [utility shortcuts](/docs/styles#utility-shortcuts), pass **`utils`** into `createStyles` (or call `styles.withUtils(…)` on the default export). The default `import { styles } from 'typestyles'` is simply `createStyles()` with default options—fine for apps that own the whole page. **Tokens and themes** use the same idea: **`createTokens({ scopeId })`** so custom properties and theme classes do not collide when multiple bundles share one document: ```ts import { createTokens } from 'typestyles'; export const tokens = createTokens({ scopeId: '@acme/design-system' }); ``` With `scopeId` set, `tokens.create('color', …)` emits variables like `--acme-design-system-color-primary` (sanitized), and `tokens.createTheme('dark', …)` registers a theme class whose segment includes the scope. For **CSS cascade layers** (`@layer`) — optional, and off by default — see [Cascade layers](/docs/cascade-layers). Use **`createTypeStyles`** when both class rules and token/theme CSS should share one layer stack and one `scopeId`. ## API ### `createStyles(options?)` Returns a style API with the same methods as the default `styles` export. Options are a partial **`ClassNamingConfig`** merged onto defaults: | Option | Type | Default | Description | | --------- | ----------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | `'semantic' \| 'hashed' \| 'compact' \| 'atomic'` | `'semantic'` | How class strings are built (see below). | | `prefix` | `string` | `'ts'` | Leading segment for hashed/compact/atomic output and for `hashClass`. | | `scopeId` | `string` | `''` | Optional id (package name, app name) so two packages can reuse the same logical namespace without sharing the same class string. In `semantic` mode the sanitized scope is prefixed onto class names; in `hashed`/`compact`/`atomic` mode it is mixed into the hash input. | | `layers` | `readonly string[]` or `{ order, prependFrameworkLayers? }` | _(omitted)_ | When set, enables `@layer` output and requires `{ layer }` on each `class` / `hashClass` / `component` call. See [Cascade layers](/docs/cascade-layers). | The instance also exposes **`styles.classNaming`**: a read-only snapshot of the resolved config (useful for debugging). ### `mergeClassNaming(partial?)` and `defaultClassNamingConfig` Use these when you need the resolved config object without creating a full API (for example tests or tooling). ### `scopedTokenNamespace(scopeId, logicalNamespace)` Returns the CSS custom property namespace segment used for `tokens.create` when `scopeId` is set (sanitized). Advanced / library use. ## Modes ### `semantic` (default) Human-readable, stable names derived from the namespace and variant segment: - `styles.class('card', { … })` → `card` - `styles.component('button', { … })` → `button-base`, `button-intent-primary`, etc. - Components with `slots` → `{namespace}-{slot}`, `{namespace}-{slot}-{dimension}-{option}`, etc. With **`scopeId`** set, the sanitized scope is prefixed onto every class name — the same way `tokens.create` scopes custom property names: - `createStyles({ scopeId: 'my-ui' })` + `styles.component('button', { … })` → `my-ui-button-base` - `createStyles({ scopeId: '@acme/ds' })` + `styles.class('card', { … })` → `acme-ds-card` This keeps semantic names readable while making isolation real: two packages can both register `styles.component('button', …)` without their CSS rules overwriting each other. In development, typestyles also logs an error if two different definitions ever emit the same class string (cross-scope collisions, or hash collisions in `hashed`/`compact` mode). ### `hashed` Deterministic names of the form **`{prefix}-{namespace-slug}-{hash}`**. The hash is computed from (when set) `scopeId`, the namespace, a variant segment (e.g. `base`, `intent-primary`, `root-compound-0`), and the serialized style object for that rule. Identical definitions produce identical class strings. Use this when you want shorter, scoped names while still recognizing the namespace in DevTools. ### `compact` **`{prefix}-{hash}`** only—no namespace slug in the string. Same hash inputs as `hashed`, so behavior is equally deterministic. Each component rule is still **one class per style chunk** (base, variant option, compound rule, etc.). Use this when you want the shortest hash-only class strings without per-declaration splitting. ### `atomic` **One class per CSS declaration.** Identical property values share a class across the codebase—CSS size plateaus as you add components instead of growing linearly with every rule chunk. - `styles.class('card', { padding: '1rem', color: 'red' })` → two classes joined with a space - `styles.component('button', { base: { color: 'red', padding: '8px' } })` → `button.base` is a space-separated list of atomic classes - Nested selectors (`&:hover`, attribute selectors) and `@media` blocks decompose the same way; each inner declaration gets its own class Hash inputs include (when set) `scopeId`, the declaration path (property + nested context), and the value. For Tailwind-style **utility prop APIs**, see [`@typestyles/props`](/docs/atomic-css). #### Migrating from the old `atomic` name Before P2.10, `atomic` meant hash-only **whole-object** classes (no namespace slug). That mode is now **`compact`**. If you were using `mode: 'atomic'` for short hash-only class strings, switch to **`mode: 'compact'`**. Use **`mode: 'atomic'`** when you want true per-declaration output and dedup. ## `styles.hashClass` `hashClass` on a given instance uses that instance’s **`prefix`** and **`scopeId`**. If **`scopeId`** is empty, the hash input matches the historical behavior (properties only, plus label handling) for the same style shape. ## Monorepos and `scopeId` Two packages might both use `styles.component('button', …)`. Give each package its own **`createStyles({ scopeId: '…' })`**: in **`semantic`** mode the scope is prefixed onto the class name (`pkg-a-button-base` vs `pkg-b-button-base`); in **`hashed`**, **`compact`**, or **`atomic`** mode the scope is mixed into the hash so identical style objects in different packages do not map to the same class string. For tokens, use **`createTokens({ scopeId })`** per package so `--color-*` and `.theme-*` rules do not overwrite each other on `:root` or clash by name. ## SSR Use the **same** `createStyles` / `createTokens` options (including `scopeId`) on the server and the client so class names, custom property names, and injected CSS match. ## Testing Use a **dedicated** `createStyles({ … })` per test file or suite when you need hashed, compact, or atomic mode. There is no global naming state to reset—only call **`reset()`** (and related sheet helpers) to clear injected CSS between tests. Default **`import { styles } from 'typestyles'`** is still shared across tests, so prefer a local `createStyles()` when asserting on class strings under non-semantic modes. ```ts import { createStyles, reset } from 'typestyles'; const styles = createStyles({ mode: 'hashed', prefix: 't', scopeId: 'test-a' }); beforeEach(() => { reset(); }); ``` If you assert on class strings under **`hashed`**, **`compact`**, or **`atomic`**, prefer stable snapshots or assert on substrings (prefix, absence of semantic segments) rather than hard-coding full hashes unless you fix `scopeId` and styles. See also [Testing](/docs/testing). ## Related - [Styles](/docs/styles) — `styles.class`, `compose`, `withUtils` - [Components](/docs/components) — `styles.component` and `slots` - [Tokens](/docs/tokens) — `createTokens` and scoped custom properties - [Atomic CSS Utilities](/docs/atomic-css) — `@typestyles/props` (separate naming scheme) - [API Reference](/docs/api-reference) — export list Source: https://typestyles.dev/docs/class-naming --- # Color Type-safe helpers for CSS color functions The `color` API provides type-safe helpers for modern CSS color functions. These functions return plain CSS strings (no runtime color math), so they compose naturally with token references and other CSS values. ## Basic color functions ### rgb Create `rgb()` colors with space-separated syntax: ```ts import { color } from 'typestyles/color'; color.rgb(0, 102, 255); // "rgb(0 102 255)" color.rgb(0, 102, 255, 0.5); // "rgb(0 102 255 / 0.5)" ``` ### hsl Create `hsl()` colors: ```ts color.hsl(220, '100%', '50%'); // "hsl(220 100% 50%)" color.hsl(220, '100%', '50%', 0.8); // "hsl(220 100% 50% / 0.8)" ``` ### oklch Create `oklch()` colors for perceptually uniform color spaces: ```ts color.oklch(0.7, 0.15, 250); // "oklch(0.7 0.15 250)" color.oklch(0.7, 0.15, 250, 0.5); // "oklch(0.7 0.15 250 / 0.5)" ``` ### oklab Create `oklab()` colors: ```ts color.oklab(0.7, -0.1, -0.1); // "oklab(0.7 -0.1 -0.1)" color.oklab(0.7, -0.1, -0.1, 0.5); // "oklab(0.7 -0.1 -0.1 / 0.5)" ``` ### lab Create `lab()` colors: ```ts color.lab('50%', 40, -20); // "lab(50% 40 -20)" ``` ### lch Create `lch()` colors: ```ts color.lch('50%', 80, 250); // "lch(50% 80 250)" ``` ### hwb Create `hwb()` colors: ```ts color.hwb(220, '10%', '0%'); // "hwb(220 10% 0%)" ``` ## Advanced color functions ### mix Mix two colors using `color-mix()`: ```ts // Mix red and blue equally (50/50) color.mix('red', 'blue'); // "color-mix(in srgb, red, blue)" // Mix 30% red with 70% blue color.mix('red', 'blue', 30); // "color-mix(in srgb, red 30%, blue)" // Mix in a different color space color.mix('red', 'blue', 50, 'oklch'); // "color-mix(in oklch, red 50%, blue)" ``` Works great with token references: ```ts const theme = tokens.create('theme', { primary: '#0066ff', }); // Create a lighter variant of your primary color color.mix(theme.primary, 'white', 20); // "color-mix(in srgb, var(--theme-primary) 20%, white)" ``` ### alpha Adjust the opacity of any color: ```ts color.alpha('red', 0.5); // "color-mix(in srgb, red 50%, transparent)" color.alpha(theme.primary, 0.2); // "color-mix(in srgb, var(--theme-primary) 20%, transparent)" color.alpha('#0066ff', 0.8, 'oklch'); // "color-mix(in oklch, #0066ff 80%, transparent)" ``` This is a convenience wrapper around `color.mix()` that mixes any color with transparent. ### lightDark Use the `light-dark()` CSS function for automatic light/dark mode switching: ```ts color.lightDark('#111', '#eee'); // "light-dark(#111, #eee)" // Works with tokens too color.lightDark(theme.textLight, theme.textDark); // "light-dark(var(--theme-textLight), var(--theme-textDark))" ``` Note: This requires the browser to support `light-dark()` and the element to have an appropriate `color-scheme` value. ## Using with tokens All color functions accept token references since tokens are just CSS `var()` strings: ```ts import { styles, tokens } from 'typestyles'; import { color as colorFn } from 'typestyles/color'; const themeColor = tokens.create('color', { primary: '#0066ff', secondary: '#6b7280', }); const card = styles.component('card', { base: { backgroundColor: colorFn.mix(themeColor.primary, 'white', 10), borderColor: colorFn.alpha(themeColor.secondary, 0.3), }, }); ``` ## Color spaces for mixing When using `mix()` or `alpha()`, you can specify different color spaces: - `'srgb'` (default) - Standard RGB, most common - `'srgb-linear'` - Linear RGB - `'display-p3'` - Wide gamut RGB - `'a98-rgb'` - Wide gamut RGB - `'prophoto-rgb'` - Very wide gamut - `'rec2020'` - Ultra HD color space - `'lab'` - CIE Lab color space - `'oklab'` - Better Lab, perceptually uniform - `'xyz'`, `'xyz-d50'`, `'xyz-d65'` - CIE XYZ - `'hsl'` - HSL color space - `'hwb'` - HWB color space - `'lch'` - CIE LCH - `'oklch'` - Better LCH, perceptually uniform ```ts // Mix in perceptually uniform space for smoother gradients color.mix('red', 'blue', 50, 'oklch'); color.alpha(theme.primary, 0.5, 'oklab'); ``` ## Why no runtime color math? Unlike some libraries that parse and manipulate colors in JavaScript, these helpers simply generate CSS strings. This means: - Zero runtime overhead - Colors are computed by the browser (which can use hardware acceleration) - Works naturally with CSS custom properties and tokens - Respects user's color scheme and accessibility preferences - Smaller bundle size If you need programmatic color manipulation (e.g., generating a palette programmatically), do that at build time and use the resulting values with these helpers. Source: https://typestyles.dev/docs/color --- # Component Library Setup Building a reusable component library with typestyles This guide shows you how to set up a reusable component library using typestyles, suitable for publishing to npm. **Reference implementation:** [examples/react-design-system](https://github.com/type-styles/typestyles/blob/main/examples/react-design-system/README.md) (React Aria wrappers + shared tokens). Consumed by [examples/vite-app](https://github.com/type-styles/typestyles/blob/main/examples/vite-app/README.md) and [examples/next-app](https://github.com/type-styles/typestyles/blob/main/examples/next-app/README.md). ## Project structure ``` my-ui-library/ ├── src/ │ ├── tokens/ │ │ ├── index.ts # Token exports │ │ ├── colors.ts # Color definitions │ │ ├── spacing.ts # Spacing scale │ │ └── typography.ts # Font tokens │ ├── components/ │ │ ├── Button/ │ │ │ ├── Button.tsx │ │ │ ├── Button.styles.ts │ │ │ └── index.ts │ │ ├── Card/ │ │ │ ├── Card.tsx │ │ │ ├── Card.styles.ts │ │ │ └── index.ts │ │ ├── Input/ │ │ │ └── ... │ │ └── index.ts # Component exports │ ├── utils/ │ │ └── style-utils.ts # Shared style utilities │ ├── index.ts # Main library export │ └── styles.css # Optional global CSS ├── package.json ├── tsconfig.json ├── vite.config.ts └── README.md ``` ## Package configuration ### package.json ```json { "name": "@myorg/ui-library", "version": "1.0.0", "description": "A React component library built with typestyles", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" }, "./tokens": { "import": "./dist/tokens/index.js", "types": "./dist/tokens/index.d.ts" }, "./styles.css": "./dist/styles.css" }, "files": ["dist"], "scripts": { "build": "tsc && vite build", "dev": "vite", "test": "vitest", "typecheck": "tsc --noEmit" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0", "typestyles": "^1.0.0" }, "devDependencies": { "@types/react": "^18.0.0", "typestyles": "^1.0.0", "typescript": "^5.0.0", "vite": "^5.0.0" } } ``` Note: `typestyles` should be a peer dependency so consuming apps can control the version. ## Scope isolation Published packages **must** use a `scopeId` to prevent class name collisions with consumer code or other libraries. Without one, `styles.component('button', …)` produces `button-base` in both your library and the consuming app — their CSS rules silently overwrite each other. ```ts // src/styles.ts import { createTypeStyles } from 'typestyles'; export const { styles, tokens } = createTypeStyles({ scopeId: '@myorg/ui-library', }); ``` Now all class names are prefixed (`myorg-ui-library-button-base`) and all token custom properties are namespaced (`--myorg-ui-library-color-primary`). See [Best Practices — Scope isolation](/docs/best-practices#scope-isolation) for more options. ## Token system ### Colors ```ts // src/tokens/colors.ts import { tokens } from './styles'; // use the scoped instance export const colors = tokens.create('color', { // Brand brand50: '#eff6ff', brand100: '#dbeafe', brand200: '#bfdbfe', brand300: '#93c5fd', brand400: '#60a5fa', brand500: '#3b82f6', brand600: '#2563eb', brand700: '#1d4ed8', brand800: '#1e40af', brand900: '#1e3a8a', // Gray scale gray50: '#f9fafb', gray100: '#f3f4f6', gray200: '#e5e7eb', gray300: '#d1d5db', gray400: '#9ca3af', gray500: '#6b7280', gray600: '#4b5563', gray700: '#374151', gray800: '#1f2937', gray900: '#111827', // Semantic success: '#10b981', warning: '#f59e0b', danger: '#ef4444', info: '#3b82f6', }); // Semantic aliases export const semanticColors = tokens.create('semantic-color', { primary: colors.brand500, primaryHover: colors.brand600, secondary: colors.gray500, secondaryHover: colors.gray600, text: colors.gray900, textMuted: colors.gray500, background: colors.gray50, surface: '#ffffff', surfaceRaised: colors.gray100, border: colors.gray200, }); ``` ### Spacing ```ts // src/tokens/spacing.ts import { tokens } from 'typestyles'; export const spacing = tokens.create('space', { 0: '0', 1: '4px', 2: '8px', 3: '12px', 4: '16px', 5: '20px', 6: '24px', 8: '32px', 10: '40px', 12: '48px', 16: '64px', 20: '80px', 24: '96px', // Semantic aliases xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', '2xl': '48px', '3xl': '64px', }); ``` ### Typography ```ts // src/tokens/typography.ts import { tokens } from 'typestyles'; export const fontSize = tokens.create('font-size', { xs: '0.75rem', // 12px sm: '0.875rem', // 14px base: '1rem', // 16px lg: '1.125rem', // 18px xl: '1.25rem', // 20px '2xl': '1.5rem', // 24px '3xl': '1.875rem', // 30px '4xl': '2.25rem', // 36px }); export const fontWeight = tokens.create('font-weight', { normal: '400', medium: '500', semibold: '600', bold: '700', }); export const lineHeight = tokens.create('line-height', { none: '1', tight: '1.25', snug: '1.375', normal: '1.5', relaxed: '1.625', loose: '2', }); ``` ### Token exports ```ts // src/tokens/index.ts export { colors, semanticColors } from './colors'; export { spacing } from './spacing'; export { fontSize, fontWeight, lineHeight } from './typography'; ``` ## Component implementation ### Button component ```ts // src/components/Button/Button.styles.ts import { styles } from 'typestyles'; import { semanticColors, spacing, fontSize, fontWeight } from '../../tokens'; export const button = styles.component('button', { base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: spacing[2], borderRadius: '6px', lineHeight: '1.5', cursor: 'pointer', border: 'none', transition: 'all 150ms ease', '&:disabled': { opacity: 0.5, cursor: 'not-allowed', }, '&:focus': { outline: 'none', boxShadow: `0 0 0 3px ${semanticColors.primary}33`, }, }, variants: { intent: { primary: { backgroundColor: semanticColors.primary, color: '#ffffff', '&:hover:not(:disabled)': { backgroundColor: semanticColors.primaryHover, }, }, secondary: { backgroundColor: semanticColors.secondary, color: '#ffffff', '&:hover:not(:disabled)': { backgroundColor: semanticColors.secondaryHover, }, }, outline: { backgroundColor: 'transparent', border: `1px solid ${semanticColors.border}`, color: semanticColors.text, '&:hover:not(:disabled)': { backgroundColor: semanticColors.surfaceRaised, }, }, ghost: { backgroundColor: 'transparent', color: semanticColors.text, '&:hover:not(:disabled)': { backgroundColor: semanticColors.surfaceRaised, }, }, }, size: { sm: { padding: `${spacing[1]} ${spacing[3]}`, fontSize: fontSize.sm, fontWeight: fontWeight.medium, }, md: { padding: `${spacing[2]} ${spacing[4]}`, fontSize: fontSize.base, fontWeight: fontWeight.medium, }, lg: { padding: `${spacing[3]} ${spacing[6]}`, fontSize: fontSize.lg, fontWeight: fontWeight.medium, }, }, width: { auto: {}, full: { width: '100%' }, }, }, defaultVariants: { intent: 'primary', size: 'md', width: 'auto', }, }); ``` ```tsx // src/components/Button/Button.tsx import { cx } from 'typestyles'; import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'; import { button } from './Button.styles'; export interface ButtonProps extends ButtonHTMLAttributes { variant?: 'primary' | 'secondary' | 'outline' | 'ghost'; size?: 'sm' | 'md' | 'lg'; fullWidth?: boolean; children: ReactNode; } export const Button = forwardRef( ({ variant = 'primary', size = 'md', fullWidth = false, children, className, ...props }, ref) => { return ( ); }, ); Button.displayName = 'Button'; ``` ```ts // src/components/Button/index.ts export { Button } from './Button'; export type { ButtonProps } from './Button'; ``` ## Theming support ### Dark theme ```ts // src/tokens/themes.ts import { tokens } from 'typestyles'; export const darkTheme = tokens.createTheme('dark', { base: { 'semantic-color': { primary: '#60a5fa', primaryHover: '#3b82f6', text: '#f9fafb', textMuted: '#9ca3af', background: '#111827', surface: '#1f2937', surfaceRaised: '#374151', border: '#4b5563', }, }, }); export const highContrastTheme = tokens.createTheme('high-contrast', { base: { 'semantic-color': { text: '#000000', background: '#ffffff', primary: '#0000ff', border: '#000000', }, }, }); ``` ### Theme provider ```tsx // src/components/ThemeProvider/ThemeProvider.tsx import { createContext, useContext, useState, type ReactNode } from 'react'; import { darkTheme } from '../../tokens/themes'; interface ThemeContextValue { theme: 'light' | 'dark'; setTheme: (theme: 'light' | 'dark') => void; } const ThemeContext = createContext({ theme: 'light', setTheme: () => {}, }); export function useTheme() { return useContext(ThemeContext); } interface ThemeProviderProps { children: ReactNode; defaultTheme?: 'light' | 'dark'; } export function ThemeProvider({ children, defaultTheme = 'light' }: ThemeProviderProps) { const [theme, setTheme] = useState(defaultTheme); return (
{children}
); } ``` ## Build configuration ### Vite config ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import dts from 'vite-plugin-dts'; import path from 'path'; export default defineConfig({ plugins: [ react(), dts({ insertTypesEntry: true, }), ], build: { lib: { entry: path.resolve(__dirname, 'src/index.ts'), name: 'MyUILibrary', formats: ['es', 'cjs'], fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`, }, rollupOptions: { external: ['react', 'react-dom', 'typestyles'], output: { globals: { react: 'React', 'react-dom': 'ReactDOM', typestyles: 'TypeStyles', }, }, }, }, }); ``` ### TypeScript config ```json // tsconfig.json { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "declaration": true, "declarationMap": true, "outDir": "dist" }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ``` ## Main exports ```ts // src/index.ts // Components export { Button } from './components/Button'; export type { ButtonProps } from './components/Button'; export { Card } from './components/Card'; export type { CardProps } from './components/Card'; export { Input } from './components/Input'; export type { InputProps } from './components/Input'; // Theme export { ThemeProvider, useTheme } from './components/ThemeProvider'; export { darkTheme, highContrastTheme } from './tokens/themes'; // Tokens (for custom styling) export { colors, semanticColors, spacing, fontSize, fontWeight, lineHeight } from './tokens'; ``` ## Usage examples ### Installation ```bash npm install @myorg/ui-library typestyles react react-dom ``` ### Basic usage ```tsx import { Button, Card, Input } from '@myorg/ui-library'; function App() { return (

Login

); } ``` ### With theming ```tsx import { ThemeProvider, Button } from '@myorg/ui-library'; function App() { return ( ); } ``` ### Using tokens for custom styling ```tsx import { Button } from '@myorg/ui-library'; import { semanticColors, spacing } from '@myorg/ui-library/tokens'; import { styles } from 'typestyles'; const customCard = styles.component('custom-card', { base: { padding: spacing[6], border: `2px solid ${semanticColors.primary}`, borderRadius: '12px', }, }); function CustomComponent() { return (
); } ``` ## Publishing ### Build the library ```bash npm run build ``` ### Publish to npm ```bash npm login npm publish --access public ``` ## Versioning Follow semantic versioning: - **Patch (1.0.1)**: Bug fixes, token value changes - **Minor (1.1.0)**: New components, new tokens (backward compatible) - **Major (2.0.0)**: Breaking changes, removed components, renamed tokens ## Best practices 1. **Export prop types** - Let consumers extend your components 2. **Document breaking changes** - Keep a detailed changelog 3. **Test thoroughly** - Components should work in any React app 4. **Minimize dependencies** - Keep the library lightweight 5. **Provide examples** - Show how to customize and extend 6. **Support tree-shaking** - Use ES modules and avoid side effects 7. **Version tokens separately** - Consider a separate tokens package 8. **Test with multiple React versions** - Ensure compatibility ## Troubleshooting ### Styles not working in consumer app Make sure `typestyles` is installed in the consumer app: ```bash npm install typestyles ``` ### TypeScript errors in consumer Ensure the consumer has compatible TypeScript settings: ```json { "compilerOptions": { "moduleResolution": "bundler", "esModuleInterop": true } } ``` ### Duplicate typestyles instances If you see duplicate style tags, ensure `typestyles` is a peer dependency, not a regular dependency. Source: https://typestyles.dev/docs/component-library --- # Components Build typed variant APIs with styles.component `styles.component()` is the first-class API for variant-driven component styling. `styles.component()` is the unified API for all component styling. For flat configs (no dimensioned `variants`), see [Styles](/docs/styles). Use the dimensioned config when you want a typed interface with: - `base` styles - `variants` dimensions - `compoundVariants` for combinations - `defaultVariants` ## Basic component The live example defines a dimensioned `button` with `intent` and `size` variants, then shows how to call it. Class strings follow the global [class naming](/docs/class-naming) configuration (`semantic` by default). > Interactive demo: https://typestyles.dev/docs/components ```ts import { createStyles } from 'typestyles'; const styles = createStyles(); const button = styles.component('variant-button', { base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', border: '1px solid transparent', borderRadius: '8px', fontWeight: 500, }, variants: { intent: { primary: { backgroundColor: '#2563eb', color: 'white' }, ghost: { backgroundColor: 'transparent', color: '#1f2937' }, }, size: { sm: { padding: '6px 10px', fontSize: '14px' }, lg: { padding: '10px 16px', fontSize: '16px' }, }, }, defaultVariants: { intent: 'primary', size: 'sm' }, }); ``` ## Compound variants Use `compoundVariants` for styles that should apply only when multiple variant values match. ```ts const badge = styles.component('badge', { variants: { tone: { success: { color: '#166534' }, warning: { color: '#92400e' }, danger: { color: '#991b1b' }, }, size: { sm: { fontSize: '12px' }, lg: { fontSize: '14px' }, }, }, compoundVariants: [ { variants: { tone: ['success', 'warning'], size: 'lg' }, style: { fontWeight: 700 }, }, ], }); badge({ tone: 'success', size: 'lg' }); // includes "badge-compound-0" badge({ tone: 'danger', size: 'lg' }); // does not include compound class ``` `compoundVariants` supports: - single values: `{ size: 'lg' }` - multi-value arrays: `{ tone: ['success', 'warning'] }` ## Boolean variants Boolean variant dimensions are represented with `"true"` / `"false"` option keys. ```ts const input = styles.component('input', { base: { border: '1px solid #d1d5db' }, variants: { invalid: { true: { borderColor: '#ef4444' }, false: { borderColor: '#d1d5db' }, }, }, defaultVariants: { invalid: false, }, }); input(); // "input-base input-invalid-false" input({ invalid: true }); // "input-base input-invalid-true" ``` ## Multipart `slots` Pass a `slots` array for components with multiple parts (for example root, trigger, and panel). `base`, `variants`, `compoundVariants`, and `defaultVariants` can each target specific slot keys. TypeScript infers each slot name from the array literal, so the return value is typed with those keys (for example `tabs.root`, `tabs.trigger`) and unknown keys are errors. You do not need `as const` on `slots` when you pass an inline array inside `styles.component(...)`. ```ts const tabs = styles.component('tabs', { slots: ['root', 'trigger', 'content'], base: { root: { display: 'grid' }, trigger: { cursor: 'pointer' }, }, variants: { size: { sm: { trigger: { fontSize: '12px' }, content: { padding: '8px' }, }, lg: { trigger: { fontSize: '16px' }, content: { padding: '12px' }, }, }, }, defaultVariants: { size: 'sm' }, }); const c = tabs(); c.root; // class string for the root element c.trigger; c.content; ``` ## Data and ARIA selectors `styles.component` supports all CSS selectors: ```ts const accordionTrigger = styles.component('accordion-trigger', { base: { '&[data-state="open"]': { fontWeight: 600 }, '&[aria-expanded="true"]': { color: '#1d4ed8' }, }, }); ``` ## Attribute-driven variants Some design systems want variant state expressed as `data-*` attributes on the DOM (Radix/shadcn-style: one stable class, `data-variant`/`data-size` legible in the markup) rather than as discrete classes. Set `variantStrategy: 'attribute'` and each `variants` option compiles to a `&[data-{dimension}="{option}"]` selector scoped under the single `base` class, instead of its own class: ```ts const button = styles.component('button', { base: { padding: '8px 16px', borderRadius: '6px' }, variants: { variant: { primary: { backgroundColor: '#0066ff', color: '#fff' }, secondary: { backgroundColor: '#6b7280', color: '#fff' }, }, size: { small: { fontSize: '14px' }, large: { fontSize: '18px' }, }, }, defaultVariants: { variant: 'primary', size: 'small' }, variantStrategy: 'attribute', }); const b = button({ variant: 'primary', size: 'small' }); b.className; // "button-base" b.attrs; // { 'data-variant': 'primary', 'data-size': 'small' } b.props; // { className: 'button-base', 'data-variant': 'primary', 'data-size': 'small' } ; // ``` ### StyleX (compiler, atomic classes) Author-time objects; **`stylex.props`** merges styles at compile time. Hashed atomic classes in the DOM. ```ts import * as stylex from '@stylexjs/stylex'; const s = stylex.create({ base: { padding: '8px 16px', borderRadius: '6px', fontWeight: 500, border: 'none', cursor: 'pointer', }, primary: { backgroundColor: '#2563eb', color: '#fff' }, ghost: { backgroundColor: 'transparent', color: '#2563eb', borderWidth: '1px', borderStyle: 'solid', borderColor: '#2563eb', }, }); ``` ```tsx ``` ### Panda CSS (`cva` / recipes) Codegen from **`panda.config`**; import paths vary (`../styled-system/css` here is illustrative). Token strings like `blue.600` are **your** scale. ```ts import { cva } from '../styled-system/css'; export const button = cva({ base: { px: '4', py: '2', rounded: 'md', fontWeight: 'medium', cursor: 'pointer' }, variants: { intent: { primary: { bg: 'blue.600', color: 'white', borderWidth: '0' }, ghost: { bg: 'transparent', color: 'blue.600', borderWidth: '1px', borderColor: 'blue.600' }, }, }, defaultVariants: { intent: 'primary' }, }); ``` ```tsx ``` ### vanilla-extract (`recipe`) **`.css.ts`** files; bundler emits **static CSS**. Typed variants; class strings usually **hashed**. ```ts import { recipe } from '@vanilla-extract/recipes'; export const button = recipe({ base: { padding: '8px 16px', borderRadius: '6px', fontWeight: 500, border: 'none', cursor: 'pointer', }, variants: { intent: { primary: { background: '#2563eb', color: '#fff' }, ghost: { background: 'transparent', color: '#2563eb', border: '1px solid #2563eb' }, }, }, defaultVariants: { intent: 'primary' }, }); ``` ```tsx ``` ### Emotion / styled-components **Styled component** or `css` prop; **hashed** classes; **tokens** are usually theme objects or vars you wire. ```tsx import styled from '@emotion/styled'; export const Button = styled.button<{ intent?: 'primary' | 'ghost' }>` padding: 8px 16px; border-radius: 6px; font-weight: 500; cursor: pointer; border: none; ${(p) => p.intent === 'ghost' ? `background: transparent; color: #2563eb; border: 1px solid #2563eb;` : `background: #2563eb; color: #fff;`} `; ``` ```tsx ``` ### CSS Modules **Plain CSS** in `.module.css`; bundler scopes names. Variants are **manual** (`clsx`, toggles, BEM). ```css /* Button.module.css */ .base { padding: 8px 16px; border-radius: 6px; font-weight: 500; cursor: pointer; border: none; } .primary { background: #2563eb; color: #fff; } .ghost { background: transparent; color: #2563eb; border: 1px solid #2563eb; } ``` ```tsx import styles from './Button.module.css'; import clsx from 'clsx'; ; ``` ### Plain CSS Global stylesheets, BEM modifiers, or attribute selectors—**no** TS variant layer unless you add one yourself. Maximum portability; colocation and typing are DIY. --- ## Theming architecture: TypeStyles vs. StyleX (and Astryx) Theming is where compiler restrictions surface in practice. **Astryx**—Meta's design system for internal tools, built on **StyleX**—has a genuinely flexible theming story, but it gets there by working _around_ StyleX's constraints. TypeStyles never had those constraints, so the workaround layer doesn't exist. Three concrete differences: ### Token variables: plain custom properties vs. compiler-managed vars - **TypeStyles** — [`tokens.create`](/docs/tokens) emits **plain CSS custom properties** with predictable names (`--app-color-primary` under `scopeId: 'app'`) from any ordinary TypeScript module. Themes are [`tokens.createTheme`](/docs/theming-patterns) surfaces: a stable `theme-{name}` class whose properties override token values for that subtree. Because the variables are just CSS, any stylesheet—yours, legacy, third-party—can read or set them directly. - **StyleX** — `defineVars` also compiles to custom properties, but with **hashed names the compiler owns**, and variable definitions **must live in `.stylex.ts` / `.stylex.js` files** so imports can be resolved statically; theming (`createTheme`) flows through those same files. Astryx's theming flexibility is built by routing around these rules. With TypeStyles there is **no compiler restriction to route around**. TypeStyles theme surfaces also carry a general condition engine—`tokens.when` (media queries, attributes, class names, `or` / `and` / `not` combinators) plus [`tokens.colorMode`](/docs/theming-patterns#light-dark-system-on-data) presets—so "dark when the OS prefers it, unless `data-color-mode="light"` wins" is a declaration, not bespoke infrastructure on top of fixed light/dark modes. ### Distribution: build-always vs. runtime injection with a warning - **TypeStyles** — Token and theme CSS goes through the **same pipeline as every other style**: runtime injection in dev, static CSS via [zero-runtime extraction](/docs/zero-runtime) when you want it. There is no dedicated theme-build command, because none is needed—and therefore no "did you forget to build your theme" state to warn about. - **Astryx** — Themes that weren't compiled ahead of time are **injected at runtime with a console warning**, and users are pushed toward a separate `astryx theme build` step to get static CSS. ### Component overrides: plain CSS vs. a config DSL - **TypeStyles** — [`styles.component`](/docs/components) emits **semantic, deterministic class names** (`button-intent-primary`). A consumer restyling a component writes ordinary CSS targeting that class—any property, any selector, any stylesheet—and [cascade layers](/docs/cascade-layers) keep override order predictable. - **StyleX / Astryx** — Hashed atomic classes can't be targeted from outside the compiler, so Astryx exposes overrides through an **`@scope` + data-attribute configuration DSL**: you can override what the DSL anticipates, in the shapes it anticipates. The pattern across all three: a StyleX-based system must **generate an escape hatch** for each theming capability its compiler forecloses. TypeStyles ships the underlying primitives—real custom properties, readable class names, cascade layers—so the capability is the default, not the workaround. --- ## When TypeStyles is a strong default - **Readable classes** and **scoped CSS variables** for DevTools, legacy CSS, and third-party markup. - **Typed variants** (CVA-/recipe-like) **without** a compiler on day one. - **Incremental adoption** and **`createTypeStyles` + `scopeId`** for libraries or micro-frontends. - Comfortable with **runtime injection in dev**, with an **optional** [zero-runtime](/docs/zero-runtime) path when you need static CSS. ## When another tool might win - **StyleX** — Standardized on Meta’s compiler; want static guarantees and atomic output. - **Panda CSS** — Want codegen utilities and a strict config-first token pipeline. - **vanilla-extract** — Want **zero runtime by default** and are fine with `.css.ts` contracts. - **Emotion / styled-components** — Want the classic styled API and accept runtime (or a separate extraction story). - **CSS Modules** — Mostly hand-written CSS; no first-class variant API in the styling layer. - **Plain CSS** — Zero JS styling layer; maximum portability. ## Practical migration Start with [Migration](/docs/migration): Panda- and CVA-like APIs map closely to **`styles.component`**; Emotion and CSS Modules map well to **`styles.class`** plus [`cx`](/docs/compose) from `'typestyles'`. ## Related docs - [Getting started](/docs/getting-started) - [Design system with tokens](/docs/design-system) - [Theming patterns](/docs/theming-patterns) - [Cascade layers](/docs/cascade-layers) - [Zero-runtime extraction](/docs/zero-runtime) - [Component library setup](/docs/component-library) Source: https://typestyles.dev/docs/framework-comparison --- # Getting Started Install TypeStyles and ship your first typed component styles in a few minutes TypeStyles is **CSS in TypeScript**: style objects (selectors, media queries, pseudos), **typed variants**, **tokens as CSS variables**, and ordinary `className` strings in React, Vue, Svelte, or HTML. In production, TypeStyles extracts styles at build time into a **static CSS file** with zero runtime overhead — the same approach as StyleX and Vanilla Extract. During development, the runtime injects styles for instant feedback with HMR. ## Installation ```bash pnpm add typestyles @typestyles/vite # npm: npm install typestyles @typestyles/vite # yarn: yarn add typestyles @typestyles/vite ``` The bundler plugin gives you **zero-runtime production builds** and **HMR in dev**. Plugins are also available for [Next.js](/docs/zero-runtime#nextjs), [Rollup](/docs/zero-runtime#rollup--rolldown), [esbuild](/docs/zero-runtime#esbuild), and [webpack](/docs/zero-runtime#webpack). > **Prototyping without a plugin?** TypeStyles also works as a pure runtime library — just `npm install typestyles` and skip the plugin. You can add build extraction later without changing any application code. See [Zero-runtime extraction](/docs/zero-runtime#switching-between-modes) for the migration path. ## Set up the Vite plugin ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import typestyles from '@typestyles/vite'; export default defineConfig({ plugins: [react(), typestyles()], }); ``` Create a **convention entry** that imports all your style registrations. The plugin discovers it automatically and extracts CSS on `vite build`: ```ts // src/typestyles-entry.ts import './tokens'; import './components/Button.styles'; import './components/Card.styles'; ``` That's it. In dev you get runtime injection + HMR. In production, `typestyles.css` is emitted as a static asset with no runtime JS. ## Your first styles Use [`createTypeStyles`](/docs/api-reference#createtypestyles-options) once so `styles` and `tokens` share one **`scopeId`** (namespaced CSS variables and predictable class names). Put the module in a file you import from your UI — the live example below includes the full source and a `className` usage snippet. > Interactive demo: https://typestyles.dev/docs/getting-started ```ts import { createTypeStyles } from 'typestyles'; export const { styles, tokens } = createTypeStyles({ scopeId: 'app' }); export const color = tokens.create('color', { primary: '#0066ff', surface: '#ffffff', }); export const button = styles.component('button', { base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '8px 16px', borderRadius: '6px', fontWeight: 500, border: 'none', cursor: 'pointer', color: color.surface, backgroundColor: color.primary, }, variants: { intent: { primary: { backgroundColor: color.primary, color: color.surface }, ghost: { backgroundColor: 'transparent', color: color.primary, border: `1px solid ${color.primary}`, }, }, }, defaultVariants: { intent: 'primary' }, }); ``` Toggle **Ghost** in the demo and check the **DOM** and **Emitted CSS** panels (or DevTools on the preview button). Scoped class names are prefixed with your `scopeId`, and token-backed values resolve to custom properties such as `--app-color-primary`. **What just happened:** definitions register when the module loads. In dev, the runtime injects CSS rules into a managed `
${html}
`); }); app.listen(3000); ``` ## How it works During SSR: 1. **Collection mode**: When `collectStyles()` wraps your render, TypeStyles switches to collection mode 2. **CSS capture**: All styles, tokens, themes, and keyframes are captured to a buffer instead of being injected into the DOM 3. **Single style tag**: The collected CSS is returned as a single string ready to embed in your HTML On the client: 1. **Hydration detection**: TypeStyles looks for an existing `` - `streamingDocumentShell(css)` — doctype + `` with charset and styles + `` - `injectStylesIntoHtml(html, css)` — insert before `` (Remix-style full documents) - `TYPESTYLES_STYLE_ID` — the stable `"typestyles"` id (must match client hydration) ```tsx import { renderToString, renderToPipeableStream } from 'react-dom/server'; import type { Response } from 'express'; import { collectStyles, streamingDocumentShell } from 'typestyles/server'; app.get('/', (req, res: Response) => { const { css } = collectStyles(() => renderToString()); const { pipe } = renderToPipeableStream(, { onShellReady() { res.statusCode = 200; res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.write(streamingDocumentShell(css)); pipe(res); }, onShellError(error) { res.statusCode = 500; console.error(error); res.end(); }, }); }); ``` If your shell opens wrappers around ``, add the matching closing tags in the appropriate callback (`onAllReady` when streaming deferred content, or follow the full pattern in the React docs) so you never write to `res` after it has ended. **Next.js App Router** already streams — prefer `TypestylesStylesheet` or build-time extraction instead of manual two-pass rendering. ## Important considerations ### Style deduplication TypeStyles automatically deduplicates CSS during collection. If multiple components use the same style variant, it's only included once in the output. ### Critical CSS By default, `getRegisteredCss()` returns every rule registered in the process. For large apps, prefer **build-time per-route CSS** on Next.js: `buildTypestylesForNext` writes route-scoped stylesheets and a v2 manifest (see [zero-runtime — Next.js](/docs/zero-runtime#nextjs) and [per-route critical CSS](#per-route-critical-css-nextjs) below). ### Per-route critical CSS (Next.js) `buildTypestylesForNext` traces each App Router `page` plus its layout chain, extracts CSS for modules that import `typestyles`, and writes self-contained files under `app/_typestyles/routes/` (default). The manifest maps route paths to those files: ```json { "version": 2, "css": "app/typestyles.css", "routes": { "/": { "css": "app/_typestyles/routes/index.css" }, "/about": { "css": "app/_typestyles/routes/about.css" } } } ``` At request time, read the pre-built CSS for the current route instead of the full global sheet: ```tsx // app/about/layout.tsx (Server Component) import { getRouteCss } from '@typestyles/next/server'; export default function AboutLayout({ children }: { children: React.ReactNode }) { const css = getRouteCss('/about', { root: process.cwd() }); return ( <> ``` If the IDs don't match, you'll get duplicate styles. ### Memory and cleanup `collectStyles()` manages collection state automatically. After the render function completes and CSS is collected, the internal state is reset. You don't need to manually clean up. ## Troubleshooting ### Styles missing in SSR output Make sure you're actually rendering components that use typestyles during the `collectStyles()` call. If styles are defined but the component isn't rendered, no CSS will be generated. ### Styles appearing twice This happens when the client can't find the server-rendered style tag: 1. Check that the `id` is exactly `"typestyles"` 2. Make sure the style tag is present in the initial HTML 3. Verify no ad blockers or CSP are interfering ### Flash of unstyled content (FOUC) If you see FOUC: 1. Ensure styles are in the ``, not the body 2. Check that the CSS string isn't empty 3. Verify that `collectStyles()` wraps the actual component render, not just an empty render Source: https://typestyles.dev/docs/ssr --- # Style Dictionary & W3C tokens Use TypeStyles alongside the W3C Design Tokens Community Group (DTCG) format and Style Dictionary for a design-tool-to-code pipeline TypeStyles already stores your design tokens as **real CSS custom properties** from TypeScript. The **W3C Design Tokens Community Group** (DTCG) format and **[Style Dictionary](https://styledictionary.com/)** solve a different problem: **exchange**. They let designers, documentation sites, iOS / Android apps, and your TypeStyles app all agree on one canonical token set. This guide shows how to combine them without losing the parts that make TypeStyles pleasant: typed access, scoped variables, and no required compiler. ## Who does what - **W3C DTCG** — A **JSON file format**. Each token is an object with `$value`, `$type`, optional `$description`, and `$extensions`. Groups nest tokens; aliases reference other tokens with `"{color.brand.500}"`. This is the interchange layer consumed by Figma plugins, Tokens Studio, documentation tools, etc. - **Style Dictionary** — A **build-time transformer**. It reads DTCG (or its own legacy format), resolves aliases, and emits CSS, SCSS, JS/TS, iOS, Android, and custom formats. It does **not** run at runtime. - **TypeStyles** — The **runtime** that turns TypeScript objects into `var(--…)` custom properties, typed style APIs, themes, and SSR output. It does not read DTCG directly — it consumes plain TS values. The natural split: **DTCG is your source of truth**, **Style Dictionary generates a TypeScript module**, and **TypeStyles consumes that module** through `tokens.create(…)`. ``` Figma / Tokens Studio → tokens.json (DTCG) → Style Dictionary → tokens/generated.ts → tokens.create(...) │ ▼ real CSS custom properties ``` ## A minimal DTCG file ```json // tokens/core.tokens.json { "color": { "brand": { "500": { "$value": "#3b82f6", "$type": "color" }, "600": { "$value": "#2563eb", "$type": "color" } }, "text": { "default": { "$value": "{color.brand.600}", "$type": "color" } } }, "space": { "sm": { "$value": "0.5rem", "$type": "dimension" }, "md": { "$value": "1rem", "$type": "dimension" }, "lg": { "$value": "1.5rem", "$type": "dimension" } } } ``` The aliased value `{color.brand.600}` is resolved by Style Dictionary at build time — by the time your TS module runs, `color.text.default` is already `#2563eb`. ## Workflow A — DTCG → Style Dictionary → TypeStyles ### 1. Install and configure ```bash pnpm add -D style-dictionary # npm: npm install -D style-dictionary # yarn: yarn add -D style-dictionary ``` Style Dictionary v4 parses the DTCG format natively. Create a config that emits **a flat TypeScript primitives module** — no CSS output, because TypeStyles owns CSS generation. ```js // sd.config.mjs import StyleDictionary from 'style-dictionary'; const sd = new StyleDictionary({ source: ['tokens/**/*.tokens.json'], platforms: { ts: { transformGroup: 'js', buildPath: 'src/tokens/generated/', files: [{ destination: 'primitives.ts', format: 'typestyles/primitives' }], }, }, }); StyleDictionary.registerFormat({ name: 'typestyles/primitives', format: ({ dictionary }) => { const groups = {}; for (const token of dictionary.allTokens) { const [group, ...rest] = token.path; const key = rest.join('-'); // e.g. "brand-500" (groups[group] ??= {})[key] = token.$value ?? token.value; } const body = Object.entries(groups) .map(([name, vals]) => `export const ${name} = ${JSON.stringify(vals, null, 2)} as const;`) .join('\n\n'); return `// AUTO-GENERATED. Do not edit by hand.\n${body}\n`; }, }); await sd.buildAllPlatforms(); ``` Run it whenever tokens change: ```bash node sd.config.mjs ``` This produces `src/tokens/generated/primitives.ts`: ```ts // AUTO-GENERATED. Do not edit by hand. export const color = { 'brand-500': '#3b82f6', 'brand-600': '#2563eb', 'text-default': '#2563eb', } as const; export const space = { sm: '0.5rem', md: '1rem', lg: '1.5rem', } as const; ``` ### 2. Feed primitives into `tokens.create` Keep the DTCG-derived primitives **separate** from your semantic token layer, so the code stays readable and refactors at the semantic level do not touch design-tool output. ```ts // src/tokens/index.ts import { createTypeStyles } from 'typestyles'; import { color as corePrimitives, space as corePrimitivesSpace } from './generated/primitives'; export const { styles, tokens } = createTypeStyles({ scopeId: 'app' }); export const color = tokens.create('color', { brand: corePrimitives['brand-500'], brandHover: corePrimitives['brand-600'], text: corePrimitives['text-default'], }); export const space = tokens.create('space', corePrimitivesSpace); ``` `color.brand` is now `var(--app-color-brand)`; the underlying hex came from DTCG. ### 3. Wire it into your build Add a prebuild script so the generated file is fresh before type-checking: ```json // package.json { "scripts": { "tokens:build": "node sd.config.mjs", "dev": "pnpm tokens:build && vite", "build": "pnpm tokens:build && vite build" } } ``` Commit `src/tokens/generated/*.ts` so tooling (editors, CI, TypeScript) always sees the resolved values. ## Workflow B — TypeStyles → DTCG (export to design tools) Going the other direction — making your TS primitives discoverable to Figma plugins or Tokens Studio — takes a few lines of plain JavaScript. Export the **primitive objects** (not `tokens.create` outputs — those are `var(...)` strings) and walk them into DTCG shape: ```ts // scripts/export-dtcg.ts import { writeFile } from 'node:fs/promises'; import { primitiveColors } from '../src/tokens/primitives/colors'; import { primitiveSpacing } from '../src/tokens/primitives/spacing'; function toDtcg(values: Record, $type: string): Record { const out: Record = {}; for (const [key, value] of Object.entries(values)) { if (value && typeof value === 'object') { out[key] = toDtcg(value as Record, $type); } else { out[key] = { $value: value, $type }; } } return out; } const bundle = { color: toDtcg(primitiveColors, 'color'), space: toDtcg(primitiveSpacing, 'dimension'), }; await writeFile('dist/tokens.json', JSON.stringify(bundle, null, 2)); ``` This emits a valid DTCG file your design team can import. The **primitives** are the right export boundary — semantic tokens like `color.text` are often aliases whose meaning depends on theme and are better expressed as DTCG aliases rather than baked values. ## Themes and color modes across the boundary DTCG does not yet standardise multi-mode tokens. Two pragmatic patterns work well with TypeStyles: ### Pattern 1 — one file per mode ``` tokens/ core.tokens.json # mode-agnostic (spacing, radii, typography) color.light.tokens.json # color/* in light mode color.dark.tokens.json # color/* in dark mode ``` Run Style Dictionary twice (one platform per mode) into separate primitive modules, then feed each into a `tokens.createTheme`: ```ts import { color as lightColor } from './generated/light'; import { color as darkColor } from './generated/dark'; export const darkTheme = tokens.createTheme('dark', { base: { color: { brand: darkColor['brand-500'], text: darkColor['text-default'] } }, }); ``` ### Pattern 2 — DTCG `$extensions` for modes Tokens Studio and many design tools carry mode variants under `$extensions`. Read them in a **custom Style Dictionary format** and emit one primitive module per mode. From TypeStyles' perspective it looks exactly like Pattern 1 — `tokens.createTheme` plus `tokens.colorMode.*` presets (see [Tokens](/docs/tokens) and [Theming patterns](/docs/theming-patterns)). ## Aliases: build-time vs runtime You have two valid strategies for DTCG aliases like `{color.brand.500}`: - **Resolve at build time** — the default Style Dictionary behavior. `color.text.default` lands in your TS module as a literal hex. **Pro:** simple, zero runtime cost. **Con:** the link between semantic and primitive is lost in code. - **Preserve as `var(...)` at runtime** — emit the primitives with `tokens.create`, then reference them from a second `tokens.create` call. Aliases resolve via the CSS cascade. **Pro:** themes can swap primitives without rebuilding. **Con:** requires two token namespaces. ```ts const primitive = tokens.create('primitive-color', { brand500: '#3b82f6' }); const semantic = tokens.create('color', { brand: primitive.brand500, // var(--app-primitive-color-brand500) }); ``` The second strategy is worth the extra layer when you ship **multiple brand themes** that share the same semantic contract. ## Matching external CSS names When Style Dictionary (or another tool) already defines your canonical `--*` naming convention, mirror it with **`nameTemplate`** on `tokens.create` so TypeStyles refs point at the same names TypeStyles injects — without duplicating `:root` blocks from SD. ```ts const tokens = createTokens({ scopeId: 'acme' }); // SD might emit --color-brand-500; match that pattern (drop scope from var names if globals are unscoped). const color = tokens.create('color', generatedPalette, { nameTemplate: ({ segments }) => `--color-${segments.join('-')}`, }); const semantic = tokens.create( 'color-semantic', { text: { primary: color.brand[500] }, }, { nameTemplate: ({ path }) => `--ds-color-${path}`, }, ); ``` **Defaults are still recommended** for new apps — custom names are for interop and migration, not aesthetics. **`scopeId`** on `createTokens` / `createStyles` still scopes theme **classes** even when var names match globals. ## Gotchas - **Do not let Style Dictionary emit the CSS.** If both Style Dictionary and TypeStyles ship `:root { --color-brand: … }`, you get duplicate declarations and scoping conflicts. Keep TypeStyles as the single source of CSS. - **Do not pass `tokens.create` output to DTCG exporters.** It contains `var(--…)` strings, not literal values. Export the underlying primitive objects instead. - **Type your generated module.** Keep `as const` in the Style Dictionary format so TypeStyles' inference preserves literal key types. Missing keys become type errors at the `tokens.create` call site. - **Version the JSON, generate the TS.** Treat `tokens/*.tokens.json` as the reviewed artefact and `src/tokens/generated/**` as derived output. Some teams commit both; some gitignore the generated TS and rebuild in CI. Either works — pick one and stick with it. - **Scope still matters.** Style Dictionary does not know about TypeStyles' `scopeId`. When you change `scopeId`, only the TypeStyles side updates — the DTCG file is unchanged because it describes design intent, not emission naming. ## Related - [Tokens](/docs/tokens) — the `tokens.create`, `createTokens`, and `createTheme` APIs. - [Design system](/docs/design-system) — three-layer token architecture (primitives / semantic / component) that this pipeline slots into. - [Theming patterns](/docs/theming-patterns) — multi-theme and color-mode strategies. - [Open Props](/docs/open-props) — pre-built primitive tokens, an alternative starting point when you do not have a design-tool source of truth yet. Source: https://typestyles.dev/docs/style-dictionary --- # Styles Create and compose style variants with styles.component The `styles` API lets you define named style variants and compose them at the call site. `styles.component()` is the unified API for creating component styles. It supports both **flat** configs (simple named variants) and **dimensioned** configs (typed `variants`, `compoundVariants`, `defaultVariants`). For the full dimensioned variant API, see [Components](/docs/components). ## How TypeStyles runs 1. **Registration** — When your module loads, definitions are registered. Nothing paints until a class is actually used. 2. **First use** — The first time a returned class name is applied, TypeStyles injects the rules into a managed `
${html}
`); }); ``` ## Component overrides (two-tier model) When a theme needs to change how a component looks beyond token overrides, there are two tiers — pick the lightest option that fits. ### Tier 1 — component-scoped CSS custom properties (preferred) If a component author exposed a property as a CSS custom property (`ctx.vars` / `c.vars`), consumers override it per theme with ordinary token or theme CSS. Custom properties inherit down the DOM and reset at each `.theme-*` boundary, so nested themes stay proximity-correct with no extra tooling. See [Components — expose themeable properties as vars](/docs/components#expose-themeable-properties-as-vars). ### Tier 2 — plain CSS against semantic class names For properties the author did not expose as vars, target the stable semantic class names from `styles.component()` (for example `button-base`, `button-intent-primary`). See [Class naming](/docs/class-naming) and the [public contract](#public-semantic-class-names). **Non-nested themes:** a descendant selector in a later cascade layer is enough: ```ts import { createStyles } from 'typestyles'; const styles = createStyles({ layers: ['components', 'overrides'] as const, }); const button = styles.component( 'button', { base: { padding: '8px 16px' } }, { layer: 'components' }, ); // In theme setup for `.theme-acme`: styles.class('.theme-acme .button-base', { borderRadius: '999px' }, { layer: 'overrides' }); ``` **Nested conflicting themes:** when two `.theme-*` regions nest and both override the same component class, plain selectors tie on specificity and source order wins. Use `styles.scope()` so the nearest scoping root wins: ```ts styles.scope({ root: '.theme-beta', to: '.theme-acme', layer: 'overrides' }, 'button-base', { backgroundColor: 'rebeccapurple', '&:hover': { opacity: 0.9 }, }); ``` `styles.scope()` reuses the same `serializeStyle` / `applyLayerToRules` / `insertRules` pipeline as other TypeStyles APIs — pseudo-selectors and `@media` in `overrides` work unchanged. **Browser support:** `@scope` ships in Chrome 118+, Firefox 128+, Safari 17.4+. Treat it as an opt-in escalation for nested conflicts; older browsers can keep Tier 2 plain selectors and accept the documented load-order caveat. ## Public semantic class names In **`semantic` naming mode** (the default), every class emitted by `styles.component()` / `styles.class()` is a **public, semver-guarded surface**. Consumers may target those names in plain CSS, `styles.scope()`, or any other CSS tooling. Renaming a namespace or variant key is a **breaking change** — TypeScript will not catch a renamed string literal, so publishable design systems should opt into the [`@typestyles/no-removed-public-classname`](/docs/publishing-packages#guard-public-class-names) rule and regenerate `.typestyles-public-classnames.json` deliberately when names change. ## Best practices 1. **Use semantic token names** - `primary`, `surface`, `text` instead of `blue`, `white`, `black` 2. **Define dark mode alongside light mode** - Keep them in sync 3. **Test both themes** - Use visual regression for both modes 4. **Respect system preferences** - Default to `prefers-color-scheme` 5. **Provide user override** - Let users choose independently of system 6. **Store preference** - Use localStorage to remember user choice 7. **Avoid theme flash** - Set theme class before first paint 8. **Use CSS custom properties** - They cascade naturally for nested themes Source: https://typestyles.dev/docs/theming-patterns --- # Tokens Design tokens and theming with tokens.create, createTheme, and color modes Tokens are design primitives (colors, spacing, etc.) exposed as CSS custom properties. They keep your styles consistent and make theming straightforward. ## Scoped token instances The default `import { tokens } from 'typestyles'` is unscoped. For a **package or micro-frontend** that shares the page with other TypeStyles bundles, call **`createTokens({ scopeId })`** once and reuse that instance so custom properties and theme classes do not collide: ```ts import { createTokens } from 'typestyles'; export const tokens = createTokens({ scopeId: 'acme-ui' }); const color = tokens.create('color', { primary: '#0066ff' }); // var(--acme-ui-color-primary) ``` See [Class naming](/docs/class-naming) for how this pairs with `createStyles({ scopeId })` for styles. To share a **cascade layer** stack with styles, use **`createTypeStyles`** or pass **`layers`** and **`tokenLayer`** to `createTokens` (see [Cascade layers](/docs/cascade-layers)). ## Creating tokens Use `tokens.create(prefix, object)` to define a set of tokens: ```ts import { tokens } from 'typestyles'; const space = tokens.create('space', { xs: '4px', sm: '8px', md: '16px', lg: '24px', }); const color = tokens.create('color', { primary: '#0066ff', text: '#111827', border: '#e5e7eb', }); ``` Each value becomes a CSS custom property: `--space-xs`, `--color-primary`. The create function returns an object of the same shape whose values are `var(--prefix-key)` so you can use them in styles: ```ts padding: space.md, // var(--space-md) backgroundColor: color.primary, // var(--color-primary) ``` When you use **`createTypeStyles({ scopeId: 'app' })`**, the same `tokens` instance emits scoped names (for example `--app-space-md`). Add new namespaces in any module that imports `tokens` from your shared `./typestyles` module: ```ts import { tokens } from './typestyles'; export const space = tokens.create('space', { sm: '8px', md: '16px', }); // With scopeId 'app': padding: space.md → var(--app-space-md) ``` ## Custom CSS variable names (`nameTemplate`) The default naming pattern (`--{scopeId}-{namespace}-{path}`) is recommended for greenfield TypeStyles apps. When migrating from an existing CSS variable system, matching Style Dictionary output, or aliasing across namespaces, pass an optional **`nameTemplate`** function to control emitted `--*` names while keeping typed `var(--…)` references and theme integration. ```ts const tokens = createTokens({ scopeId: 'acme' }); const primitive = tokens.create('color', palette, { nameTemplate: ({ segments }) => `--color-${segments.join('-')}`, }); // --color-brand-500 (no acme- prefix on these vars) const semantic = tokens.create( 'semantic-color', { text: { primary: primitive.brand[500] }, }, { nameTemplate: ({ path }) => `--ds-color-${path}`, }, ); // --ds-color-text-primary: var(--color-brand-500) ``` Set a default on the instance with `createTokens({ nameTemplate })`, or override per namespace in `tokens.create(…, { nameTemplate })`. Templates receive `scopeId`, `scope`, `namespace`, flattened `path`, and `segments` (object keys at each nesting level — use `segments` when your external spec uses a different joiner than `-`). **Migration notes:** - Omitting `scopeId` from a custom template restores cross-package collision risk — keep `scopeId` on **classes** even when vars match global names. - Theme overrides use the same names registered at `tokens.create` time; renaming a template after shipping is a breaking change for plain CSS targeting `--*`. - Do not let Style Dictionary emit `:root` CSS — TypeStyles remains the single injector. Mirror SD naming via `nameTemplate` instead. See [Style Dictionary & W3C tokens](/docs/style-dictionary#matching-external-css-names) for pipeline examples. ## Referencing tokens defined elsewhere When tokens are created in another module or package, use `tokens.use(namespace)` to get the same `var(--namespace-key)` references **without** emitting another `:root` rule. The namespace must already be registered (via `tokens.create`) before those variables exist in CSS. ### Type inference (cross-package) `tokens.create()` returns a branded ref. Pass that ref to `tokens.use()` so consumers get the same typed shape without duplicating a manual generic: ```ts // design-system/tokens.ts export const space = tokens.create('space', { sm: '8px', md: '16px' }); // app/styles.ts import { space as spaceTokens } from '@acme/design-system'; const space = tokens.use(spaceTokens); space.md; // string — typed as var(--space-md) ``` For string-only lookups inside one package, declare a registry on `createTokens()`: ```ts type DesignTokens = { space: { sm: '8px'; md: '16px' }; color: { primary: '#0066ff' }; }; const tokens = createTokens(); const space = tokens.use('space'); // typed from Registry ``` Export `InferTokenValues` when consumers must reference tokens by namespace string. ## Theming Use `tokens.createTheme(name, config)` to register a **theme surface**: a class `theme-{name}` whose custom properties override token values for that subtree. - **`base`** — Overrides always applied on the surface (typical light / default brand). - **`modes`** — Extra layers with explicit `tokens.when.*` conditions. - **`colorMode`** — Preset layers from `tokens.colorMode.*` (mutually exclusive with `modes`). ```ts const dark = tokens.createTheme('dark', { base: { color: { primary: '#66b3ff', text: '#e0e0e0', surface: '#1a1a2e', }, }, }); ``` `createTheme` returns a **`ThemeSurface`** (`className`, `name`, string coercion). Pass **`dark.className`** to DOM or React `className` props, or use `String(dark)` / `` `${dark}` `` in templates. ```ts document.body.classList.add(dark.className); ``` **Shorthand — dark under `prefers-color-scheme` only:** ```ts const autoDark = tokens.createDarkMode('app', { color: { text: '#e5e7eb', surface: '#0f172a' }, }); ``` **Preset — system + `data-color-mode` toggle:** ```ts const light = { color: { text: '#111', surface: '#fff' } }; const dark = { color: { text: '#eee', surface: '#111' } }; const shell = tokens.createTheme('shell', { base: light, colorMode: tokens.colorMode.systemWithLightDarkOverride({ attribute: 'data-color-mode', values: { light: 'light', dark: 'dark', system: 'system' }, scope: 'ancestor', light, dark, }), }); ``` Other presets: `tokens.colorMode.mediaOnly`, `attributeOnly`, `mediaOrAttribute`. Condition primitives: `tokens.when.media`, `prefersDark`, `attr`, `className`, `selector`, `and`, `or`, `not`. `attr` and `className` take a `scope` of `'self'`, `'ancestor'`, or `'descendant'` describing where the marker lives relative to the theme root (see [Theming patterns](/docs/theming-patterns#condition-scopes-self-ancestor-descendant)). See [Theming patterns](/docs/theming-patterns) for end-to-end examples. ## Interop with DTCG and Style Dictionary If your tokens originate in Figma, Tokens Studio, or another design tool that emits the **W3C Design Tokens Community Group (DTCG)** JSON format, use **Style Dictionary** as a build step that emits a plain TypeScript primitives module — then feed that module into `tokens.create(…)` here. See [Style Dictionary & W3C tokens](/docs/style-dictionary) for the full pipeline in both directions. Source: https://typestyles.dev/docs/tokens --- # Troubleshooting Common issues and how to fix them Common issues you might encounter and their solutions. ## Styles not applying ### Check the class name First, verify the class name is being applied: ```tsx function Button() { return ; } ``` Open DevTools and check that the element has the expected class: ```html ``` **If no class is present:** - Check that the component is actually rendering - Verify the style definition is being imported - Ensure no JavaScript errors are preventing execution **If class is present but styles don't apply:** - Check the computed styles in DevTools - Verify no other CSS is overriding your styles - Look for CSS specificity issues ### Check CSS injection Styles are injected lazily. Open DevTools and look for a ` ``` **If the style tag is missing:** - Ensure typestyles is actually being imported - Check that components using the styles are rendering - Verify no bundler issues (check console for errors) **If styles are in the tag but not applied:** - Check for CSS syntax errors - Verify the class name in HTML matches the CSS selector - Look for ad blockers that might be removing styles ## Duplicate namespace warnings ### What it means ``` Style namespace "button" is also used in /path/to/other/file.ts. Duplicate namespaces cause class name collisions. ``` This means you've created two different styles with the same namespace: ```ts // File A const button = styles.component('button', { ... }); // File B const button = styles.component('button', { ... }); // Same namespace! ``` ### How to fix Use unique, descriptive namespaces: ```ts // ✅ Good - descriptive names const iconButton = styles.component('icon-button', { ... }); const textButton = styles.component('text-button', { ... }); const submitButton = styles.component('submit-button', { ... }); // ❌ Bad - generic names that collide const button = styles.component('button', { ... }); const button2 = styles.component('button', { ... }); // Collision! ``` ## TypeScript errors ### "Property does not exist" ``` Property 'tertiary' does not exist on type '{ primary: string; secondary: string; }' ``` You're trying to access a token that doesn't exist: ```ts const color = tokens.create('color', { primary: '#0066ff', secondary: '#6b7280', }); color.tertiary; // Error! This token doesn't exist ``` **Fix:** Add the missing token or use an existing one. ### "No overload matches this call" ``` No overload matches this call. The last overload gave the following error. Argument of type 'string' is not assignable to parameter of type... ``` You're passing an invalid variant: ```ts const button = styles.component('button', { base: { ... }, primary: { ... }, }); button({ secondary: true }); // Error! 'secondary' is not a valid flat flag on this recipe ``` **Fix:** Pass only variant keys that exist on the recipe (for dimensioned components, use the correct dimension keys and option names). ### Cannot find module 'typestyles' ``` Cannot find module 'typestyles' or its corresponding type declarations. ``` **Fix:** 1. Make sure typestyles is installed: ```bash npm install typestyles ``` 2. Check your import path: ```ts // ✅ Correct import { styles } from 'typestyles'; // ❌ Incorrect import { styles } from './typestyles'; ``` 3. Restart TypeScript server in your editor ## SSR issues ### Styles not included in SSR output **Symptom:** Page renders without styles, then styles appear after hydration (flash of unstyled content). **Cause:** Not using `collectStyles()` during render. **Fix:** ```ts import { collectStyles } from 'typestyles/server'; import { renderToString } from 'react-dom/server'; // ❌ Wrong const html = renderToString(); // ✅ Correct const { html, css } = collectStyles(() => renderToString()); // Include css in your HTML response ``` For Next.js, follow the [SSR guide](/docs/ssr) and use `@typestyles/next` (`getRegisteredCss`, `TypestylesStylesheet`, or `@typestyles/next/server` helpers) so the document matches what App Router streams. ### Hydration mismatch **Symptom:** React warning about hydration mismatch or styles appearing twice. **Cause:** Mismatch between server and client CSS injection. **Fix:** 1. Ensure the style tag ID matches: ```html ``` 2. Don't manually create the style tag on client: ```tsx // ❌ Don't do this on client document.head.innerHTML += ``; // ✅ TypeStyles handles this automatically ``` ### Empty CSS in SSR **Symptom:** `css` string is empty. **Cause:** Styles aren't being created during the render pass. **Fix:** Make sure components with typestyles are actually rendered inside `collectStyles()`: ```ts // ❌ Wrong - App doesn't use typestyles components const { css } = collectStyles(() => renderToString()); // css is empty because App doesn't use styles // ✅ Correct - Components with styles are rendered const { css } = collectStyles(() => renderToString( ); } ``` ### Stricter object literals You can add `as const` to **nested values** when you want literal types preserved (for example token-like maps). For `styles.component`, variant keys are inferred from the config object, and **multipart `slots` names are inferred from a `slots` array literal** (no `as const` needed when the array is written inline in the config). Use explicit component prop types when you need a narrower public API than the style keys alone. ## Utility types ### Extracting style types ```ts import { styles } from 'typestyles'; const card = styles.component('card', { base: { ... }, elevated: { ... }, }); // Optional argument is a partial of flat variant flags type CardOptions = Parameters[0]; // ^? { elevated?: boolean } | undefined // Create a type for your component props type CardProps = { elevated?: boolean; }; ``` ### Token type extraction ```ts import { tokens } from 'typestyles'; const themeTokens = { color: tokens.create('color', { primary: '#0066ff', secondary: '#6b7280', }), space: tokens.create('space', { sm: '8px', md: '16px', }), }; // Extract specific token types type ColorToken = keyof typeof themeTokens.color; // ^? 'primary' | 'secondary' type SpaceToken = keyof typeof themeTokens.space; // ^? 'sm' | 'md' ``` ## Type-safe themes ### Theme type definition ```ts // types/theme.ts export interface Theme { color: { primary: string; secondary: string; text: string; surface: string; }; space: { sm: string; md: string; lg: string; }; } // Ensure your tokens match the theme export const color = tokens.create('color', { primary: '#0066ff', secondary: '#6b7280', text: '#111827', surface: '#ffffff', }); // TypeScript will error if you miss a key ``` ### Theme-aware components ```ts import { tokens } from 'typestyles'; const themeTokens = { color: tokens.create('color', { primary: '#0066ff', secondary: '#6b7280', }), space: tokens.create('space', { sm: '8px', md: '16px', }), }; interface ThemedComponentProps { color: keyof typeof themeTokens.color; space: keyof typeof themeTokens.space; } function ThemedComponent({ color, space }: ThemedComponentProps) { const inline = { color: themeTokens.color[color], padding: themeTokens.space[space], }; return
Content
; } // Usage with autocomplete: // ``` ## Generic components ### Generic style components ```ts import { styles } from 'typestyles'; const box = styles.component('box', { base: { padding: '16px' }, variants: { tone: { default: {}, elevated: { boxShadow: '0 4px 6px rgba(0,0,0,0.1)' }, }, }, defaultVariants: { tone: 'default' }, }); function StyledBox({ tone = 'default', children, }: { tone?: 'default' | 'elevated'; children: React.ReactNode; }) { return
{children}
; } // Usage Content; ``` ## Conditional types ### Responsive style types ```ts type Breakpoint = 'sm' | 'md' | 'lg' | 'xl'; type ResponsiveValue = T | Partial>; interface ResponsiveProps { padding: ResponsiveValue; display: ResponsiveValue<'block' | 'flex' | 'grid'>; } // Implementation would handle responsive logic ``` ### Variant combinations ```ts // Type for all possible button combinations type ButtonVariant = | { variant: 'primary'; size: 'small' | 'medium' | 'large' } | { variant: 'secondary'; size: 'small' | 'medium' | 'large' } | { variant: 'ghost'; size: 'small' | 'medium' }; function Button(props: ButtonVariant & { children: React.ReactNode }) { const { variant, size, children } = props; // Implementation } // TypeScript enforces valid combinations: Button({ variant: 'primary', size: 'large', children: 'Click' }); // ✓ Button({ variant: 'ghost', size: 'large', children: 'Click' }); // ✗ Error: 'large' not assignable ``` ## Module augmentation ### Extending typestyles types If you need to add custom types to typestyles: ```ts // types/typestyles.d.ts declare module 'typestyles' { export interface CSSProperties { // Add custom properties 'anchor-name'?: string; 'position-anchor'?: string; // Add custom values to existing properties display?: 'block' | 'flex' | 'grid' | 'custom-value'; } } ``` ## Type guards ### Safe variant checking ```ts import { styles } from 'typestyles'; const button = styles.component('button', { base: { padding: '8px 12px' }, variants: { intent: { primary: { color: 'white', backgroundColor: '#2563eb' }, secondary: { color: '#111', backgroundColor: '#e5e7eb' }, ghost: { color: '#111', backgroundColor: 'transparent' }, }, }, defaultVariants: { intent: 'primary' }, }); function isValidVariant( variant: string ): variant is 'primary' | 'secondary' | 'ghost' { return ['primary', 'secondary', 'ghost'].includes(variant); } function Button({ variant }: { variant?: string }) { const intent = variant && isValidVariant(variant) ? variant : 'primary'; return ; } ``` ## Configuration types ### Strict style configuration ```ts // styles/config.ts import { type CSSProperties, styles } from 'typestyles'; interface StyleConfig { namespace: string; base: CSSProperties; variants: Record; } function createStrictStyles(config: StyleConfig) { const { namespace, base, variants } = config; return styles.component(namespace, { base, ...variants }); } // Usage with full type safety const button = createStrictStyles({ namespace: 'button', base: { padding: '8px' }, variants: { primary: { backgroundColor: 'blue' }, }, }); button({ primary: true }); // ✓ base + primary classes ``` ## Type narrowing ### Narrowing with type predicates ```ts import { styles } from 'typestyles'; type ButtonVariant = 'primary' | 'secondary' | 'ghost'; function isButtonVariant(value: string): value is ButtonVariant { return ['primary', 'secondary', 'ghost'].includes(value); } const button = styles.component('button', { base: { padding: '8px 12px' }, variants: { intent: { primary: { color: 'white', backgroundColor: '#2563eb' }, secondary: { color: '#111', backgroundColor: '#e5e7eb' }, ghost: { color: '#111', backgroundColor: 'transparent' }, }, }, defaultVariants: { intent: 'primary' }, }); function Button({ variant: variantProp }: { variant?: string }) { const intent: ButtonVariant = isButtonVariant(variantProp ?? '') ? variantProp : 'primary'; return ; } ``` ## Best practices 1. **Let types be inferred** when possible 2. **Define explicit interfaces** for component props 3. **Use `as const`** on nested maps when you need literal types 4. **Extract shared types** to avoid duplication 5. **Leverage `keyof`** for token-based props 6. **Use strict mode** for best type safety 7. **Export types** that consumers might need 8. **Document complex types** with JSDoc comments ## Complex CSS values (`calc`, `clamp`, and other parentheses) Property values are typed as **strings** (or numbers where unit conversion applies). TypeScript cannot tell whether `calc(…)`, `clamp(…)`, `min(…)`, `max(…)`, `url(…)`, or similar is syntactically valid. A single missing or extra `)` often produces **invalid CSS**; depending on where it appears, the browser may ignore one declaration or mis-parse **many rules** after it. ### Built-in helpers: `calc` and `clamp` TypeStyles exports **`calc`** (a tagged template) and **`clamp(min, preferred, max)`** so the outer `calc(…)` / `clamp(…)` parentheses are always emitted together: ```ts import { calc, clamp, styles } from 'typestyles'; styles.class('sidebar', { height: calc`100vh - 2 * ${t.space[4]}`, fontSize: clamp('0.875rem', '2vw', '1.125rem'), }); ``` You still need valid syntax inside the template (for example balanced parentheses in nested `min()` / `max()`), but you won’t forget the closing `)` of `calc` itself. ### Other patterns 1. **Manual template literals** — keep `calc(` and `)` in one literal and interpolate only the middle if you prefer not to use the tag: ```ts height: `calc(100vh - 2 * ${t.space[4]})`, ``` 2. **Custom helpers** when you repeat the same shape: ```ts function calcSubtract(minuend: string, subtrahend: string) { return `calc(${minuend} - ${subtrahend})`; } ``` 3. **Name intermediate pieces** if a value gets long—easier to review than a single huge string. 4. **Validate in the browser** (devtools → computed / rules) when you touch tricky values; there is no full compile-time substitute today. ## Nested keys: `container()`, `has()`, `is()`, `where()` Style objects allow nested keys that start with **`&`** (pseudos, descendants), **`[`** (attributes), or **`@`** (at-rules). When you use a **computed** key next to normal longhands (`color`, `padding`, …), TypeScript must keep that key as a **narrow template literal**. If it widens to plain `string`, the object no longer matches `CSSProperties` and you might be tempted to use `as CSSProperties`. TypeStyles narrows keys when you use the builders with **literal** inputs: - **`styles.container({ minWidth: 400 })`**, **`styles.container('sidebar', { minWidth: 300 })`**, and **`styles.container('(min-width: 1px)')`** infer a concrete `` `@container …` `` key. - **`styles.has('.active')`**, **`styles.is(':hover', ':focus-visible')`**, **`styles.where('.nav')`** infer a concrete `` `&:…` `` key. So this pattern type-checks without casting: ```ts import { styles } from 'typestyles'; styles.class('card', { color: 'inherit', [styles.container({ minWidth: 400 })]: { display: 'grid' }, [styles.has('.expanded')]: { borderColor: 'blue' }, }); ``` When the query or selector is only known as a **`string`** variable at compile time, use **`…styles.atRuleBlock(containerKey, { … })`** (or spread a one-key object) instead of `[someString]: { … }`. See [Custom selectors & at-rules](/docs/custom-at-rules). ## Common type issues ### Issue: "Type instantiation is excessively deep" This can happen with very complex nested styles. Solution: simplify nesting or add explicit type annotations. ```ts // If you get deep type errors, add explicit return type const complex = styles.component('complex', { base: { // very deep nesting }, } as const); // or use explicit type annotation ``` ### Issue: "Property does not exist" Make sure you're importing the correct types: ```ts // ✅ Import from typestyles import type { CSSProperties } from 'typestyles'; // ❌ Don't use React's CSSProperties for styles import type { CSSProperties } from 'react'; // Wrong! ``` ### Issue: "Excessively deep type instantiation" Break complex styles into smaller pieces: ```ts // ❌ Avoid very complex single definitions const complex = styles.component('complex', { base: { // hundreds of lines }, }); // ✅ Break into logical groups const header = styles.component('header', { ... }); const content = styles.component('content', { ... }); const footer = styles.component('footer', { ... }); ``` ## Summary TypeStyles provides excellent TypeScript support out of the box. Key points: - Types are inferred automatically - Strict mode is fully supported - You can extend types for custom use cases - Use explicit interfaces for component props - Leverage TypeScript's type system for variant safety Source: https://typestyles.dev/docs/typescript-tips --- # Vite Plugin Enhance your Vite development experience with HMR and style validation The optional `@typestyles/vite` plugin enhances your development experience with Hot Module Replacement (HMR) and helpful warnings. **Runnable examples:** [examples/vite-app](https://github.com/type-styles/typestyles/blob/main/examples/vite-app/README.md) (React), [examples/vue-app](https://github.com/type-styles/typestyles/blob/main/examples/vue-app/README.md), [examples/svelte-app](https://github.com/type-styles/typestyles/blob/main/examples/svelte-app/README.md), and [examples/typewind](https://github.com/type-styles/typestyles/blob/main/examples/typewind/README.md). Each includes a convention entry and production extraction. ## Installation ```bash npm install -D @typestyles/vite # or pnpm add -D @typestyles/vite # or yarn add -D @typestyles/vite ``` ## Basic setup Add the plugin to your `vite.config.ts`: ```ts import { defineConfig } from 'vite'; import typestyles from '@typestyles/vite'; export default defineConfig({ plugins: [typestyles()], }); ``` For **zero-runtime production** builds while keeping **runtime + HMR in development**, add a [convention style entry](/docs/zero-runtime#vite) (e.g. `src/typestyles-entry.ts`) or set `extract.modules` (see [Zero-runtime extraction](/docs/zero-runtime)). When at least one extraction module resolves, the plugin defaults to `mode: 'build'`: `vite dev` still injects styles at runtime, and `vite build` emits a static CSS file and strips client injection. For loading self-hosted fonts with Vite’s asset pipeline, see [Fonts](/docs/fonts). ## Features ### Hot Module Replacement (HMR) Without the plugin, editing a style file causes a full page reload. With the plugin: - Style changes apply instantly - Component state is preserved - No flicker or re-render cascade The plugin works by: 1. Detecting when a module uses typestyles 2. Injecting HMR accept handlers 3. Invalidating affected style registrations on file change 4. Triggering a targeted update instead of a full reload ### Duplicate namespace warnings The plugin warns you when multiple files use the same namespace: ``` Style namespace "button" is also used in /path/to/other/file.ts. Duplicate namespaces cause class name collisions. ``` This helps catch issues early, since duplicate namespaces can cause unexpected style overwrites. ## Configuration ### Runtime in dev, static CSS in production (recommended for SPAs) This is the usual setup for Vite: **development** uses the typestyles runtime so edits hot-reload without running a separate extraction step; **production** emits a real `.css` asset the browser can cache and parse in parallel with JS. Add a convention entry file that imports every registration side effect (see [Zero-runtime extraction](/docs/zero-runtime)), **or** set `extract.modules` explicitly. You do **not** need to pass `mode` — when modules resolve (discovered or explicit), it defaults to `build`, and the plugin only disables the runtime during `vite build`, not during `vite dev`. **Using discovery (minimal `vite.config`):** ```ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import typestyles from '@typestyles/vite'; export default defineConfig({ plugins: [ react(), typestyles(), // discovers e.g. src/typestyles-entry.ts when present ], }); ``` **Explicit `extract` (multiple entries or custom paths):** ```ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import typestyles from '@typestyles/vite'; export default defineConfig({ plugins: [ react(), typestyles({ extract: { modules: ['src/typestyles-entry.ts'], fileName: 'typestyles.css', }, }), ], }); ``` Link the generated file from `index.html` (for example ``). In dev the file is not emitted yet; the link may 404 harmlessly while the runtime supplies the same rules. Production builds include the asset. To force runtime-only everywhere (no extraction), set `mode: 'runtime'` even when `extract` is present. For dynamic values that cannot be extracted, see `mode: 'hybrid'` in [Zero-runtime extraction](/docs/zero-runtime). ### Disable duplicate warnings If you have a legitimate use case for duplicate namespaces (uncommon), you can disable the warning: ```ts import { defineConfig } from 'vite'; import typestyles from '@typestyles/vite'; export default defineConfig({ plugins: [ typestyles({ warnDuplicates: false, }), ], }); ``` ## How HMR works When you save a file that imports from `typestyles`, the plugin: 1. **Extracts namespaces**: Parses your code to find all `styles.component()`, `tokens.create()`, `createTheme()`, and `keyframes.create()` calls 2. **Injects HMR code**: Adds Vite's `import.meta.hot` handlers to the module 3. **On file change**: - The HMR handler calls `invalidateKeys()` from `typestyles/hmr` - Affected styles are removed from the internal registry - The module re-executes with fresh style definitions - New CSS is injected, replacing the old rules 4. **State preserved**: React/Vue/Svelte components keep their state since only the module containing styles changed ## Example: seeing HMR in action Create a simple Vite app with typestyles: ```ts // src/tokens.ts import { tokens } from 'typestyles'; export const color = tokens.create('color', { primary: '#0066ff', }); // src/styles.ts import { styles } from 'typestyles'; import { color } from './tokens'; export const button = styles.component('button', { base: { backgroundColor: color.primary, padding: '8px 16px', }, }); // src/main.ts import { button } from './styles'; document.getElementById('app').innerHTML = ` `; ``` Run the dev server: ```bash npm run dev ``` Now edit `src/tokens.ts` and change the primary color. The button updates instantly without a page reload! ## Framework integration The plugin works with any Vite-based framework: ### React ```bash npm create vite@latest my-app -- --template react-ts npm install typestyles @typestyles/vite ``` ```ts // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import typestyles from '@typestyles/vite'; export default defineConfig({ plugins: [react(), typestyles()], }); ``` ### Vue ```bash npm create vite@latest my-app -- --template vue-ts npm install typestyles @typestyles/vite ``` ```ts // vite.config.ts import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import typestyles from '@typestyles/vite'; export default defineConfig({ plugins: [vue(), typestyles()], }); ``` ### Svelte ```bash npm create vite@latest my-app -- --template svelte-ts npm install typestyles @typestyles/vite ``` ```ts // vite.config.ts import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; import typestyles from '@typestyles/vite'; export default defineConfig({ plugins: [svelte(), typestyles()], }); ``` ## SSR with Vite HMR hooks are dev-only (`import.meta.hot`). For build-time extraction, production client bundles get `__TYPESTYLES_RUNTIME_DISABLED__` so the sheet does not inject `