# Conditions

Style browser states, viewport and container sizes, supported features, and related elements with standard CSS conditions.

## Overview

A condition makes declarations apply only while something is true, such as the pointer hovering an element, the viewport reaching a width, or an ancestor being open. Otherwise, the element keeps its base declarations.

In a definition, a condition is a key whose value holds the declarations to apply while it matches. Keys use CSS syntax: pseudo-classes such as `':hover'`, at-rules such as `'@media (min-width: 48rem)'`, and selectors such as `'&[aria-expanded="true"]'` grouped under `selectors`.

The compiler emits each condition as an ordinary CSS rule ahead of time, so the browser evaluates it without runtime listeners from Zyzz. TypeScript checks the declarations inside every condition, and the compiler checks selector grammar and query names.

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

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

namespace styles {
  export const card = style({
    containerType: 'inline-size',
    padding: '1rem',
    ':focus-within': { outline: '2px solid currentColor' },
    '@media (min-width: 48rem)': { padding: '1.5rem' },
  })

  export const title = style({
    fontSize: '1.25rem',
    '@container (min-width: 32rem)': { fontSize: '1.5rem' },
    selectors: {
      [`${card}[data-state="open"] &`]: { color: '#06c' },
    },
  })
}
```

## Walkthrough

The steps below build a disclosure trigger, from browser states to an icon that follows the trigger's state. Start with [Getting Started](/docs/introduction/getting-started) to connect compilation. The walkthrough imports the root `style`, so literal CSS values work without a config.

### Style Browser States

Pseudo-class keys such as `':hover'`, `':active'`, `':focus-visible'`, and `':disabled'` apply while the browser reports that state.

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

export function Disclosure(props: Disclosure.Props) {
  return (
    <button disabled={props.disabled} type="button" {...styles.trigger()}>
      Details
    </button>
  )
}

export declare namespace Disclosure {
  type Props = { disabled?: boolean | undefined }
}

namespace styles {
  export const trigger = style({
    backgroundColor: '#06c',
    color: 'white',
    padding: '0.5rem 1rem',
    ':hover': { backgroundColor: '#05a' },
    ':active': { backgroundColor: '#048' },
    ':focus-visible': { outline: '2px solid #06c', outlineOffset: '2px' },
    ':disabled': { backgroundColor: '#999', cursor: 'not-allowed' },
  })
}
```

Conditions keep their authored order, which decides the result when several states match at once. While pressing, `:active` follows `:hover` and wins. A disabled trigger stays gray under the pointer because `:disabled` comes last.

### Style Attribute States

String selectors live under `selectors` and include `&` for the styled element. Attribute selectors read state that the element already exposes, such as `aria-expanded`, `aria-invalid`, or `data-state`.

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

export function Disclosure(props: Disclosure.Props) {
  return (
    <button
      aria-expanded={props.open}
      disabled={props.disabled}
      type="button"
      {...styles.trigger()}
    >
      Details
    </button>
  )
}

export declare namespace Disclosure {
  type Props = { disabled?: boolean | undefined; open: boolean }
}

namespace styles {
  export const trigger = style({
    backgroundColor: '#06c',
    color: 'white',
    padding: '0.5rem 1rem',
    ':hover': { backgroundColor: '#05a' },
    ':active': { backgroundColor: '#048' },
    ':focus-visible': { outline: '2px solid #06c', outlineOffset: '2px' },
    // Authored before :disabled, so a disabled trigger stays gray while open
    selectors: {
      '&[aria-expanded="true"]': { backgroundColor: '#048' },
    },
    ':disabled': { backgroundColor: '#999', cursor: 'not-allowed' },
  })
}
```

Styling the attribute keeps the visual state and the accessible state in agreement. A separate prop or class for the same state could drift from what assistive technology reports.

### Style Related Elements

Relationship selectors interpolate another definition without calling it, and `&` stays the target. The icon reads the trigger's `aria-expanded` attribute from its own definition and rotates while the disclosure is open.

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

export function Disclosure(props: Disclosure.Props) {
  return (
    <button
      aria-expanded={props.open}
      disabled={props.disabled}
      type="button"
      {...styles.trigger()}
    >
      Details
      <span aria-hidden="true" {...styles.icon()}>
        ▾
      </span>
    </button>
  )
}

export declare namespace Disclosure {
  type Props = { disabled?: boolean | undefined; open: boolean }
}

namespace styles {
  export const trigger = style({
    backgroundColor: '#06c',
    color: 'white',
    display: 'inline-flex',
    gap: '0.5rem',
    padding: '0.5rem 1rem',
    ':hover': { backgroundColor: '#05a' },
    ':active': { backgroundColor: '#048' },
    ':focus-visible': { outline: '2px solid #06c', outlineOffset: '2px' },
    // Authored before :disabled, so a disabled trigger stays gray while open
    selectors: {
      '&[aria-expanded="true"]': { backgroundColor: '#048' },
    },
    ':disabled': { backgroundColor: '#999', cursor: 'not-allowed' },
  })

  export const icon = style({
    transition: 'rotate 150ms',
    selectors: {
      [`${trigger}[aria-expanded="true"] &`]: { rotate: '180deg' },
    },
  })
}
```

