dx-styleszero runtime

CSS-in-TS that compiles away

The styling foundation for design systems. Authoring stays in TypeScript. CSS is statically extracted at build time — no runtime renderer, no provider, no FOUC. Deterministic class names, theme contracts, and RTL on rails.

0kbStyle runtime
100%Static extraction
AutoRTL mirroring
Button.tsx — inline or colocatedts
import { recipe } from "dx-styles";

export const button = recipe({
  base: {
    display: "inline-flex",
    alignItems: "center",
    borderRadius: "999px",
  },
  variants: {
    appearance: {
      primary: { backgroundColor: "var(--button-bg-primary)" },
      secondary: { backgroundColor: "var(--button-bg-secondary)" },
    },
    size: {
      sm: { minHeight: "28px", paddingInline: "10px" },
      md: { minHeight: "32px", paddingInline: "12px" },
    },
  },
  defaultVariants: { appearance: "primary", size: "md" },
});
Why dx-styles

Three guarantees you can build a design system on.

A small authoring surface, an aggressive build-time pipeline, and a framework-agnostic runtime. Designed to scale across applications without shipping a single byte of styling code to the browser.

01no provider · no FOUC
Zero runtime.

dx-styles is not a runtime renderer. CSS is emitted at build time and loaded as normal module-side artifacts. No React provider, no style collector, no flash of unstyled content.

02predictable hashes
Deterministic by design.

Class names are precompiled. The runtime selector only chooses among them. Identical inputs produce identical outputs across builds — no hash drift, no surprises in diffs.

03compile-time mirroring
RTL on rails.

Opt-in compile-time RTL overrides for conservative physical-side declarations. Authors keep writing LTR; extraction handles the mirrored side. ESLint enforces RTL-friendly authoring.

The API

Five primitives. Everything you need for a design system.

Authoring stays in TypeScript. Composition is statically analyzable. Tab through the tagged APIs that participate in extraction — css, recipe, slotRecipe, createTheme — plus the runtime boundary assignVars. Each tab shows the declaration and the JSX that consumes it.

css()

Compose deterministic class names from style objects. Inputs are previously declared css() results, plain style objects, or public StyleHandle values.

1Declare
Button.tsx — colocatedts
import { css } from "dx-styles";

const base = css({
  display: "inline-flex",
  alignItems: "center",
  gap: "8px",
});

export const button = css(base, {
  minHeight: "32px",
  paddingInline: "12px",
  borderRadius: "999px",
});
2Consume
Button.tsx — JSXtsx
export const Button = (props: ButtonProps) => (
  <button className={button} {...props} />
);
Live demo

The same recipe, every component below.

These site-local spike components are rendered with the recipes you see on the right. No runtime style insertion: open the network tab and the CSS arrived as a static asset.

Button · styleMode
Button · size
Checkbox · appearance
What you're seeingEvery variant on this page is a precompiled class. The component just selects between them — there is no style object passed at runtime, no provider in the tree.
From apps/site/src/spike/components/Button.styles.ts
Button.styles.tsts
import { recipe } from "dx-styles";

import { siteTokens } from "../theme";

export const buttonRoot = recipe({
  base: {
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
  },
  variants: {
    size: {
      small:  { minHeight: "24px", paddingInline: "8px"  },
      medium: { minHeight: "32px", paddingInline: "12px" },
      large:  { minHeight: "40px", paddingInline: "16px" },
    },
    styleMode: {
      contained: { backgroundColor: siteTokens["color-accent-strong"] },
      outline:   { backgroundColor: "transparent" },
      text:      { backgroundColor: "transparent" },
    },
  },
});
Theme studio

One base color. A live OKLCH scheme.

Pick a seed color and the site recomputes a balanced dark token set at runtime. Static extraction still owns the CSS rules; the picker only assigns typed CSS variables at the root boundary.

OKLCH · assignVars

The base hue drives accents, borders, cards, and dark surfaces. Every section keeps reading the same token contract, so the whole page shifts without regenerating rules or adding a provider.

#7C8CFFoklch(0.681 0.169 275)
theme-runtime.tsts
import { assignVars } from "dx-styles";

const palette = getSitePalette(baseColor);
const vars = assignVars(siteTokens, palette);

for (const [name, value] of Object.entries(vars)) {
  document.documentElement.style.setProperty(name, String(value));
}

