# Grid

Divides the available width into up to 12 equal tracks that respond to the grid's own width, not the viewport. Use it for card layouts and forms; wrap it in [Container](/components/container.md) for a centered width cap.

```tsx
// demo.tsx
import {Card, Grid, Stack} from '@earendil-works/elanor-react';
import {useState} from 'react';

import styles from './grid.module.css';

const sections = ['Overview', 'Usage', 'Activity', 'Members', 'Settings', 'Billing'];

export default function GridDemo() {
  const [width, setWidth] = useState(100);

  return (
    <Stack gap="large" align="stretch">
      <label>
        Grid width: {width}%{' '}
        <input
          type="range"
          min={20}
          max={100}
          value={width}
          onChange={(event) => setWidth(event.currentTarget.valueAsNumber)}
        />
      </label>
      <div style={{inlineSize: `${width}%`}}>
        <Grid.Root>
          {sections.map((section) => (
            <Grid.Item
              key={section}
              span={{base: 'full', xSmall: 6, small: 4}}
              render={<Card className={styles.cell} />}
            >
              {section}
            </Grid.Item>
          ))}
        </Grid.Root>
      </div>
    </Stack>
  );
}
```

```css
/* grid.module.css */
/* Shows the cell boundaries in these demos. */
.cell {
  border-radius: var(--semantic-dimension-radius-small);
  background: var(--semantic-color-background-fill-secondary);
}

/* Marks each subgrid row as one element in this demo. */
.row {
  padding-block: var(--semantic-dimension-space-padding-small);
  border-radius: var(--semantic-dimension-radius-small);
  background: var(--semantic-color-background-fill-secondary);
}
```

## Usage

```tsx
import {Card, Grid} from '@earendil-works/elanor-react';

<Grid.Root>
  <Grid.Item span={{base: 'full', small: 6, medium: 4}} render={<Card />}>
    Overview
  </Grid.Item>
  <Grid.Item>Full row</Grid.Item>
</Grid.Root>;
```

Items span a full row unless `span` says otherwise. `render` lets another component, such as Card, be the item without an extra wrapper.

## Breakpoints

`span` and `start` keys are container breakpoints. They query the nearest `Grid.Root`, including from inside a subgrid.

| Key      | Minimum grid width |
| -------- | ------------------ |
| `base`   | Always             |
| `xSmall` | 40rem              |
| `small`  | 48rem              |
| `medium` | 64rem              |
| `large`  | 80rem              |
| `xLarge` | 96rem              |

A capped Container leaves 76rem for content, so `large` and `xLarge` apply only when the cap is removed.

## Start

`start` places an item on a track, or with `end`, at the end of its row. Key it by breakpoint like `span`. Below, the filter takes a full row in narrow grids and ends the heading's row from `xSmall` on.

```tsx
// start.tsx
import {Dropdown, Grid, Heading} from '@earendil-works/elanor-react';

const ranges = [
  {value: 'week', label: 'Last 7 days'},
  {value: 'month', label: 'Last 30 days'},
  {value: 'quarter', label: 'Last 90 days'},
];

export default function GridStart() {
  return (
    <Grid.Root>
      <Grid.Item span={{base: 'full', xSmall: 6}}>
        <Heading level={3} size={4}>
          Usage
        </Heading>
      </Grid.Item>
      <Grid.Item span={{base: 'full', xSmall: 4}} start={{xSmall: 'end'}}>
        <Dropdown.Root items={ranges} defaultValue="month">
          <Dropdown.Trigger density="compact">
            <Dropdown.Prefix>Date range</Dropdown.Prefix>
            <Dropdown.Value />
            <Dropdown.Icon />
          </Dropdown.Trigger>
          <Dropdown.Portal>
            <Dropdown.Positioner>
              <Dropdown.Popup>
                <Dropdown.List>
                  {ranges.map((range) => (
                    <Dropdown.Item key={range.value} value={range.value}>
                      <Dropdown.ItemIndicator />
                      <Dropdown.ItemText>{range.label}</Dropdown.ItemText>
                    </Dropdown.Item>
                  ))}
                </Dropdown.List>
              </Dropdown.Popup>
            </Dropdown.Positioner>
          </Dropdown.Portal>
        </Dropdown.Root>
      </Grid.Item>
    </Grid.Root>
  );
}
```