Any `style()` definition can mark the related element. The compiler checks interpolated identities and selector grammar, but cannot prove that the related element exists in the DOM.

## Recipes

### Respond to Size

Media queries measure the viewport. Container queries measure the nearest ancestor that declares a `containerType`. Both use CSS syntax as written.

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

export function Panel() {
  return (
    <div {...styles.container()}>
      <section {...styles.panel()}>
        <span>First</span>
        <span>Second</span>
      </section>
    </div>
  )
}

namespace styles {
  export const container = style({ containerType: 'inline-size' })

  export const panel = style({
    display: 'grid',
    gap: '0',
    padding: '0.5rem',
    '@container (min-width: 24rem)': { gap: '1rem' },
    '@media (min-width: 48rem)': { padding: '1rem' },
  })
}
```

### Name Queries

A config can name breakpoints for media queries, container widths for container queries, and container names. Aliases compile to literal conditions, so selecting a different variable set never moves a threshold.

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

export const { style } = defineConfig({
  vars: {
    breakpoint: { desktop: '64rem', phone: '30rem', tablet: '48rem' },
    container: { card: '24rem' },
    containerNames: ['sidebar'],
  },
})
```

Each alias form compiles to one query:

```ts title="layout.styles.ts"
import { style } from './zyzz.config.js'

export const layout = style({
  // @media (width >= 48rem)
  '@media tablet': { padding: '2rem' },
  // @media (width < 48rem)
  '@media <tablet': { padding: '1rem' },
  // @media (30rem <= width < 48rem)
  '@media phone..tablet': { gap: '1rem' },
  // @container (width >= 24rem)
  '@container card': { display: 'grid' },
  // @container sidebar (width >= 24rem)
  '@container sidebar card': { gap: '2rem' },
})
```

`tablet` and `>=tablet` are equivalent, and ranges include the lower bound but not the upper one. An unknown name fails the type check, and the compiler reports it at the key. Conditions with parentheses and media types such as `print` pass through unchanged.

### Mobile-First Layout

Base declarations style the smallest screens, and each breakpoint adds an override. At desktop widths both queries match, and the later `desktop` rule wins because rules keep their authored order.

```ts title="grid.styles.ts"
import { style } from './zyzz.config.js'

export const grid = style({
  display: 'grid',
  gap: '1rem',
  gridTemplateColumns: '1fr',
  '@media tablet': { gridTemplateColumns: 'repeat(2, 1fr)' },
  '@media desktop': { gridTemplateColumns: 'repeat(3, 1fr)' },
})
```

A range such as `'@media tablet..desktop'` applies only between two thresholds, which avoids relying on order for overlapping queries.

### Container-Aware Cards

A container query reacts to the space a component receives rather than the viewport. The same card can stack in a narrow sidebar and sit side by side in a wide column.

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

export function ProductCard() {
  return (
    <div {...styles.container()}>
      <article {...styles.card()}>
        <img alt="" src="/product.png" />
        <p>Product details</p>
      </article>
    </div>
  )
}

namespace styles {
  export const container = style({
    containerName: 'sidebar',
    containerType: 'inline-size',
  })

  export const card = style({
    display: 'grid',
    gap: '1rem',
    '@container sidebar card': { gridTemplateColumns: '8rem 1fr' },
  })
}
```

### Check Feature Support

`@supports` applies declarations only where the browser supports a feature. The declarations outside the query act as the fallback.

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

export const gallery = style({
  display: 'flex',
  flexWrap: 'wrap',
  gap: '1rem',
  '@supports (display: grid)': {
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fill, minmax(12rem, 1fr))',
  },
})
```

### Guard Hover Styles

Touch devices can keep a hover style after a tap. Nested conditions combine with AND, so wrapping `:hover` in a hover-capability query limits it to devices with a hovering pointer.

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

