# Layout

Spacing, radius, shadow, width, and breakpoint tokens, and the primitives that apply them.

## Overview

Tempo layouts sit on a quarter-rem spacing scale, with rounded corners and flat cards on a tinted page. Layout primitives such as `Stack` and `Grid` apply the tokens for common arrangements, and `style` applies them everywhere else.

This page covers the tokens and primitives. For page frames, sidebars, and how layouts change across widths, see [Responsive Layout](/docs/patterns/responsive-layout).

## Spacing

Spacing tokens count quarter rems, so `'6'` is 1.5rem, or 24px at the default root size. Author them without a unit, such as `padding: '6'` or `gap: '3'`.

```tsx
import { style, variants } from '@tempoxyz/ds'

export const fill = true
const scale = [
  ['1', '0.25rem', '4px'],
  ['2', '0.5rem', '8px'],
  ['3', '0.75rem', '12px'],
  ['4', '1rem', '16px'],
  ['5', '1.25rem', '20px'],
  ['6', '1.5rem', '24px'],
  ['8', '2rem', '32px'],
  ['10', '2.5rem', '40px'],
  ['12', '3rem', '48px'],
  ['16', '4rem', '64px'],
  ['24', '6rem', '96px'],
] as const

export function Demo() {
  return (
    <div {...styles.grid()}>
      {scale.map(([token, rem, px]) => (
        <div key={token} {...styles.row()}>
          <code {...styles.token()}>'{token}'</code>
          <span {...styles.value()}>
            {rem} · {px}
          </span>
          <span {...styles.bar({ size: token })} />
        </div>
      ))}
    </div>
  )
}

namespace styles {
  export const bar = variants({
    base: { backgroundColor: 'foreground', borderRadius: 'xs', height: '3' },
    variants: {
      size: {
        '1': { width: '1' },
        '10': { width: '10' },
        '12': { width: '12' },
        '16': { width: '16' },
        '2': { width: '2' },
        '24': { width: '24' },
        '3': { width: '3' },
        '4': { width: '4' },
        '5': { width: '5' },
        '6': { width: '6' },
        '8': { width: '8' },
      },
    },
  })

  export const grid = style({ display: 'grid', gap: '2', maxWidth: '28rem !custom' })

  export const row = style({
    alignItems: 'center',
    columnGap: '4',
    display: 'grid',
    gridTemplateColumns: '3rem 7.5rem minmax(0, 1fr) !custom',
  })

  export const token = style({ color: 'foreground', typography: 'label.13.mono' })

  export const value = style({
    color: 'gray.900',
    fontVariantNumeric: 'tabular-nums !custom',
    typography: 'copy.13',
  })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Foundations/Spacing.tsx)

The scale runs from `'0'` to `'12'` in single steps, then `'14'` and `'16'`. From there it rises by 4 to `'64'`, then `'72'`, `'80'`, and `'96'`. `'px'` is a single pixel.

There are no half steps such as `'1.5'`. Write a literal such as `'6px !custom'` when a component needs one.

| Token  | Size | Use for                                                                  |
| ------ | ---- | ------------------------------------------------------------------------ |
| `'1'`  | 4px  | A label and its value, or a popover title and its description            |
| `'2'`  | 8px  | Icon to label inside a button, and the default `Inline` gap              |
| `'3'`  | 12px | Buttons in a row, and the padding of a `small` `Surface`                 |
| `'4'`  | 16px | The default `Stack` gap, `medium` `Surface` padding, and the toast inset |
| `'6'`  | 24px | The default `Grid` gap, `large` `Surface` padding, and `Popover` padding |
| `'8'`  | 32px | `Dialog` padding, and the `Container` gutter from 768px                  |
| `'12'` | 48px | Space between page sections                                              |

Because spacing uses rem, it grows when someone raises their browser's default font size. Type sets use px line heights, so check layouts at 200% zoom as well as with a larger default font.

## Radius

Corners get rounder as elements get larger. Buttons and filled fields share a radius at each size, and containers step down from `4xl`.

```tsx
import { style, variants } from '@tempoxyz/ds'

