

# Styling

Define typed styles beside components, apply them as props, and combine them in a predictable order.

## Overview

A `style` definition describes one element with camelCase CSS properties. The compiler reads every definition during the build and writes its rules to a stylesheet. Calling a definition at render time returns props that reference those rules, so the browser never generates CSS.

Most tasks in this guide use three functions, with `style` taking two forms:

* **`style({ … })`:** Declarations, states, and queries for one element.
* **`style((values) => ({ … }))`:** Values that change per instance, bound to precompiled rules.
* **`cx(…)`:** Applied styles merged in argument order, so later conflicts win.
* **`variable(…)`:** A typed CSS variable shared by styles and their descendants.

> [!TIP]
> This guide covers element styles. [Variants](/docs/guides/variants) handle a fixed set of choices such as size or tone, and [Themes & Tokens](/docs/guides/themes) handle values shared across an application.

A component spreads each applied definition onto its element. This card styles an article and its heading with two definitions:

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

export function Card() {
  return (
    <article {...styles.card()}>
      <h2 {...styles.title()}>Account</h2>
      <p>Manage the name, email, and password for this account.</p>
    </article>
  )
}

namespace styles {
  export const card = style({
    border: '1px solid #d4d4d4',
    borderRadius: '8px',
    display: 'grid',
    gap: '4px',
    padding: '1rem',
  })

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



## Walkthrough

The steps below build a save button in React on the web, from its first declarations to states and queries. They assume the compiler setup from [Getting Started](/docs/introduction/getting-started). Native support has separate [capabilities and limits](/docs/introduction/compatibility).

### Define a Style

Call `style` at module scope, and keep a component's definitions in a `namespace styles` at the bottom of its file.

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

namespace styles {
  export const button = style({
    backgroundColor: '#0070f3',
    border: 'none',
    borderRadius: '6px',
    color: 'white',
    padding: '8px 16px',
  })
}
```

Properties and values are typed from CSS data. Editors list the keywords each property accepts, and an unsupported value fails type checking at the declaration:

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

style({ flexDirection: '' })
// completions: inherit, initial, revert-layer, revert, unset, column, column-reverse, row, row-reverse
```

The compiler reads definitions without running application code. Literal values, local `const` records, and typed callbacks qualify. Running source that skipped the transform throws `style.MissingTransformError`, which means the build integration did not process that file.

### Apply the Style

During rendering, call the definition and spread the result onto its element.

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

export function Button() {
  return (
    <button {...styles.button()} type="button">
      Save
    </button>
  )
}

namespace styles {
  export const button = style({
    backgroundColor: '#0070f3',
    border: 'none',
    borderRadius: '6px',
    color: 'white',
    padding: '8px 16px',
  })
}
```

With the default atomic output, the compiler writes one rule per declaration during the build. A call returns only the names of rules that already exist, such as the three rules behind the overview card's title:

```ts
styles.title()
// { className: 'z-font-size-18px z-font-weight-600 z-m-0' }
```

### Add Conditions

Nest declarations under pseudo-class, pseudo-element, attribute, and at-rule keys. In attribute keys, `&` stands for the styled element. Nested keys combine with AND, and each selector keeps its CSS specificity.

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

export function Button() {
  return (
    <button {...styles.button()} type="button">
      Save
    </button>
  )
}

namespace styles {
  export const button = style({
    backgroundColor: '#0070f3',
    border: 'none',
    borderRadius: '6px',
    color: 'white',
    padding: '8px 16px',
    ':hover': { backgroundColor: '#0060df' },
    ':focus-visible': {
      outline: '2px solid #0070f3',
      outlineOffset: '2px',
    },
    '&[aria-busy="true"]': { cursor: 'progress', opacity: 0.6 },
    '@media (min-width: 48rem)': { padding: '12px 24px' },
  })
}
```



Hover darkens the button, keyboard focus adds a ring, and `aria-busy="true"` dims it. At widths of `48rem` and above, the larger padding applies.

Conditional rules keep their authored position in the stylesheet, so a condition overrides only the declarations written before it. [Conditions](/docs/guides/conditions) covers container queries, named breakpoints, and styles that depend on another element.

## Recipes

### Combine Styles

`cx` merges applied styles into one props object. When declarations conflict, the later argument wins. `false`, `null`, and `undefined` arguments are skipped, so `&&` applies a style conditionally.

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

export function Button(props: Button.Props) {
  return (
    <button
      {...cx(styles.button(), props.compact && styles.compact())}
      type="button"
    >
      Save
    </button>
  )
}

export declare namespace Button {
  type Props = { compact?: boolean | undefined }
}

namespace styles {
  export const button = style({ padding: '12px 20px' })