export const link = style({
  opacity: 1,
  '@media (hover: hover)': {
    ':hover': { opacity: 0.8 },
  },
})
```

The compiler emits the rules in the same order, with the pseudo-class nested inside the query. Class names depend on the module path:

```css
.z-bcI3yC-link-opacity-0 {
  opacity: 1;
}

@media (hover: hover) {
  .z-bcI3yC-link-opacity-1 {
    &:hover {
      opacity: 0.8;
    }
  }
}
```

Write base declarations before their conditions. Rules keep their authored order, so a query written before a base declaration does not override it.

### Mark Invalid Fields

`aria-invalid` reports a failed validation to assistive technology. The input styles that attribute, and the hint after it reacts through a sibling selector.

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

export function EmailField(props: EmailField.Props) {
  return (
    <div {...styles.field()}>
      <input
        aria-describedby="email-hint"
        aria-invalid={props.invalid}
        type="email"
        {...styles.input()}
      />
      <p id="email-hint" {...styles.hint()}>
        Enter a valid email address.
      </p>
    </div>
  )
}

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

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

  export const input = style({
    border: '1px solid #d4d4d4',
    selectors: {
      '&[aria-invalid="true"]': { borderColor: '#c00' },
    },
  })

  export const hint = style({
    color: '#666',
    selectors: {
      [`${input}[aria-invalid="true"] ~ &`]: { color: '#c00' },
    },
  })
}
```

A sibling selector only reaches elements that come later in the DOM, so the hint follows the input.

### Mark an Ancestor

An empty `style()` supplies an identity for an ancestor or sibling that needs no declarations of its own. Here, the label follows the card's hover state and its `data-state` attribute.

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

export function Card(props: Card.Props) {
  return (
    <section data-state={props.open ? 'open' : 'closed'} {...styles.card()}>
      <span {...styles.label()}>Details</span>
    </section>
  )
}

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

namespace styles {
  export const card = style()

  export const label = style({
    opacity: 0.5,
    selectors: {
      [`${card}:hover &`]: { opacity: 0.8 },
      [`${card}[data-state="open"] &`]: { opacity: 1 },
    },
  })
}
```

### Reveal Hidden Actions

Actions inside a card can stay hidden until the card is hovered or focused. Including `:focus-within` keeps the actions reachable with a keyboard.

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

export function Card() {
  return (
    <article {...styles.card()}>
      <h2>Invoice</h2>
      <button type="button" {...styles.actions()}>
        Download
      </button>
    </article>
  )
}

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

  export const actions = style({
    opacity: 0,
    selectors: {
      [`${card}:hover &`]: { opacity: 1 },
      [`${card}:focus-within &`]: { opacity: 1 },
    },
  })
}
```

### Follow Color Schemes

Zyzz has no built-in dark condition. Colors switch through `light-dark()`, which follows the `color-scheme` of the element. Properties other than colors use a `prefers-color-scheme` query.

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

export const panel = style({
  backgroundColor: 'light-dark(#fff, #111)',
  boxShadow: '0 1px 2px rgb(0 0 0 / 0.2)',
  color: 'light-dark(#111, #eee)',
  '@media (prefers-color-scheme: dark)': { boxShadow: 'none' },
})
```

The query follows the system setting even when a scope selects a scheme explicitly, so `light-dark()` suits most color changes. Config color pairs compile to `light-dark()`, and [Themes & Tokens](/docs/guides/themes) covers selecting a scheme.

### Respect Reduced Motion

`prefers-reduced-motion` reports a user preference for less motion. The transition still exists by default and turns off when the preference is set.

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

export const panel = style({
  transition: 'transform 200ms ease-out',
  '@media (prefers-reduced-motion: reduce)': { transition: 'none' },
})
```

### Style for Print

Media types pass through unchanged, so a `print` query can drop interactive controls from printed pages.

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

export const toolbar = style({
  display: 'flex',
  gap: '0.5rem',
  '@media print': { display: 'none' },
})
```

### Lower Selector Specificity

A selector's specificity follows its authored text. Wrapping part of it in `:where(...)` removes that part's weight, so later overrides of the same element win more easily.

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

export const tab = style({
  color: '#666',
  selectors: {
    '&:where([aria-selected="true"])': { color: '#111' },
  },
})
```

## More

[Styling](/docs/guides/styling)

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

[Variants](/docs/guides/variants)

Select component choices, including choices driven by CSS conditions.

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

Name breakpoints and containers, and make token values responsive.

[Concepts & Principles](/docs/concepts)

See how conditions and relationships compile into ordinary CSS.