export const fill = true
const scale = [
  ['xs', '2px'],
  ['sm', '4px'],
  ['md', '6px'],
  ['lg', '8px'],
  ['xl', '12px'],
  ['2xl', '16px'],
  ['3xl', '24px'],
  ['4xl', '32px'],
  ['full', 'Pill'],
] as const

export function Demo() {
  return (
    <div {...styles.grid()}>
      {scale.map(([token, value]) => (
        <figure key={token} {...styles.item()}>
          <span {...styles.shape({ radius: token })} />
          <figcaption {...styles.caption()}>
            <code {...styles.token()}>{token}</code> {value}
          </figcaption>
        </figure>
      ))}
    </div>
  )
}

namespace styles {
  export const caption = style({ color: 'gray.900', typography: 'copy.13' })

  export const grid = style({
    display: 'grid',
    gap: '4',
    gridTemplateColumns: 'repeat(auto-fill, minmax(5rem, 1fr)) !custom',
    maxWidth: '36rem !custom',
  })

  export const item = style({ display: 'grid', gap: '2', justifyItems: 'start', margin: '0' })

  export const shape = variants({
    base: {
      backgroundColor: 'grayAlpha.100',
      borderColor: 'grayAlpha.300',
      borderStyle: 'solid',
      borderWidth: '1px !custom',
      boxSizing: 'border-box',
      height: '16',
      width: '16',
    },
    variants: {
      radius: {
        '2xl': { borderRadius: '2xl' },
        '3xl': { borderRadius: '3xl' },
        '4xl': { borderRadius: '4xl' },
        full: { borderRadius: 'full' },
        lg: { borderRadius: 'lg' },
        md: { borderRadius: 'md' },
        sm: { borderRadius: 'sm' },
        xl: { borderRadius: 'xl' },
        xs: { borderRadius: 'xs' },
      },
    },
  })

  export const token = style({ color: 'foreground', typography: 'label.13.mono' })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Foundations/Radius.tsx)

| Token  | Size | Components use it for                                                 |
| ------ | ---- | --------------------------------------------------------------------- |
| `xs`   | 2px  | Focus outlines around links in `Surface.Links`                        |
| `sm`   | 4px  | Small checkboxes, and actions inside small filled fields              |
| `md`   | 6px  | Checkboxes, `Kbd`, and actions inside filled fields                   |
| `lg`   | 8px  | Large checkboxes, small buttons and filled fields, and small callouts |
| `xl`   | 12px | Medium buttons, filled fields, and callouts                           |
| `2xl`  | 16px | Large buttons, filled fields, and callouts, and `small` `Surface`     |
| `3xl`  | 24px | `medium` `Surface`, `Popover`, and `Toast`                            |
| `4xl`  | 32px | `large` `Surface`, the default, and `Dialog`                          |
| `full` | Pill | Outlined and ghost fields, pill tags, and `Switch`                    |

## Shadows

Shadows mark layers that float above the page. Cards stay flat and rely on `background.surface` against `background.primary`.

