

# Migrating from StyleX

Move StyleX components, variables, and themes to Zyzz, with before-and-after examples for each API.

## Overview

Both libraries define styles beside components and compile them to CSS ahead of time. StyleX collects styles in a `create` map and applies them with `props`. Zyzz defines each style with `style`, groups definitions in `namespace styles`, and applies one by calling it.

| StyleX | Zyzz |
| --- | --- |
| `props(a, b)` | [`cx(a(), b())`](#merging-styles) |
| `firstThatWorks` | [Fallback arrays](#fallback-values) |
| `when`<br /> and markers | [Selectors with an empty style](#ancestor-states) |
| Style maps selected by props | [`variants`](#variants) |
| `defineVars` | [Config tokens](#variables) |
| `createTheme` | [`extendVars` and `vars`](#themes) |
| `keyframes` | [`keyframes` from `zyzz/web`](#keyframes) |
| `StyleXStyles`<br /> props | [`className` and `style` overrides](#component-props) |

Migrate one component at a time while StyleX's compiler builds the rest. This card renders the same declarations before and after migrating:

```tsx title="Card.tsx"
import * as stylex from '@stylexjs/stylex'

// create collects named styles in one map
const styles = stylex.create({
  card: { display: 'grid', gap: 16, padding: 24 },
  title: { fontSize: 20, margin: 0 },
})

export function Card() {
  return (
    // props converts each style to className and style props
    <article {...stylex.props(styles.card)}>
      <h2 {...stylex.props(styles.title)}>Account</h2>
    </article>
  )
}
```

```tsx title="Card.tsx"
import { style } from 'zyzz'

export function Card() {
  return (
    // Calling a style returns its props
    <article {...styles.card()}>
      <h2 {...styles.title()}>Account</h2>
    </article>
  )
}

// Styles sit in a namespace at the bottom of the module
namespace styles {
  export const card = style({
    display: 'grid',
    gap: '16px',
    padding: '24px',
  })

  export const title = style({ fontSize: '20px', margin: 0 })
}
```

## Run Both

Add the Zyzz integration from [Getting Started](/docs/introduction/getting-started) beside StyleX's. Keep StyleX's compiler, lint rules, and stylesheet until the last component migrates.

While an element moves over, pass the `className` and `style` from `stylex.props` to the Zyzz style call:

```tsx title="SaveButton.tsx"
import * as stylex from '@stylexjs/stylex'

// Both declarations come from StyleX
const styles = stylex.create({
  button: { borderRadius: 8, padding: 16 },
})

export function SaveButton() {
  return <button {...stylex.props(styles.button)}>Save</button>
}
```

```tsx title="SaveButton.tsx"
import * as stylex from '@stylexjs/stylex'
import { style } from 'zyzz'

// borderRadius stays in StyleX until it moves
const legacy = stylex.create({
  button: { borderRadius: 8 },
})

export function SaveButton() {
  const { className, style, ...props } = stylex.props(legacy.button)

  return (
    // StyleX's class and inline styles pass through as overrides
    <button {...props} {...styles.button({ className, style })}>
      Save
    </button>
  )
}

namespace styles {
  export const button = style({ padding: '16px' })
}
```

Zyzz treats StyleX classes as external CSS, so neither library resolves conflicts with the other. Avoid setting one property from both libraries on the same element.

## Units

StyleX adds `px` to numeric lengths. Zyzz passes numbers through unchanged, so write lengths as strings with units, and keep unitless values such as `lineHeight` and `opacity` as numbers.

```ts
import * as stylex from '@stylexjs/stylex'

export const styles = stylex.create({
  label: {
    // Numbers become pixels, except for unitless properties
    fontSize: 14,
    lineHeight: 1.5,
    padding: 16,
  },
})
```

```ts
import { style } from 'zyzz'

export namespace styles {
  export const label = style({
    // Lengths include units, and unitless values stay numbers
    fontSize: '14px',
    lineHeight: 1.5,
    padding: '16px',
  })
}
```

With a token config, a number can name a token instead. For example, `padding: 4` from `zyzz/default` selects `1rem`.

## Fallback Values

`firstThatWorks` lists the preferred value first. A Zyzz array follows CSS declaration order, so reverse the list and put the preferred value last.

```ts
import * as stylex from '@stylexjs/stylex'

export const styles = stylex.create({
  label: {
    // The preferred value comes first
    color: stylex.firstThatWorks('oklch(60% 0.2 250)', '#2563eb'),
  },
})
```

```ts
import { style } from 'zyzz'

export namespace styles {
  export const label = style({
    // Fallbacks follow CSS order, with the preferred value last
    color: ['#2563eb', 'oklch(60% 0.2 250)'],
  })
}
```

Arrays hold fallbacks, not responsive values. Responsive declarations go under [media queries](#media-queries).

## Merging Styles

`cx` takes applied styles where `stylex.props` takes style objects, so call each style first. Later arguments win conflicts, and `false`, `null`, and `undefined` arguments are skipped.

```tsx title="Label.tsx"
import * as stylex from '@stylexjs/stylex'

const styles = stylex.create({
  base: { color: '#111', padding: 8 },
  selected: { color: '#2563eb' },
})

export function Label(props: Label.Props) {
  return (
    // Later styles win, and falsy arguments are skipped
    <span {...stylex.props(styles.base, props.selected && styles.selected)}>
      Account
    </span>
  )
}

export declare namespace Label {
  type Props = { selected: boolean }
}
```

```tsx title="Label.tsx"
import { cx, style } from 'zyzz'

export function Label(props: Label.Props) {
  return (
    // cx merges applied styles in the same order
    <span {...cx(styles.base(), props.selected && styles.selected())}>
      Account
    </span>
  )
}

export declare namespace Label {
  type Props = { selected: boolean }
}

namespace styles {
  export const base = style({ color: '#111', padding: '8px' })

  export const selected = style({ color: '#2563eb' })
}
```

A shorthand resets its longhands in authored order. StyleX gives longhands priority over shorthands by default, so check merges that mix them, such as `padding` and `paddingLeft`.

## Unsetting Styles

StyleX removes a declaration with `null`. Zyzz has no equivalent, and `unset` would still emit a declaration, so move the removable declaration into its own style and apply it conditionally.

```tsx title="Card.tsx"
import * as stylex from '@stylexjs/stylex'

const styles = stylex.create({
  card: { borderRadius: 8, padding: 16 },
  // null removes the padding set by card
  flush: { padding: null },
})

export function Card(props: Card.Props) {
  return (
    <section {...stylex.props(styles.card, props.flush && styles.flush)}>
      Account
    </section>
  )
}

export declare namespace Card {
  type Props = { flush: boolean }
}
```

```tsx title="Card.tsx"
import { cx, style } from 'zyzz'

export function Card(props: Card.Props) {
  return (
    // Apply the padding only when the card is not flush
    <section {...cx(styles.card(), !props.flush && styles.padded())}>
      Account
    </section>
  )
}

export declare namespace Card {
  type Props = { flush: boolean }
}

namespace styles {
  export const card = style({ borderRadius: '8px' })

  // The removable declaration has its own style
  export const padded = style({ padding: '16px' })
}
```

For finite choices, a `null` selection in [`variants`](#variants) skips a choice and its default.

## Pseudo-Classes

StyleX nests conditions inside each property. Zyzz turns that inside out: base declarations come first, and each condition key holds the declarations it changes.

```ts
import * as stylex from '@stylexjs/stylex'

export const styles = stylex.create({
  button: {
    // Each property lists a value per condition
    backgroundColor: { default: '#2563eb', ':hover': '#1d4ed8' },
    opacity: { default: 1, ':disabled': 0.5 },
  },
})
```

```ts
import { style } from 'zyzz'

export namespace styles {
  export const button = style({
    backgroundColor: '#2563eb',
    opacity: 1,
    // Each condition groups the declarations it changes
    ':hover': { backgroundColor: '#1d4ed8' },
    ':disabled': { opacity: 0.5 },
  })
}
```

Move each `default` into the base, and omit it when it is `null`. Conditions keep their authored order, which decides the result when several match.

## Media Queries

Property-level query branches become query blocks that hold every declaration they change. `@container` and `@supports` conditions move the same way.

```ts
import * as stylex from '@stylexjs/stylex'

export const styles = stylex.create({
  card: {
    // The query is one branch of the property
    padding: {
      default: 16,
      '@media (min-width: 768px)': 24,
    },
  },
})
```

```ts
import { style } from 'zyzz'

export namespace styles {
  export const card = style({
    padding: '16px',
    // One query block can change several declarations
    '@media (min-width: 768px)': { padding: '24px' },
  })
}
```

Query strings shared through `defineConsts` become literal queries or named breakpoints. A CSS variable cannot replace a query threshold. See [Name Queries](/docs/guides/conditions#name-queries).

## Pseudo-Elements

Pseudo-element blocks keep their keys. Keep `content` in `::before` and `::after` rules, since the pseudo-element does not render without it.

```ts
import * as stylex from '@stylexjs/stylex'

export const styles = stylex.create({
  input: {
    color: '#111',
    // The pseudo-element nests inside the style
    '::placeholder': { color: '#666' },
  },
})
```

```ts
import { style } from 'zyzz'

export namespace styles {
  export const input = style({
    color: '#111',
    // The same key works unchanged
    '::placeholder': { color: '#666' },
  })
}
```

## Ancestor States

`stylex.when` matches the state of a marked ancestor or sibling. In Zyzz, an empty `style()` marks the element, and a selector interpolates it, with `&` as the styled element.

```tsx title="Card.tsx"
import * as stylex from '@stylexjs/stylex'

const styles = stylex.create({
  label: {
    opacity: {
      default: 0.5,
      // Matches while a marked ancestor is hovered
      [stylex.when.ancestor(':hover')]: 1,
    },
  },
})

export function Card() {
  return (
    // The default marker identifies the ancestor
    <section {...stylex.props(stylex.defaultMarker())}>
      <span {...stylex.props(styles.label)}>Details</span>
    </section>
  )
}
```

```tsx title="Card.tsx"
import { style } from 'zyzz'

export function Card() {
  return (
    <section {...styles.card()}>
      <span {...styles.label()}>Details</span>
    </section>
  )
}

namespace styles {
  // An empty style marks the ancestor
  export const card = style()

  export const label = style({
    opacity: 0.5,
    // Interpolate the marker without calling it
    selectors: {
      [`${card}:hover &`]: { opacity: 1 },
    },
  })
}
```

Named markers from `defineMarker` become separate empty styles. Sibling conditions become `~` and `+` selectors, so keep the element order. See [Mark an Ancestor](/docs/guides/conditions#mark-an-ancestor).

## Variants

Picking entries from a style map by prop becomes `variants`, which declares the choices, defaults, and compound rules in one definition. `Props.Variants` infers the component's styling props.

```tsx title="Button.tsx"
import * as stylex from '@stylexjs/stylex'

const styles = stylex.create({
  base: { display: 'inline-flex' },
  large: { padding: 16 },
  small: { padding: 8 },
})

export function Button(props: Button.Props) {
  return (
    // The size prop picks an entry from the map
    <button {...stylex.props(styles.base, styles[props.size ?? 'small'])}>
      Save
    </button>
  )
}

export declare namespace Button {
  type Props = { size?: 'large' | 'small' | undefined }
}
```

```tsx title="Button.tsx"
import { type Props, variants } from 'zyzz'

export function Button(props: Button.Props) {
  return <button {...styles.button({ size: props.size })}>Save</button>
}

export declare namespace Button {
  type Props = Props.Variants<typeof styles.button>
}

namespace styles {
  export const button = variants({
    base: { display: 'inline-flex' },
    // The default replaces the fallback in the lookup
    defaultVariants: { size: 'small' },
    variants: {
      size: {
        large: { padding: '16px' },
        small: { padding: '8px' },
      },
    },
  })
}
```

Each recipe styles one element. See [Variants](/docs/guides/variants) for compound rules and conditional choices.

## Dynamic Styles

Both libraries bind runtime values to CSS variables in precompiled rules. A Zyzz callback takes one object of typed values, so each call names its values.

```tsx title="Bar.tsx"
import * as stylex from '@stylexjs/stylex'

const styles = stylex.create({
  // A function entry takes runtime arguments
  bar: (width: string) => ({ width }),
})

export function Bar(props: Bar.Props) {
  return <div {...stylex.props(styles.bar(props.width))} />
}

export declare namespace Bar {
  type Props = { width: `${number}%` }
}
```

```tsx title="Bar.tsx"
import { style } from 'zyzz'

export function Bar(props: Bar.Props) {
  return <div {...styles.bar({ width: props.width })} />
}

export declare namespace Bar {
  type Props = { width: `${number}%` }
}

namespace styles {
  // A callback takes a typed object of runtime values
  export const bar = style((values: { width: `${number}%` }) => ({
    width: values.width,
  }))
}
```

Calculate values before passing them in. A callback cannot add properties, selectors, or queries at runtime.

## Variables

`defineVars` groups become config tokens, grouped by the properties that use them. Styles that import `style` from the config accept token names in place of variable references.

```ts
import * as stylex from '@stylexjs/stylex'

// Variable groups live in a .stylex.ts module
export const colors = stylex.defineVars({
  accent: '#2563eb',
  surface: '#fff',
})

export const spacing = stylex.defineVars({ gap: '16px' })
```

```tsx
import * as stylex from '@stylexjs/stylex'
import { colors, spacing } from './tokens.stylex.js'

const styles = stylex.create({
  // Styles reference each imported variable
  panel: {
    backgroundColor: colors.surface,
    color: colors.accent,
    padding: spacing.gap,
  },
})

export function Panel() {
  return <section {...stylex.props(styles.panel)}>Account</section>
}
```

```ts title="zyzz.config.ts"
import { defineConfig } from 'zyzz'

export const { style } = defineConfig({
  // Each group sets which properties accept its tokens
  vars: {
    color: { accent: '#2563eb', surface: '#fff' },
    spacing: { gap: '16px' },
  },
})
```

```tsx title="Panel.tsx"
import { style } from './zyzz.config.js'

export function Panel() {
  return <section {...styles.panel()}>Account</section>
}

namespace styles {
  export const panel = style({
    // Properties accept token names from the config
    backgroundColor: 'surface',
    color: 'accent',
    padding: 'gap',
  })
}
```

A variable that one component owns, rather than the design system, fits `variable()` instead. See [Inline CSS Variables](/docs/guides/styling#inline-css-variables).

## Themes

`createTheme` overrides a variable group for a subtree. In Zyzz, `extendVars` derives a set that overrides some tokens, and `vars({ set })` applies it to an element and its descendants.

```tsx title="Preview.tsx"
import * as stylex from '@stylexjs/stylex'
import type { ReactNode } from 'react'
import { colors } from './tokens.stylex.js'

// A theme overrides some variables in one group
const mint = stylex.createTheme(colors, { accent: '#047857' })

export function Preview(props: Preview.Props) {
  return (
    // Applying the theme scopes it to this subtree
    <section {...stylex.props(mint)}>{props.children}</section>
  )
}

export declare namespace Preview {
  type Props = { children: ReactNode }
}
```

```ts title="zyzz.config.ts"
import { defineConfig, defineVars, extendVars } from 'zyzz'

const base = defineVars({
  color: { accent: '#2563eb', surface: '#fff' },
  spacing: { gap: '16px' },
})

// The mint set keeps every other token from base
const mint = extendVars(base, { color: { accent: '#047857' } })

export const { style, vars } = defineConfig({
  defaultVars: 'base',
  vars: { base, mint },
})
```

```tsx title="Preview.tsx"
import type { ReactNode } from 'react'
import { vars } from './zyzz.config.js'

export function Preview(props: Preview.Props) {
  return (
    // Selecting the set scopes it to this subtree
    <section {...vars({ set: 'mint' })}>{props.children}</section>
  )
}

export declare namespace Preview {
  type Props = { children: ReactNode }
}
```

Sets share one token contract, so applying one never changes component classes. To combine overrides, define one set that holds all of them. See [Add a Theme](/docs/guides/themes#add-a-theme).

## Dark Mode

Color variables that switch under `prefers-color-scheme` become token pairs. Pairs compile to `light-dark()` and follow the element's `color-scheme`, so set `light dark` on the root to follow the system preference.

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

export const colors = stylex.defineVars({
  // Each variable switches value under the media query
  surface: {
    default: '#fff',
    '@media (prefers-color-scheme: dark)': '#111',
  },
  text: { default: '#111', '@media (prefers-color-scheme: dark)': '#fff' },
})
```

```ts title="zyzz.config.ts"
import { defineConfig } from 'zyzz'

export const { style, vars } = defineConfig({
  vars: {
    color: {
      // A pair holds one value per color scheme
      surface: { dark: '#111', light: '#fff' },
      text: { dark: '#fff', light: '#111' },
    },
  },
})
```

```tsx title="Document.tsx"
import type { ReactNode } from 'react'
import { vars } from './zyzz.config.js'

export function Document(props: Document.Props) {
  return (
    // light dark follows the system preference, like the media query
    <html lang="en" {...vars({ colorScheme: 'light dark' })}>
      <body>{props.children}</body>
    </html>
  )
}

export declare namespace Document {
  type Props = { children: ReactNode }
}
```

A manual toggle needs its saved preference migrated too. See [Switch Themes](/docs/guides/themes#switch-themes) for `appearance.set`, which applies and saves a scheme.

## Keyframes

`stylex.keyframes` becomes `keyframes` from `zyzz/web`, with the same frames. Both return a reference for `animationName`.

```ts
import * as stylex from '@stylexjs/stylex'

// keyframes returns a generated animation name
const fadeIn = stylex.keyframes({ from: { opacity: 0 }, to: { opacity: 1 } })

export const styles = stylex.create({
  notice: {
    animationDuration: '200ms',
    animationName: {
      default: fadeIn,
      '@media (prefers-reduced-motion: reduce)': 'none',
    },
  },
})
```

```ts
import { style } from 'zyzz'
import { keyframes } from 'zyzz/web'

// The same frames, imported from zyzz/web
const fadeIn = keyframes({ from: { opacity: 0 }, to: { opacity: 1 } })

export namespace styles {
  export const notice = style({
    animationDuration: '200ms',
    animationName: fadeIn,
    '@media (prefers-reduced-motion: reduce)': { animationName: 'none' },
  })
}
```

## Component Props

A Zyzz style cannot merge style objects passed through props, since the compiler resolves composition at build time. Accept `className` and `style` overrides instead, or expose [`variants`](#variants) choices for the appearances callers need.

```tsx title="Button.tsx"
import * as stylex from '@stylexjs/stylex'
import type { StyleXStyles } from '@stylexjs/stylex'

const styles = stylex.create({
  button: { padding: 8 },
})

export function Button(props: Button.Props) {
  // Callers pass StyleX styles, merged after the base
  return <button {...stylex.props(styles.button, props.style)}>Save</button>
}

export declare namespace Button {
  type Props = { style?: StyleXStyles | undefined }
}
```

```tsx title="Button.tsx"
import type { CSSProperties } from 'react'
import { style } from 'zyzz'

export function Button(props: Button.Props) {
  return (
    <button
      // Callers pass a class or inline styles as overrides
      {...styles.button({ className: props.className, style: props.style })}
    >
      Save
    </button>
  )
}

export declare namespace Button {
  type Props = {
    className?: string | undefined
    style?: CSSProperties | undefined
  }
}

namespace styles {
  export const button = style({ padding: '8px' })
}
```

An external class follows the normal cascade, and inline styles cannot hold pseudo-classes or queries. See [Accept Overrides](/docs/guides/styling#accept-overrides).

## Remove StyleX

After the last component migrates, remove StyleX's imports, compiler integration, lint rules, and stylesheet entry. The [Oxlint plugin](/docs/api/oxlint) replaces its lint rules.

```sh
# Remove the runtime, then the integration and lint packages
pnpm remove @stylexjs/stylex
```

Compare computed styles in a browser before and after each step, including every theme and color scheme. See [Testing & Troubleshooting](/docs/guides/testing).

## More

[Styling](/docs/guides/styling)

Compose definitions, override styles, and bind dynamic values.

[Conditions](/docs/guides/conditions)

Write state selectors, media and container queries, and element
relationships.

[Themes & Tokens](/docs/guides/themes)

Define tokens, variable sets, and color schemes in a typed config.

[Variants](/docs/guides/variants)

Define typed choices such as size and tone, with defaults and compounds.

[CSS Output](/docs/guides/css-output)

Choose atomic or grouped output and compare the emitted rules.

[Testing & Troubleshooting](/docs/guides/testing)

Test compiled styles in a browser and diagnose missing styles.
