# Thinking in Zyzz

Keep styles beside components, compose them by name, and let TypeScript check the choices between them.

These conventions guide how components, shared styles, and configuration fit together. Each section names a preference and the alternative it replaces.

## Co-location over Separation

Component-specific styles belong beside the component, in a module-level `namespace styles` at the bottom of the file. Markup, behavior, and styling change together, without a separate stylesheet or style module for every component.

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

export function PasswordField() {
  return (
    <label {...styles.field()}>
      <span {...styles.label()}>Password</span>
      <input type="password" {...styles.input()} />
    </label>
  )
}

namespace styles {
  export const field = style({ display: 'grid', gap: '0.375rem' })

  export const input = style({
    border: '1px solid',
    borderColor: '#d4d4d4',
    borderRadius: '0.5rem',
    padding: '0.375rem 0.75rem',
  })

  export const label = style({ fontWeight: 500 })
}
```

A [shared style module](/docs/guides/styling) comes later, once a second component needs the same definition. Co-location is the starting point, and reuse supplies the reason to move a definition. Some repetition across components costs less than a shared definition that couples them.

## Composition over Inlining

Named definitions keep styling decisions out of markup. Names such as `button` and `focusRing` describe the element or treatment, while the declarations stay in the namespace. Definition and application remain separate within the same file.

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

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

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

  export const focusRing = style({
    selectors: {
      '&:focus-visible': {
        outline: '2px solid currentColor',
        outlineOffset: '2px',
      },
    },
  })
}
```

The focus ring composes with buttons, links, and other controls. `cx` preserves variable bindings and variant attributes. Later conflicting declarations win within matching conditions, subject to importance.

Two spreads on one element replace each other's `className` and `style`, so they cannot substitute for composition. [Styling](/docs/guides/styling) covers overrides and the inputs `cx` accepts.

## Feedback While Authoring

Types put feedback where styles are defined and applied. Property names, supported values, configured tokens, variant choices, and callback inputs reach the editor as completions and diagnostics. A renamed variant or removed token fails at its callers before a build runs.

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

namespace styles {
  export const stack = style((values: { gap: `${number}px` }) => ({
    display: 'flex',
    gap: values.gap,
  }))
}

styles.stack({ gap: '8px' })
styles.stack({ gap: 8 })
// error: Type 'number' is not assignable to type '`${number}px`'.
```

Type checking is static analysis, and it complements compilation. Selector grammar, extraction constraints, and target capabilities still need the compiler. Types cannot prove DOM structure or rendered appearance either. [Agents](/docs/introduction/agents) covers editor and agent setup.

## Standards over Shorthand

Styles use CSS property names in camelCase, such as `borderColor` and `outlineOffset`. Values keep CSS units and syntax. Conditions use CSS names too, such as `:focus-visible`, `[aria-invalid="true"]`, and `@media (min-width: 48rem)`, so there is no separate utility vocabulary to learn.

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

export function PasswordField(props: PasswordField.Props) {
  return (
    <label {...styles.field()}>
      <span {...styles.label()}>Password</span>
      <input aria-invalid={props.invalid} type="password" {...styles.input()} />
    </label>
  )
}

export declare namespace PasswordField {
  type Props = { invalid?: boolean | undefined }
}

namespace styles {
  export const field = style({ display: 'grid', gap: '0.375rem' })

  export const input = style({
    border: '1px solid',
    borderColor: '#d4d4d4',
    borderRadius: '0.5rem',
    padding: '0.375rem 0.75rem',
    selectors: {
      '&:focus-visible': { outline: '2px solid #06c' },
      '&[aria-invalid="true"]': { borderColor: '#c00' },
    },
  })

  export const label = style({
    fontWeight: 500,
    selectors: {
      [`${field}:focus-within &`]: { color: '#06c' },
    },
  })
}
```

Keyboard focus and validity are states the element already exposes, so selectors style them directly. The label reacts to focus inside the field from its own definition, with `&` as the target. Each element's rules stay in its own definition rather than in descendant selectors elsewhere.

CSS knowledge still applies to selectors, inheritance, specificity, and the cascade. Configured token names add a project's own vocabulary on top.

## Explicit Configuration

Importing `style` from `zyzz` gives token-free authoring. Importing it from a project's `zyzz.config.ts` gives the tokens and layers that config declares. The import identifies the contract, and another config does not silently change it.

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

