# Concepts & Principles

How styles, tokens, and compilation behave.







Guides such as [Styling](/docs/guides/styling) cover complete tasks. [Thinking in Zyzz](/docs/introduction/thinking-in-zyzz) applies these principles to component organization and everyday styling decisions.

## Principles

* **Agnostic:** Core data and types do not depend on a framework, host, or bundler.
* **Compiled:** Rules exist before rendering. Calls return props that apply those rules to elements.
* **Minimal:** Core imports include no bundled theme, reset, or provider.
* **Modular:** Explicit configuration and narrow adapters separate responsibilities.
* **Standard:** CSS properties, custom properties, selectors, and the cascade retain their meaning.
* **Typed:** Values retain constraints through definitions, imports, and applications.
* **Universal:** Shared authoring targets explicit web and native capabilities.

## Typed Styles

Definitions describe static rules. Calling a definition returns styling props and never creates CSS rules. Authoring calls require compilation, even for literal styles, and [Getting Started](/docs/introduction/getting-started) covers compiler setup for each build tool.

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

export function Card() {
  return (
    <section {...styles.card()}>
      <h2 {...styles.title()}>Account</h2>
    </section>
  )
}

namespace styles {
  export const card = style({ padding: '1rem' })

  export const title = style({ fontWeight: 600 })
}
```



Definitions sit at module scope, and the compiler reads literals, local constant records, and typed callbacks statically without evaluating arbitrary application code. Compiling `Card.tsx` emits each rule before rendering. With the default atomic output, every declaration becomes one class, and `styles.card()` returns `{ className: 'z-p-1rem' }`:

```css
.z-p-1rem {
  padding: 1rem;
}

.z-font-weight-600 {
  font-weight: 600;
}
```

Values are typed from CSS data. Editors complete the keywords that a property accepts:

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

style({ position: '' })
// completions: inherit, initial, revert-layer, revert, unset, absolute, fixed, relative, static, sticky
```

## Configuration

`defineConfig` binds authoring functions to explicit tokens and layers. `zyzz.config.ts` exports the bound helpers, and components import them by name. Integrations follow each binding to its config without a default export. The compiler reads static data without executing application code.

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

export const { style, vars } = defineConfig({
  layers: ['base', 'components'],
  vars: { spacing: { md: '1rem' } },
})
```

* **Layers:** Keys such as `@layer components` come from the config, and unknown names fail.
* **No variables:** Authoring stays token-free.
* **One variable set:** Inline tokens or a reusable `defineVars` value.
* **Several sets:** Named sets under `vars`, with a required `defaultVars`.

Named sets share the default's token paths and value domains. Imports outside the config receive no ambient tokens or layer types.

Named exports preserve the config's inferred contract. `vars` exposes CSS variable references rather than runtime setters, and compatible scopes change their inherited values.

### Property Mappings

Each category supplies specific properties. `color` supplies `color` and `backgroundColor`, and `spacing` supplies padding and gap. [Property mappings](/docs/guides/themes#customize-property-mappings) replace those defaults one category at a time.

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

export function Card() {
  return <section {...styles.card()}>Account</section>
}

namespace styles {
  export const card = style({ padding: 'md' })
}
```

A value outside the configured tokens fails at the declaration, and the ` !custom` suffix opts into a literal:

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

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

style({ padding: '12px' })
// error: Type '"12px"' is not assignable to type '"12px" & Expected<"md" | `${string} !custom`>'.
// Type 'string' is not assignable to type 'Expected<"md" | `${string} !custom`>'.
style({ padding: '12px !custom' })
```

## Themes & Tokens

A token gives a repeated design decision a name. Instead of each component choosing its own blue or padding, `accent` and `md` point to shared values. Changing those values changes every component that uses them.

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

export function Card() {
  return <section {...styles.card()}>Account</section>
}

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

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

export const { style, vars } = defineConfig({
  layers: ['base', 'components'],
  vars: {
    color: { accent: { dark: '#8cf', light: '#06c' } },
    spacing: { md: '1rem' },
  },
})
```