```tsx
import { style, variants } from '@tempoxyz/ds'

export const fill = true
const shadows = [
  ['raised', 'Primary buttons, thumbs, Kbd'],
  ['popover', 'Popovers, menus, toasts'],
  ['sm', 'Zyzz default'],
  ['md', 'Zyzz default'],
  ['lg', 'Zyzz default'],
  ['xl', 'Zyzz default'],
] as const

export function Demo() {
  return (
    <div {...styles.grid()}>
      {shadows.map(([token, use]) => (
        <figure key={token} {...styles.card({ shadow: token })}>
          <code {...styles.token()}>{token}</code>
          <figcaption {...styles.use()}>{use}</figcaption>
        </figure>
      ))}
    </div>
  )
}

namespace styles {
  export const card = variants({
    base: {
      backgroundColor: 'background.surface',
      borderRadius: '2xl',
      display: 'grid',
      gap: '1',
      margin: '0',
      padding: '4',
    },
    variants: {
      shadow: {
        lg: { boxShadow: 'lg' },
        md: { boxShadow: 'md' },
        popover: { boxShadow: 'popover' },
        raised: { boxShadow: 'raised' },
        sm: { boxShadow: 'sm' },
        xl: { boxShadow: 'xl' },
      },
    },
  })

  export const grid = style({
    backgroundColor: 'background.primary',
    borderRadius: '3xl',
    display: 'grid',
    gap: '6',
    gridTemplateColumns: 'repeat(auto-fill, minmax(9rem, 1fr)) !custom',
    padding: '6',
  })

  export const token = style({ color: 'foreground', typography: 'label.13.mono' })

  export const use = style({ color: 'gray.900', typography: 'copy.13' })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Foundations/Shadows.tsx)

| Token                   | Source | Use for                                                                                                                                                |
| ----------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `raised`                | Web    | Controls that sit on a track or page, such as the primary `Button`, `Switch` thumb, `Tabs` and `ChoiceGroup` indicators, `AppShell.Trigger`, and `Kbd` |
| `popover`               | Web    | Popups anchored to a trigger: `Popover`, `Menu`, `Select` and `Combobox` lists, the `DatePicker` popup, and `Toast`                                    |
| `2xs` to `2xl`, `inner` | Zyzz   | General shadows. No component uses them, so prefer `raised` or `popover`                                                                               |

`popover` includes a 1px ring that switches from 6% black to 8% white in dark mode, so popups keep an edge on dark surfaces where a shadow alone disappears. `Dialog` draws its own deeper shadow above its backdrop.

## Widths

`Container` centers content at a maximum width and adds side gutters of 1.5rem, growing to 2rem from 768px. Pick its `size` by the content, not by the screen.

| `size`   | Max width     | Use for                                                  |
| -------- | ------------- | -------------------------------------------------------- |
| `narrow` | 48rem (`3xl`) | Forms, settings, and single flows such as Send payment   |
| `medium` | 64rem (`5xl`) | Detail pages, such as a transaction or an API key        |
| `wide`   | 80rem (`7xl`) | The default. Dashboards, activity tables, and card grids |
| `full`   | None          | Full-bleed charts and canvases                           |

The `container` tokens run from `3xs` (16rem) to `7xl` (80rem) and size elements directly, such as `maxWidth: 'xl'` for a 36rem panel. `Dialog` caps at 28rem and the toast stack at 24rem.

## Breakpoints

Zyzz defines five breakpoint tokens.

| Token | Width          |
| ----- | -------------- |
| `sm`  | 40rem (640px)  |
| `md`  | 48rem (768px)  |
| `lg`  | 64rem (1024px) |
| `xl`  | 80rem (1280px) |
| `2xl` | 96rem (1536px) |

Write them as query aliases inside a style, and Zyzz compiles each alias to a width range.

| Alias                            | Compiles to                       |
| -------------------------------- | --------------------------------- |
| `'@media md'` or `'@media >=md'` | `@media (width >= 48rem)`         |
| `'@media <md'`                   | `@media (width < 48rem)`          |
| `'@media sm..lg'`                | `@media (40rem <= width < 64rem)` |

```tsx
import { style } from '@tempoxyz/ds'