An item never moves before the previous one; if it can't start after it, it begins a new row. Screen readers and keyboard focus follow source order.

## Columns

Spans larger than `columns` are clamped to the column count. Starts move back until the item fits.

```tsx
// columns.tsx
import {Card, Grid, Stack, type GridColumns} from '@earendil-works/elanor-react';
import {useState} from 'react';

import styles from './grid.module.css';

export default function GridColumnsExample() {
  const [columns, setColumns] = useState<GridColumns>(4);

  return (
    <Stack gap="large" align="stretch">
      <label>
        Columns{' '}
        <select
          value={columns}
          onChange={(event) => setColumns(Number(event.currentTarget.value) as GridColumns)}
        >
          {Array.from({length: 12}, (_, index) => (
            <option key={index}>{index + 1}</option>
          ))}
        </select>
      </label>
      <Grid.Root columns={columns}>
        {Array.from({length: columns}, (_, index) => (
          <Grid.Item key={index} span={1} render={<Card className={styles.cell} />}>
            {index + 1}
          </Grid.Item>
        ))}
        <Grid.Item render={<Card className={styles.cell} />}>No span: a full row</Grid.Item>
        <Grid.Item span={12} render={<Card className={styles.cell} />}>
          span=12: clamped to the column count
        </Grid.Item>
      </Grid.Root>
    </Stack>
  );
}
```

```css
/* grid.module.css */
/* Shows the cell boundaries in these demos. */
.cell {
  border-radius: var(--semantic-dimension-radius-small);
  background: var(--semantic-color-background-fill-secondary);
}

/* Marks each subgrid row as one element in this demo. */
.row {
  padding-block: var(--semantic-dimension-space-padding-small);
  border-radius: var(--semantic-dimension-radius-small);
  background: var(--semantic-color-background-fill-secondary);
}
```

## Subgrid

Use `Grid.Subgrid` when one element, such as a list row or a card, must place its children on the parent's columns. Below, each project row is one element with its own background, and its cells line up with the header. Subgrid shares columns only; rows stay independent.

```tsx
// subgrid.tsx
import {Grid} from '@earendil-works/elanor-react';

import styles from './grid.module.css';

const projects = [
  {name: 'Website redesign', status: 'In review', owner: 'Alex Morgan'},
  {name: 'Billing migration', status: 'Draft', owner: 'Sam Lee'},
];

export default function GridSubgridExample() {
  return (
    <Grid.Root columns={4}>
      <Grid.Item span={2}>Project</Grid.Item>
      <Grid.Item span={1}>Status</Grid.Item>
      <Grid.Item span={1}>Owner</Grid.Item>
      {projects.map((project) => (
        // Each row is one element with its own background; its cells still use the root's columns.
        <Grid.Subgrid key={project.name} className={styles.row}>
          <Grid.Item span={2}>{project.name}</Grid.Item>
          <Grid.Item span={1}>{project.status}</Grid.Item>
          <Grid.Item span={1}>{project.owner}</Grid.Item>
        </Grid.Subgrid>
      ))}
    </Grid.Root>
  );
}
```

```css
/* grid.module.css */
/* Shows the cell boundaries in these demos. */
.cell {
  border-radius: var(--semantic-dimension-radius-small);
  background: var(--semantic-color-background-fill-secondary);
}

/* Marks each subgrid row as one element in this demo. */
.row {
  padding-block: var(--semantic-dimension-space-padding-small);
  border-radius: var(--semantic-dimension-radius-small);
  background: var(--semantic-color-background-fill-secondary);
}
```