A variable set holds token data and the property domains it implies, without a name or color scheme wrapper.

* **Contracts:** Explicit references such as `vars.spacing.md` keep their defining identity and value domain. That reference compiles to `var(--z-spacing-md, 1rem)`, so expressions such as `calc()` read the value of the nearest scope.
* **Extensions:** `extendVars` changes existing paths while keeping the set compatible.
* **Names:** Token names infer only for properties whose domain they fit, so a color name is not a spacing value.
* **Schemes:** Color leaves accept one shared value or a complete light and dark pair.

Root helpers from `zyzz` use literal values, and `zyzz/default` supplies bundled tokens. [Themes & Tokens](/docs/guides/themes) covers definitions, mappings, and typography sets.

### Theme Scopes

Compatible variable sets share paths and value domains. Applying `vars({ set: 'alternate' })` to an ancestor changes the values that its subtree inherits. Components keep the same classes in every scope, and defaults supply values outside an explicit scope.



The named config defines a base set and extends its accent color. Both sets keep the same token paths.

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

export function Preview() {
  return (
    <section {...vars({ set: 'alternate' })}>
      <div {...styles.card()}>Outer card</div>
      <section {...vars({ set: 'base' })}>
        <div {...styles.card()}>Nested base card</div>
      </section>
    </section>
  )
}

namespace styles {
  export const card = style({ color: 'accent', padding: '1rem' })
}
```

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

const base = defineVars({
  color: { accent: { dark: '#8cf', light: '#0066cc' } },
})
const alternate = extendVars(base, {
  color: { accent: { dark: '#d8b4fe', light: '#9333ea' } },
})

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



Switch the set in the Rendered tab. The outer card changes color, while the nested base scope keeps its original accent.

* **Color pairs:** `{ dark, light }` compiles to `light-dark()`.
* **Color scheme:** `light` or `dark` selects a scheme explicitly, while `light dark` follows the browser preference.
* **Set selection:** Changes tokens independently of the color scheme. [Themes & Tokens](/docs/guides/themes) shows the complete config for selecting sets and restoring a saved set before the first paint.
* **Query thresholds:** Compile to literal conditions and do not change with the selected set.

## Composition and Overrides

`cx` combines generated styling props. Later generated conflicts win within matching conditions, subject to importance. Composition keeps owned variable bindings and recipe attributes attached.

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

export function SaveButton() {
  return <button {...cx(styles.button(), styles.compact())}>Save</button>
}

namespace styles {
  export const button = style({ padding: '1rem' })

  export const compact = style({ padding: '0.5rem' })
}
```



Swapping the arguments swaps the result. `cx(styles.compact(), styles.button())` applies `1rem` of padding, because `button` comes last.

Multiple JSX spreads replace fields rather than compose them. External classes follow the CSS cascade, and their order in a class string does not establish precedence. [Styling](/docs/guides/styling) explains the override rules.

## Variants

Root and config-bound recipes support static choices, defaults, ordered compounds, conditional selections, and dynamic payloads. A recipe styles one element and returns one props object. Multipart components use separate definitions with shared inputs, so there is no slots option.

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

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

namespace styles {
  export const button = variants({
    variants: {
      size: {
        md: { padding: '1rem' },
        sm: { padding: '0.5rem' },
      },
    },
  })
}
```



Axes, defaults, and compounds select precompiled alternatives, so choosing `sm` applies existing rules. A variant here is a component choice such as size, not a condition prefix such as `hover:`.

[Variants](/docs/guides/variants) covers defaults, compounds, conditional choices, and dynamic payloads.

## Conditions

Pseudo-classes, media queries, container queries, and feature queries keep their CSS meaning. Nested conditions combine with AND while preserving property and token inference. The browser evaluates emitted conditions without runtime viewport listeners from Zyzz.

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

export function Button() {
  return <button {...styles.button()}>Hover this button</button>
}

namespace styles {
  export const button = style({
    ':hover': {
      '@media (hover: hover)': { opacity: 0.8 },
    },
  })
}
```