export const { style, variants } = defineConfig({
  vars: {
    color: { border: '#d4d4d4', danger: '#c00', focus: '#06c' },
    radius: { md: '0.5rem' },
    spacing: { md: '0.75rem', sm: '0.375rem', xs: '0.25rem' },
  },
})
```

The field switches its import, and its literals become token names, so renaming or removing a token produces a type error at every definition that uses it:

```diff title="PasswordField.tsx"
-import { style } from 'zyzz'
+import { style } from './zyzz.config.js'
 
 // ...
 
 namespace styles {
-  export const field = style({ display: 'grid', gap: '0.375rem' })
+  export const field = style({ display: 'grid', gap: 'sm' })
 
   export const input = style({
     border: '1px solid',
-    borderColor: '#d4d4d4',
-    borderRadius: '0.5rem',
-    padding: '0.375rem 0.75rem',
+    borderColor: 'border',
+    borderRadius: 'md',
+    paddingBlock: 'sm',
+    paddingInline: 'md',
     selectors: {
-      '&:focus-visible': { outline: '2px solid #06c' },
-      '&[aria-invalid="true"]': { borderColor: '#c00' },
+      '&:focus-visible': { outline: '2px solid', outlineColor: 'focus' },
+      '&[aria-invalid="true"]': { borderColor: 'danger' },
     },
   })
 
   export const label = style({
     fontWeight: 500,
     selectors: {
-      [`${field}:focus-within &`]: { color: '#06c' },
+      [`${field}:focus-within &`]: { color: 'focus' },
     },
   })
 }
```

`fontWeight` keeps its literal because the config defines no font weights. Shared colors, spacing, and typography belong in a [theme](/docs/guides/themes). The opt-in `zyzz/default` import bundles Geist colors and typography, plus spacing, radius, shadow, and query scales.

## Variants over Concatenation

Component choices such as size or tone belong in `variants`. Callers select typed values such as `{ size: 'compact' }` instead of concatenating class names. The definition owns the choices, defaults, and compound rules, so call sites do not rebuild that logic.

```tsx title="PasswordField.tsx"
import type { Props } from 'zyzz'
import { style, variants } from './zyzz.config.js'

export function PasswordField(props: PasswordField.Props) {
  return (
    <label {...styles.field()}>
      <span {...styles.label()}>Password</span>
      <input
        aria-invalid={props.invalid}
        type="password"
        {...styles.input({ size: props.size })}
      />
    </label>
  )
}

export declare namespace PasswordField {
  type Props = Pick<Props.Variants<typeof styles.input>, 'size'> & {
    invalid?: boolean | undefined
  }
}

namespace styles {
  export const field = style({ display: 'grid', gap: 'sm' })

  export const input = variants({
    base: {
      border: '1px solid',
      borderColor: 'border',
      borderRadius: 'md',
      selectors: {
        '&:focus-visible': { outline: '2px solid', outlineColor: 'focus' },
        '&[aria-invalid="true"]': { borderColor: 'danger' },
      },
    },
    defaultVariants: { size: 'regular' },
    variants: {
      size: {
        compact: { paddingBlock: 'xs', paddingInline: 'sm' },
        regular: { paddingBlock: 'sm', paddingInline: 'md' },
      },
    },
  })

  export const label = style({
    fontWeight: 500,
    selectors: {
      [`${field}:focus-within &`]: { color: 'focus' },
    },
  })
}
```

Call sites see the choices that the definition declares. Editors and agents complete `compact` and `regular`, and any other value fails the type check:

```tsx title="SignIn.tsx"
import { PasswordField } from './PasswordField.js'

export function SignIn() {
  return <PasswordField size="" />
  // completions: regular, compact
}
```

Browser states such as focus belong in CSS conditions, and application state stays in props and ordinary data or ARIA attributes. [Variants](/docs/guides/variants) covers compound rules and choices selected by CSS conditions.

## Static Rules

Rules compile ahead of time. Runtime calls select existing styles or bind values to precompiled CSS variables. A strength meter accepts a different width for every instance without generating a CSS rule for each width.

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

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

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

namespace styles {
  export const fill = style((values: { width: `${number}%` }) => ({
    backgroundColor: 'focus',
    width: `${values.width} !custom`,
  }))

  export const track = style({
    backgroundColor: 'border',
    borderRadius: 'md',
    display: 'flex',
    height: 'xs',
    overflow: 'hidden',
  })
}
```

Spacing tokens constrain `width`, so the ` !custom` suffix marks the bound percentage as a literal CSS value. Finite visual choices fit variants, while per-instance values fit typed callbacks or variables. Rule structure stays statically analyzable.

[Styling](/docs/guides/styling) covers the supported callback forms, and [Getting Started](/docs/introduction/getting-started) covers compilation setup.

## Next Steps

[Styling](/docs/guides/styling)

Compose overrides, bind dynamic values, and share CSS variables.

[Variants](/docs/guides/variants)

Combine matching choices and select them with CSS conditions.

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

Define shared values, color schemes, and scoped variable sets.

[Concepts & Principles](/docs/concepts)

Follow definitions from authoring through compilation to applied props.