## Nested grids

A `Grid.Root` inside an item starts a new grid: it has its own column count and measures its own width.

```tsx
// nested.tsx
import {Card, Grid} from '@earendil-works/elanor-react';

import styles from './grid.module.css';

export default function GridNested() {
  return (
    <Grid.Root>
      {([4, 8] as const).map((span) => (
        <Grid.Item key={span} span={{base: 'full', small: span}}>
          <Grid.Root columns={2} gap="small">
            <Grid.Item render={<Card className={styles.cell} />}>Nested in {span} tracks</Grid.Item>
            <Grid.Item span={{base: 'full', xSmall: 1}} render={<Card className={styles.cell} />}>
              First
            </Grid.Item>
            <Grid.Item span={{base: 'full', xSmall: 1}} render={<Card className={styles.cell} />}>
              Second
            </Grid.Item>
          </Grid.Root>
        </Grid.Item>
      ))}
    </Grid.Root>
  );
}
```

```css
/* grid.module.css */
/* Shows the cell boundaries in these demos. */
.cell {
  border-radius: var(--semantic-dimension-radius-small);
  background: var(--semantic-color-background-fill-secondary);
}

/* Marks each subgrid row as one element in this demo. */
.row {
  padding-block: var(--semantic-dimension-space-padding-small);
  border-radius: var(--semantic-dimension-radius-small);
  background: var(--semantic-color-background-fill-secondary);
}
```

## Placement rules

- Items and subgrids must be direct DOM children of their grid. Fragments are fine; wrapper elements are not.
- Other children use native CSS Grid placement, without the full-row default or clamping.
- Clamping reads `columns`, `span`, and `start`. If you set `grid-template-columns` or `grid-column` in CSS, handle all placement in CSS.
- Grid keeps source order, including with `start`.

## Changing breakpoints

Container queries can't use CSS variables, so `src/grid/grid.module.css` writes thresholds as values annotated with their tokens:

```css
@container elanor-grid (48rem /* token: semantic.dimension.breakpoint.small */ <= width) {
  /* Responsive rules */
}
```

After changing breakpoint tokens, update the annotated values in the Grid and Layout CSS:

```sh
pnpm --filter @earendil-works/elanor-react generate:css
```

`check` and `build` fail on stale values.

## API

### `Grid.Root`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns` | `1 \| … \| 12` | `12` | Number of equal tracks. |
| `gap` | `'xxSmall' \| 'xSmall' \| 'small' \| 'medium' \| 'large' \| 'xLarge'` | `'medium'` | Space between tracks. |

Renders `<div>`. Also accepts its HTML attributes, `ref`, and `render`.

### `Grid.Item`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `span` | `1 \| … \| 12 \| 'full' \| Partial<Record<GridBreakpoint, GridSpan>>` | `'full'` | Tracks to span, or `full` for a full row. Key by container breakpoint, such as `{base: 'full', small: 6}`; each value applies until the next key. |
| `start` | `1 \| … \| 12 \| 'end' \| 'auto' \| Partial<Record<GridBreakpoint, GridStart>>` | `'auto'` | Track to start on, or `end` for the row's end. Keyed like `span`; full rows ignore it. |

Renders `<div>`. Also accepts its HTML attributes, `ref`, and `render`.

### `Grid.Subgrid`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `span` | `1 \| … \| 12 \| 'full' \| Partial<Record<GridBreakpoint, GridSpan>>` | `'full'` | Tracks to span, or `full` for a full row. Key by container breakpoint, such as `{base: 'full', small: 6}`; each value applies until the next key. |
| `start` | `1 \| … \| 12 \| 'end' \| 'auto' \| Partial<Record<GridBreakpoint, GridStart>>` | `'auto'` | Track to start on, or `end` for the row's end. Keyed like `span`; full rows ignore it. |

Renders `<div>`. Also accepts its HTML attributes, `ref`, and `render`.