  export const compact = style({ padding: '4px 12px' })
}
```



`<Button compact />` receives the compact padding. With the arguments reversed, `button` would win instead. The compiler emits a rule group for each combination, so argument order decides the result and class-string order never does.

Separate JSX spreads replace each other's `className`, so each element takes one `cx` call. Ternaries and mutated bindings fail compilation, and one call accepts up to eight conditional arguments. The [cx reference](/docs/api/core/cx) covers nesting and packed libraries.

### Accept Overrides

An applied definition also accepts `className`, `style`, and `vars`. A reusable component can forward the first two, so a caller adjusts one instance without another definition.

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

export function Button(props: Button.Props) {
  return (
    <button
      {...styles.button({ className: props.className, style: props.style })}
      type="button"
    >
      {props.children}
    </button>
  )
}

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

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



`<Button style={{ padding: '16px 32px' }}>` receives the larger padding as an inline style. Inline values follow React semantics and skip token checks. An external class follows the normal cascade, and its position in `className` has no effect on which rule wins.

Other inputs are type errors. Event handlers, children, and ARIA attributes belong on the element:

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

const button = style({ padding: '8px 16px' })

button({ onClick: () => {} })
// error: Object literal may only specify known properties, and 'onClick' does not exist in type 'Options<"react"> & Record<never, never>'.
```

### Bind Dynamic Values

A typed callback handles a value that changes per instance, such as progress or a measured size. The compiler turns the callback body into static rules backed by private CSS variables. Each call assigns those variables without creating CSS.

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

export function Meter(props: Meter.Props) {
  return (
    <div
      aria-label="Upload progress"
      aria-valuemax={100}
      aria-valuemin={0}
      aria-valuenow={props.value}
      role="progressbar"
      {...styles.track()}
    >
      <div {...styles.fill({ width: `${props.value}%` })} />
    </div>
  )
}

export declare namespace Meter {
  /** Progress from 0 to 100. */
  type Props = { value: number }
}

namespace styles {
  export const fill = style((values: { width: `${number}%` }) => ({
    backgroundColor: '#0070f3',
    height: '100%',
    width: values.width,
  }))

  export const track = style({
    backgroundColor: '#e5e5e5',
    borderRadius: '4px',
    height: '8px',
    overflow: 'hidden',
  })
}
```



A changing value has a narrower tool in most cases:

* **Fixed choices**, such as size or tone: [Variants](/docs/guides/variants).
* **On and off states**: `cx` with `&&`, or an attribute key such as `&[aria-busy="true"]`.
* **Values computed per instance**, such as progress or coordinates: a typed callback.

Every callback input needs an explicit finite scalar type, and every declared input is required. Optional fields, imported or generic types, calls inside the body, and dynamic rule structure are unsupported. The `number` type above does not check the 0 to 100 range.

### Inline CSS Variables

`variable()` declares a typed CSS variable. Styles read it as a value and assign it with `vars`, either statically in a definition or inline when applying one. Unregistered variables inherit, so descendants read the nearest assignment.

```tsx title="Plan.tsx"
import { style, variable } from 'zyzz'

export function Plan(props: Plan.Props) {
  return (
    <section {...styles.plan({ vars: { [variables.accent]: props.accent } })}>
      <h3 {...styles.name()}>Pro</h3>
      <button {...styles.button()} type="button">
        Choose plan
      </button>
    </section>
  )
}

export declare namespace Plan {
  type Props = { accent?: string | undefined }
}

namespace variables {
  export const accent = variable('color')
}

namespace styles {
  export const button = style({
    backgroundColor: variables.accent,
    border: 'none',
    borderRadius: '6px',
    color: 'white',
    padding: '8px 16px',
  })