namespace styles {
  export const metrics = style({
    display: 'grid',
    gap: '4',
    '@media >=md': { gridTemplateColumns: 'repeat(3, minmax(0, 1fr)) !custom' },
  })
}
```

Components switch layout at three widths. Match them when your layout changes next to a component, so both change together.

* **640px.** At this width and below, `Dialog` becomes a bottom sheet. From it, toasts move from the bottom center to the end corner, and `AppShell` widens its side padding from 1rem to 2rem.
* **768px.** `Container` widens its gutter from 1.5rem to 2rem.
* **1024px.** Below it, the `AppShell` sidebar moves into a drawer behind `AppShell.Trigger`.

## Layout Primitives

Each primitive renders a `div` with no semantics. Pass `render` to use a meaningful element, such as `<Stack render={<ul />}>` for a list or `<Container render={<main />}>` for the main landmark.

| Primitive   | Use for                                                        | Props                                                          |
| ----------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
| `Stack`     | A vertical run of siblings, such as fields in a form           | `gap` (default `'4'`), `align`, `justify`                      |
| `Inline`    | A horizontal run, such as actions, tags, or a label and value  | `gap` (default `'2'`), `align`, `justify`, `wrap`              |
| `Grid`      | Collections of similar items, such as balances or metric tiles | `fit`, `columns`, `gap` (default `'6'`), `rowGap`, `columnGap` |
| `Container` | Page width and side gutters                                    | `size`, `gutter`                                               |
| `Inset`     | Padding without a visible container                            | `space`, `inline`, `block`                                     |
| `Surface`   | A visible card that groups related content                     | `size`, plus `Surface.Header`, `Title`, `Description`          |

Primitive gaps and padding accept `'0'`, `'1'`, `'2'`, `'3'`, `'4'`, `'6'`, `'8'`, `'12'`, and `'16'`. Use `style` with any spacing token when you need another step.

## Guidelines

### Space Siblings With Gap

Set spacing on the parent with `gap`, through a primitive or a flex or grid `style`. Margins on children collapse unpredictably, double up when items reorder, and leave stray space after the last item.

### Group Related Content by Proximity

Put less space inside a group than between groups. A label sits `'1'` from its value, while separate label and value pairs sit `'6'` apart, so the eye pairs them without dividers.

**Do:** Keep each label close to its value and leave more space between pairs.

**Don't:** Space every line equally, which makes labels and values hard to pair.

### Keep Nested Corners Concentric

An inner corner should equal the outer radius minus the padding between them. `Surface` pairs each radius with matching padding, so a `medium` surface (24px corners, 16px padding) takes 8px (`lg`) corners inside.

**Do:** Subtract the padding from the outer radius for nested corners.

**Don't:** Repeat the outer radius on nested content.

### Keep Cards Flat

Separate a `Surface` from the page with `background.surface` on `background.primary`, not a shadow. Save `popover` for content that floats over other content, so elevation keeps one meaning.

### Use Grid Fit for Collections

Prefer `fit` over a fixed `columns` count for cards and metric tiles. `fit="medium"` places as many 14rem tracks as the width allows and collapses to one column on a phone without media queries.

### Keep Source Order Visual Order

Don't reorder content with `order`, `flex-direction: row-reverse`, or grid placement. Keyboard focus and screen readers follow the DOM, so a visual order that differs makes focus jump.

## Accessibility

* Keep DOM order equal to visual order, so focus and reading order follow the layout.
* Reflow at 320 CSS pixels without horizontal scrolling. `Grid` with `fit`, `Inline` with `wrap`, and `Container` gutters do this by default.
* Give regions meaning through `render`, such as `main`, `nav`, `section`, or `ul`. Primitives add no roles of their own.
* Make targets at least 24 by 24px, or space smaller ones so a 24px circle centered on each doesn't overlap another.

## Related

* [Responsive Layout](/docs/patterns/responsive-layout) for page frames and width changes
* [Stack](/docs/components/stack), [Inline](/docs/components/inline), and [Grid](/docs/components/grid) for arranging content
* [Container](/docs/components/container) and [Inset](/docs/components/inset) for width and padding
* [Surface](/docs/components/surface) for cards
* [App Shell](/docs/components/app-shell) for the application frame