// The fallback createTheme() class stays mounted.
// Runtime variables only override the token values.
Runtime surface
Tokens follow the base hue
base=#7c8cff
ContainedOutlineText
Surface
Accent
Success
Warning
Border
theme fallbackoklch paletteruntime vars
How it works

What you author is exactly what ships.

A small build-time pipeline turns tagged calls into deterministic class names and CSS sidecars. The runtime only chooses between precompiled classes — no string concatenation at runtime, no style object serialization.

Step 1 · author

Tagged calls in TS/TSX

Type-safe authoring with recipe(), slotRecipe(), css(). Keep them inline next to a component, or split into a colocated *.styles.ts — the extractor treats both the same.

recipe(...) · slotRecipe(...) · css(...)
Step 2 · extract

WyW-in-JS transform

Tagged calls are evaluated at build time. Variant matrices compile to deterministic class groups; RTL-flippable rules emit their mirrored sibling.

processors/recipe.cts
Step 3 · ship

*.css sidecars

Built modules import generated CSS through the normal module graph. Bundler tree-shakes unused selectors. Runtime cost is a single cx() call.

sideEffects: ["**/*.css"]
No runtime renderer. No provider. No hydration mismatch surface.framework-agnostic
Developer experience

When the styling layer is honest, debugging is a grep away.

Static extraction makes the styling pipeline auditable. dx-styles ships first-class tooling so you can answer the questions that actually come up: where did this class come from, why is the variant name wrong, and what changed between builds.

explain tool

Explain any class — name to source.

Resolve any emitted class back to its authoring file, recipe node, slot context, and composition edges. Open a CSS bug, run explain, jump straight to the source.

$ bun run explain button_bhhycyd__appearance-primaryshell
class    button_bhhycyd__appearance-primary
source   button/styles.ts:14:5
symbol   button · recipe()
variant  appearance="primary"
compose   focusRing.ts:7  (css)
          styles.ts:18    (recipe.base)
css
  background: var(--button-bg-primary);
  border-color: transparent;
  color: var(--button-fg-primary);
build-time

Source-linked diagnostics at build time.

Direction-aware authoring is enforced by the extractor: physical properties surface as warnings with the file, line, and column. No runtime check, no surprises in production.

vite buildshell
warning  use logical "insetInlineEnd"
         instead of "right"
         so the rule follows document
         direction.

  Pipeline.tsx:78
  >  77 | position: "absolute",
     78 | right: "-14px",
        | ^^^^^
type-safe

Variant typos fail in the editor.

Recipe and slotRecipe variant maps are inferred. Unknown variant axes or values surface in the editor before you ever save the file — no untyped string indices.

Button.tsx — typecheckts
button({ size: "xl" });
//      ~~~~~~~~~~~~
//   Type '"xl"' is not assignable
//   to '"sm" | "md"'.

button({ tone: "danger" });
//       ~~~~
//   Property 'tone' does not exist
//   on type ButtonVariants.
compose, don't fork

Extend recipes without forking.

Component packages expose opaque StyleHandle values via createRecipeStyleHandles. Layer overrides on a recipe root or any slot through plain css() — no source fork, no specificity ladder, no provider in the tree.

compactButton.tsts
import {
  createRecipeStyleHandles,
  css,
} from "dx-styles";

import { button } from "./Button.styles";

const buttonHandles = createRecipeStyleHandles(button);

// Layer overrides on the public handle —
// no source fork, no specificity ladder.
export const compactButton = css(buttonHandles.root, {
  minHeight: "24px",
  paddingInline: "8px",
});

Wire the extractor once. Author the rest as TypeScript.

Add the package, drop the matching @wyw-in-js/* plugin into your bundler (Vite, webpack, Rollup, esbuild, Next; see the WyW-in-JS bundler guide), and start authoring. Tokens, themes, and RTL rules stay in TypeScript modules and compile into static CSS artifacts.

setupts
bun add dx-styles

// Pick the WyW-in-JS plugin for your bundler:
//   @wyw-in-js/vite      @wyw-in-js/webpack
//   @wyw-in-js/rollup    @wyw-in-js/esbuild
//   @wyw-in-js/next
bun add -d @wyw-in-js/vite

// vite.config.ts - same options across plugins
import wyw from "@wyw-in-js/vite";
export default {
  plugins: [wyw()],
};

// Component.tsx - inline or in a colocated *.styles.ts
import { recipe } from "dx-styles";