  export const name = style({ color: variables.accent, margin: 0 })

  export const plan = style({
    borderLeft: '4px solid',
    borderLeftColor: variables.accent,
    display: 'grid',
    gap: '8px',
    justifyItems: 'start',
    paddingLeft: '16px',
    vars: { [variables.accent]: '#0070f3' },
  })
}
```



`<Plan />` uses the default blue. `<Plan accent="#9333ea" />` turns the border, name, and button purple, because the inline assignment overrides the static one on the same element. An `undefined` value omits the inline assignment.

Computed keys lose each variable's value type in TypeScript. `variables.accent.set(value)` keeps it and returns an assignment for the `style` option. The [variable reference](/docs/api/core/variable) covers registration, which controls inheritance and emits `@property`.

### Use Theme Values

A config's `vars` export holds typed references to its theme values. Interpolate a reference into a CSS expression such as `calc()`. A property constrained to tokens accepts the expression after the ` !custom` suffix.

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

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

namespace styles {
  export const panel = style({
    padding: 'md',
    width: `calc(100% - ${vars.spacing.md}) !custom`,
  })
}
```

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

export const { style, vars } = defineConfig({
  vars: { spacing: { md: '1rem' } },
})
```

References follow the active theme scope. [Themes & Tokens](/docs/guides/themes) covers variable sets, color schemes, and property mappings.

### Share Styles

Definitions used by several components can live in a shared module as top-level exports. Importing the module as a namespace applies the same compiled rules in each component.

```ts title="button.styles.ts"
import { style } from 'zyzz'

export const button = style({
  borderRadius: '6px',
  padding: '8px 16px',
})
```

```tsx title="SaveButton.tsx"
import * as styles from './button.styles.js'

export function SaveButton() {
  return (
    <button {...styles.button()} type="button">
      Save
    </button>
  )
}
```

The bundler integration follows the import to the definitions. A shared module that uses tokens imports the config-bound `style`, so its definitions keep the same token contract. Packages need a build step, described in [Publish Libraries](https://github.com/wevm/zyzz/blob/main/docs/guides/compilation.md#publish-libraries).

### Reuse Declarations

A local `const` record can supply part of several definitions. The compiler copies spread declarations in place and preserves their order.

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

export function Actions() {
  return (
    <nav {...styles.nav()}>
      <button {...styles.button()} type="button">
        Save
      </button>
      <a {...styles.link()} href="/drafts">
        View drafts
      </a>
    </nav>
  )
}

namespace styles {
  const focusRing = {
    ':focus-visible': {
      outline: '2px solid #0070f3',
      outlineOffset: '2px',
    },
  } as const

  export const button = style({ ...focusRing, padding: '8px 16px' })

  export const link = style({ ...focusRing, color: '#0070f3' })

  export const nav = style({ display: 'flex', gap: '12px' })
}
```

Records must stay immutable and local to the module. Reuse a complete style with `cx`, and a record when only part of a definition repeats.

### Add Fallback Values

An array emits each value in order. The browser keeps the last value it supports, so older browsers fall back to `100vh` here.

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

export function Sheet() {
  return <div {...styles.sheet()}>Details</div>
}

namespace styles {
  export const sheet = style({
    display: 'grid',
    height: ['100vh', '100dvh'],
  })
}
```

## More

[Variants](/docs/guides/variants)

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

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

Name shared values, switch color schemes, and scope variable sets to a
section.

[Global Styles](/docs/guides/global-styles)

Style document elements with selector maps beside component styles.

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

Choose atomic or grouped output and see how composition maps to emitted
rules.

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

Test rendered components, inspect compiler output, and fix styles that do
not apply.

[Comparisons](/docs/introduction/comparisons)

Compare authoring, composition, and runtime values with other styling
libraries.