Conditional rules keep their authored order in the stylesheet. A query written before a base declaration does not override it, because the base rule follows it.

Query aliases resolve from config metadata to literal conditions, so a scope change does not move a query threshold. Container queries select the nearest eligible container, and raw queries still require compiler validation.

## Relationships

`selectors` objects interpolate `style()` definitions without calling them. `&` selects the styled element, while combinators, pseudo-classes, attributes, and `:has()` keep ordinary CSS semantics. An empty `style()` supplies a stable identity for an ancestor or sibling.

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

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

namespace styles {
  export const card = style()

  export const label = style({
    selectors: {
      [`${card}:hover &`]: { color: 'blue' },
    },
  })
}
```



References keep their identity through local aliases, namespace members, named imports, re-exports, and packed libraries. The compiler checks selector grammar and interpolated identities. TypeScript checks the nested declaration values but cannot prove the DOM relationship.

* **Targets:** Every selector branch needs `&`. A selector such as `'body'` fails compilation because it has no `&` target, and document rules belong in `global` from `zyzz/web`.
* **Specificity:** Follows the authored selector, and `:where(...)` lowers it explicitly.
* **State:** Application state stays in ordinary data or ARIA attributes.
* **Runtime:** Relationships need no runtime selector parsing, DOM lookup, or CSS generation.

## Dynamic Values

> [!NOTE]
> Finite local scalar callback types are supported. Native callbacks also support scalar payloads. Arbitrary imported type definitions and dynamic fallback groups remain unsupported.

Callbacks bind per-instance values to precompiled custom properties, and their rule structure stays static. Other values that look dynamic are compiled references:

* **`vars.spacing.md`:** A typed reference for declarations and web expressions.
* **Query thresholds:** Compiled literals that scope changes do not affect.

Choosing between fixed values belongs in variants. Callbacks handle values computed for each instance.

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

export function Bar() {
  return <div {...styles.bar({ width: '50%' })} aria-hidden="true" />
}

namespace styles {
  export const bar = style((values: { width: `${number}%` }) => ({
    width: values.width,
  }))
}
```



Calls accept their declared inputs plus `className` and `style` overrides. Other component props stay on the element.

## Layers and Stylesheets

Layers establish an explicit order for styles from different sources. In this example, the base stylesheet supplies rounded corners and gray text. The later component layer changes the text to blue.

```tsx title="LayeredCard.tsx"
import { defineConfig } from 'zyzz'
import { global } from 'zyzz/web'

const { style } = defineConfig({
  layers: ['base', 'components'],
})

global({ '@layer base': { '.concept-card': { borderRadius: '8px' } } })

export function LayeredCard() {
  return (
    <section {...styles.card({ className: 'concept-card' })}>
      The component layer wins.
    </section>
  )
}

namespace styles {
  export const card = style({
    '@layer base': { color: 'gray', padding: '1rem' },
    '@layer components': { color: '#0066cc' },
  })
}
```



* **Collection:** Scans configured sources, including unimported modules, and excludes tests and generated output. Packed library contributions come from package metadata.
* **Delivery:** Globals are eager, including declarations beside lazy components. The initial stylesheet includes the shared layer prelude.
* **Helpers:** `global`, `fontFace`, and `keyframes` come from `zyzz/web`. Keyframes have separate reachability rules.
* **Ordering:** Layer constraints merge deterministically, and conflicts produce located errors. Authored order, unlayered rules, and the reversed order for important declarations are preserved.
* **Watching:** Edits and deletions replace or remove contributions.

[Global Styles](/docs/guides/global-styles), [Fonts & Typography](/docs/guides/typography), and [Keyframes](/docs/guides/keyframes) cover document rules, font loading, and motion.
