# Grid

A 24-column grid layout system. Pair it with `oas-grid-item` to divide column widths, with support for gap, offset and a custom total column count; setting `columns` switches to an auto equal-width layout (simple-grid).

## Basic grid

<DemoBlock title="Three equal columns">
  <oas-grid gap="12px" style="width: 100%">
    <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

## Unequal columns

<DemoBlock title="Unequal columns">
  <oas-grid gap="12px" style="width: 100%">
    <oas-grid-item span="6"><div class="demo-grid-box">span 6</div></oas-grid-item>
    <oas-grid-item span="12"><div class="demo-grid-box">span 12</div></oas-grid-item>
    <oas-grid-item span="6"><div class="demo-grid-box">span 6</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

## Offset

<DemoBlock title="Offset">
  <oas-grid gap="12px" style="width: 100%">
    <oas-grid-item span="12"><div class="demo-grid-box">span 12</div></oas-grid-item>
    <oas-grid-item span="8" offset="4"><div class="demo-grid-box">span 8 offset 4</div></oas-grid-item>
    <oas-grid-item span="4" offset="6"><div class="demo-grid-box">span 4 offset 6</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

## Custom total columns

<DemoBlock title="Custom cols">
  <oas-grid cols="12" gap="12px" style="width: 100%">
    <oas-grid-item span="6"><div class="demo-grid-box">span 6 / 12 cols</div></oas-grid-item>
    <oas-grid-item span="6"><div class="demo-grid-box">span 6 / 12 cols</div></oas-grid-item>
    <oas-grid-item span="4"><div class="demo-grid-box">span 4</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

## Auto columns (simple-grid)

Setting `columns` divides the width equally with `repeat(n, 1fr)`; child `span` is ignored (span only takes effect without `columns`), and it coexists with the 24-column grid without conflict.

<DemoBlock title="columns auto equal-width">
  <oas-grid columns="3" gap="12px" style="width: 100%">
    <div class="demo-grid-box">1</div>
    <div class="demo-grid-box">2</div>
    <div class="demo-grid-box">3</div>
    <div class="demo-grid-box">4</div>
    <div class="demo-grid-box">5</div>
    <div class="demo-grid-box">6</div>
  </oas-grid>
</DemoBlock>

<DemoBlock title="columns ignores span">
  <oas-grid columns="4" gap="12px" style="width: 100%">
    <oas-grid-item span="8"><div class="demo-grid-box">span 8 ignored</div></oas-grid-item>
    <oas-grid-item span="24"><div class="demo-grid-box">span 24 ignored</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">span 8 ignored</div></oas-grid-item>
    <oas-grid-item span="24"><div class="demo-grid-box">span 24 ignored</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

## Responsive column counts

`columns` accepts breakpoint shorthand: a base value plus space-separated `breakpoint:value` pairs (same protocol as `span`: `sm`=640 / `md`=768 / `lg`=1024 / `xl`=1280, mobile-first). Children stay auto equal-width and ignore `span`. Resize the window to see the count adapt.

<DemoBlock title="columns breakpoints: 1 → sm:2 → md:3">
  <oas-grid columns="1 sm:2 md:3" gap="12px" style="width: 100%">
    <div class="demo-grid-box">1</div>
    <div class="demo-grid-box">2</div>
    <div class="demo-grid-box">3</div>
    <div class="demo-grid-box">4</div>
    <div class="demo-grid-box">5</div>
    <div class="demo-grid-box">6</div>
  </oas-grid>
  <p class="demo-grid-note">&lt;640px 1 column per row; ≥640px 2; ≥768px 3.</p>
</DemoBlock>

## Min child width (auto-fit grid)

`min-child-width` sets the minimum width of each child (e.g. `180px`; bare numbers are treated as px). The grid derives its column count automatically from the available width via `auto-fit + minmax` — children reflow without any breakpoints. When combined with `columns`, `columns` wins (`min-child-width` is ignored); in this mode child `span` / `offset` are also disabled (auto placement).

<DemoBlock title="min-child-width auto-fit grid">
  <oas-grid min-child-width="180px" gap="12px" style="width: 100%">
    <div class="demo-grid-box">1</div>
    <div class="demo-grid-box">2</div>
    <div class="demo-grid-box">3</div>
    <div class="demo-grid-box">4</div>
    <div class="demo-grid-box">5</div>
    <div class="demo-grid-box">6</div>
    <div class="demo-grid-box">7</div>
    <div class="demo-grid-box">8</div>
  </oas-grid>
  <p class="demo-grid-note">Narrow or widen the container to see the column count change automatically — no breakpoints needed.</p>
</DemoBlock>

## Large gap

<DemoBlock title="Gap">
  <oas-grid gap="24px" style="width: 100%">
    <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

## Separated row/column gutters

`gap` supports a two-value syntax: space- or comma-separated `row column` (`8 16` / `8,16`) controls row-gap and column-gap independently; a single value still applies to both axes (zero regression). The `row-gap` / `column-gap` attributes can be set individually and override the matching axis of `gap`.

<DemoBlock title="gap two values: row 8 / column 24">
  <oas-grid gap="8 24" style="width: 100%">
    <oas-grid-item span="8"><div class="demo-grid-box">row 8 / col 24</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">row 8 / col 24</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">row 8 / col 24</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">row 8 / col 24</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">row 8 / col 24</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">row 8 / col 24</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

<DemoBlock title="gap comma pair + row-gap/column-gap attributes">
  <oas-grid gap="8" column-gap="32" style="width: 100%">
    <oas-grid-item span="12"><div class="demo-grid-box">column-gap="32"</div></oas-grid-item>
    <oas-grid-item span="12"><div class="demo-grid-box">column-gap="32"</div></oas-grid-item>
  </oas-grid>
  <oas-grid gap="4,32" style="width: 100%">
    <oas-grid-item span="8"><div class="demo-grid-box">gap="4,32"</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">gap="4,32"</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">gap="4,32"</div></oas-grid-item>
  </oas-grid>
  <oas-grid gap="8" row-gap="24" style="width: 100%; margin-top: 16px">
    <oas-grid-item span="8"><div class="demo-grid-box">gap 8 + row-gap 24</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">gap 8 + row-gap 24</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">gap 8 + row-gap 24</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">row 24 / col 8</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">row 24 / col 8</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">row 24 / col 8</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

## Responsive breakpoints

`span` / `offset` accept breakpoint shorthand: a base value plus space-separated `breakpoint:value` pairs (breakpoints `sm`=640 / `md`=768 / `lg`=1024 / `xl`=1280, mobile-first). Resize the window to see the columns adapt.

<DemoBlock title="span breakpoints: 24 → md:12 → lg:8">
  <oas-grid gap="12px" style="width: 100%">
    <oas-grid-item span="24 md:12 lg:8"><div class="demo-grid-box">span 24 / md 12 / lg 8</div></oas-grid-item>
    <oas-grid-item span="24 md:12 lg:8"><div class="demo-grid-box">span 24 / md 12 / lg 8</div></oas-grid-item>
    <oas-grid-item span="24 md:12 lg:8"><div class="demo-grid-box">span 24 / md 12 / lg 8</div></oas-grid-item>
  </oas-grid>
  <p class="demo-grid-note">&lt;640px each item fills a row; ≥768px two per row; ≥1024px three equal columns.</p>
</DemoBlock>

<DemoBlock title="offset breakpoints: 0 → md:4">
  <oas-grid gap="12px" style="width: 100%">
    <oas-grid-item span="24 md:16"><div class="demo-grid-box">span 24 / md 16</div></oas-grid-item>
    <oas-grid-item span="24 md:4" offset="0 md:4"><div class="demo-grid-box">span 4 offset 4 (md+)</div></oas-grid-item>
  </oas-grid>
  <p class="demo-grid-note">From ≥768px the right item starts at column 5 and spans 4 columns (offset 4 / span 4).</p>
</DemoBlock>

## Auto-width columns

`span="auto"` sizes an item to its natural content width (no column span); it can be combined with `offset`.

<DemoBlock title="span=auto columns">
  <oas-grid gap="12px" style="width: 100%">
    <oas-grid-item span="auto"><div class="demo-grid-box">auto width</div></oas-grid-item>
    <oas-grid-item span="auto" offset="4"><div class="demo-grid-box">auto offset 4</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
    <oas-grid-item span="4"><div class="demo-grid-box">span 4</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

## Container alignment

`justify` controls the inline axis (justify-items), `align` the block axis (align-items); valid values are applied directly, invalid values fall back to `stretch` with a dev warning.

<DemoBlock title="justify (inline axis)">
  <div class="demo-grid-col">
    <span class="demo-grid-label">justify="start"（default behavior）</span>
    <oas-grid gap="8px" justify="start" style="width: 100%">
      <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
      <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
    </oas-grid>
    <span class="demo-grid-label">justify="center"</span>
    <oas-grid gap="8px" justify="center" style="width: 100%">
      <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
      <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
    </oas-grid>
    <span class="demo-grid-label">justify="end"</span>
    <oas-grid gap="8px" justify="end" style="width: 100%">
      <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
      <oas-grid-item span="8"><div class="demo-grid-box">span 8</div></oas-grid-item>
    </oas-grid>
  </div>
</DemoBlock>

<DemoBlock title="align (block axis, fixed height)">
  <div class="demo-grid-col">
    <span class="demo-grid-label">align="start"</span>
    <oas-grid gap="8px" align="start" style="width: 100%; height: 80px">
      <oas-grid-item span="8"><div class="demo-grid-box short">short</div></oas-grid-item>
      <oas-grid-item span="8"><div class="demo-grid-box">h 40</div></oas-grid-item>
      <oas-grid-item span="8"><div class="demo-grid-box tall">h 64</div></oas-grid-item>
    </oas-grid>
    <span class="demo-grid-label">align="center"</span>
    <oas-grid gap="8px" align="center" style="width: 100%; height: 80px">
      <oas-grid-item span="8"><div class="demo-grid-box short">short</div></oas-grid-item>
      <oas-grid-item span="8"><div class="demo-grid-box">h 40</div></oas-grid-item>
      <oas-grid-item span="8"><div class="demo-grid-box tall">h 64</div></oas-grid-item>
    </oas-grid>
    <span class="demo-grid-label">align="baseline"</span>
    <oas-grid gap="8px" align="baseline" style="width: 100%; height: 80px">
      <oas-grid-item span="8"><div class="demo-grid-box short">short</div></oas-grid-item>
      <oas-grid-item span="8"><div class="demo-grid-box">h 40</div></oas-grid-item>
      <oas-grid-item span="8"><div class="demo-grid-box tall">h 64</div></oas-grid-item>
    </oas-grid>
  </div>
</DemoBlock>

## Reordering (order / push / pull)

- `order` expresses reordering: the DOM/source order (and screen-reader semantics) stays unchanged, only the visual order changes.
- `push` / `pull` shift the start line right / left by N columns on top of `offset` (net shift = push − pull combined with offset); a net shift ≤ 0 falls back to auto-placement.

<DemoBlock title="Last column first (order)">
  <oas-grid gap="12px" style="width: 100%">
    <oas-grid-item span="8"><div class="demo-grid-box">source col 1 (order 0)</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">source col 2 (order 0)</div></oas-grid-item>
    <oas-grid-item span="8" order="-1"><div class="demo-grid-box">source col 3 (order -1 → first)</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

<DemoBlock title="push/pull shift">
  <oas-grid gap="12px" style="width: 100%">
    <oas-grid-item span="6"><div class="demo-grid-box">span 6</div></oas-grid-item>
    <oas-grid-item span="6" push="6"><div class="demo-grid-box">span 6 push 6 (6 cols right)</div></oas-grid-item>
    <oas-grid-item span="6" offset="6"><div class="demo-grid-box">span 6 offset 6</div></oas-grid-item>
    <oas-grid-item span="6" offset="6" pull="6"><div class="demo-grid-box">span 6 offset 6 pull 6 (pulled back)</div></oas-grid-item>
  </oas-grid>
</DemoBlock>

<DemoBlock title="Custom order">
  <oas-grid gap="12px" style="width: 100%">
    <oas-grid-item span="6" order="3"><div class="demo-grid-box">A (order 3)</div></oas-grid-item>
    <oas-grid-item span="6" order="1"><div class="demo-grid-box">B (order 1)</div></oas-grid-item>
    <oas-grid-item span="6" order="2"><div class="demo-grid-box">C (order 2)</div></oas-grid-item>
    <oas-grid-item span="6" order="0"><div class="demo-grid-box">D (order 0)</div></oas-grid-item>
  </oas-grid>
  <p class="demo-grid-note">Visual order: D (0) → B (1) → C (2) → A (3).</p>
</DemoBlock>

## Collapsed rows (collapsed-rows)

`collapsed-rows` sets how many rows stay visible in the collapsed state (row model accumulates child span/offset): children beyond the limit are hidden and an Expand tail cell appears (spanning the remaining columns of its row); clicking expands, and the tail turns into Collapse to fold back. The collapsed state writes back to the controlled `collapsed` attribute and emits `oas-collapse`. Not enabled under `columns` / `min-child-width` auto layout (the row model relies on span/offset semantics); the row model counts in 24 columns, so with a non-24 `cols` the row estimate deviates from the real layout (push/pull are not counted either) — avoid that combination. When the total row count does not exceed `collapsed-rows`, `collapsed` is never written (nothing to fold: everything stays visible with no tail cell).

<DemoBlock title="collapsed-rows=1 query filters">
  <oas-grid collapsed-rows="1" gap="12px" style="width: 100%">
    <oas-grid-item span="8"><div class="demo-grid-box">Filter 1</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">Filter 2</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">Filter 3</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">Filter 4 (row 2)</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">Filter 5 (row 2)</div></oas-grid-item>
    <oas-grid-item span="8"><div class="demo-grid-box">Filter 6 (row 2)</div></oas-grid-item>
  </oas-grid>
  <p class="demo-grid-note">Only the first row shows by default (filters 1–3); click the Expand tail to reveal all, Collapse to fold back.</p>
</DemoBlock>

## Child flex passthrough

The `oas-grid-item` `flex` attribute writes the CSS `flex` shorthand inline (not consumed by grid layout per CSS spec, zero interference) — a passthrough for host scenarios that place the item into flex containers.

<style>
  .demo-grid-box {
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--oas-color-bg-hover);
    border-radius: var(--oas-radius-md);
    font-size: var(--oas-font-size-xs);
    color: var(--oas-color-text-secondary);
  }
  /* 4-level primary gradient cycle to distinguish adjacent blocks (token-derived, dark-adaptive).
     Stops are picked at the two ranges where "light bg + dark text" and "solid bg + light text" each pass contrast; the middle range (neither works) is skipped */
  oas-grid > *:nth-child(4n+1) .demo-grid-box,
  oas-grid > .demo-grid-box:nth-child(4n+1) {
    background: color-mix(in srgb, var(--oas-color-primary) 10%, var(--oas-color-bg));
    color: var(--oas-color-primary-text);
  }
  oas-grid > *:nth-child(4n+2) .demo-grid-box,
  oas-grid > .demo-grid-box:nth-child(4n+2) {
    background: color-mix(in srgb, var(--oas-color-primary) 22%, var(--oas-color-bg));
    color: var(--oas-color-primary-text);
  }
  oas-grid > *:nth-child(4n+3) .demo-grid-box,
  oas-grid > .demo-grid-box:nth-child(4n+3) {
    background: color-mix(in srgb, var(--oas-color-primary) 78%, var(--oas-color-bg));
    color: var(--oas-color-text-on-primary);
  }
  oas-grid > *:nth-child(4n+4) .demo-grid-box,
  oas-grid > .demo-grid-box:nth-child(4n+4) {
    background: var(--oas-color-primary);
    color: var(--oas-color-text-on-primary);
  }
  /* In dark the primary gets lighter (#9ecdff), so the gradient steps are spread wider to stay distinguishable.
     Steps mirror the light two-part scheme: two light-bg steps (14%/22%) with primary text, two solid steps (90%/100%)
     with on-primary text — the middle mixed range (bg too dark for dark text, too light for light text) is skipped */
  .dark oas-grid > *:nth-child(4n+1) .demo-grid-box,
  .dark oas-grid > .demo-grid-box:nth-child(4n+1) {
    background: color-mix(in srgb, var(--oas-color-primary) 14%, var(--oas-color-bg));
    color: var(--oas-color-primary);
  }
  .dark oas-grid > *:nth-child(4n+2) .demo-grid-box,
  .dark oas-grid > .demo-grid-box:nth-child(4n+2) {
    background: color-mix(in srgb, var(--oas-color-primary) 22%, var(--oas-color-bg));
    color: var(--oas-color-primary);
  }
  .dark oas-grid > *:nth-child(4n+3) .demo-grid-box,
  .dark oas-grid > .demo-grid-box:nth-child(4n+3) {
    background: color-mix(in srgb, var(--oas-color-primary) 90%, var(--oas-color-bg));
    color: var(--oas-color-text-on-primary);
  }
  .dark oas-grid > *:nth-child(4n+4) .demo-grid-box,
  .dark oas-grid > .demo-grid-box:nth-child(4n+4) {
    background: var(--oas-color-primary);
    color: var(--oas-color-text-on-primary);
  }
  .demo-grid-box.short {
    height: 28px;
  }
  .demo-grid-box.tall {
    height: 64px;
  }
  .demo-grid-col {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 100%;
  }
  .demo-grid-label {
    font-size: var(--oas-font-size-xs);
    color: var(--oas-color-text-secondary);
  }
  .demo-grid-note {
    font-size: var(--oas-font-size-xs);
    color: var(--oas-color-text-secondary);
    margin-top: 8px;
  }
</style>

## API

### oas-grid

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `align` | Block-axis alignment (align-items): `start` / `center` / `end` / `stretch` / `baseline`; invalid values fall back to `stretch` with a dev warning (deduped) | `string` | — |
| `collapsed` | Controlled collapsed attr for collapsed-rows (bootstrapped collapsed by default; host/tail can toggle) | — | — |
| `collapsed-rows` | Rows visible when collapsed; overflow hidden with an expand/collapse tail cell (writes back collapsed, emits oas-collapse) | `string` | `1` |
| `cols` | Total column count | `string` | `24` |
| `column-gap` | Independent column gap (same rules) | `string` | — |
| `columns` | Auto equal column count (simple-grid; child span ignored when set): supports breakpoint shorthand (e.g. 3 md:2 sm:1; breakpoints sm=640/md=768/lg=1024/xl=1280); takes precedence over min-child-width | `string` | — |
| `gap` | Gap; a single value applies to both axes, two space-separated values set `row column` (e.g. `8 16` = row 8, column 16); three or more values are invalid and silently fall back to `0` | `string` | `0` |
| `justify` | Inline-axis alignment (justify-items): `start` / `center` / `end` / `stretch`; invalid values fall back to `stretch` with a dev warning (deduped) | `string` | — |
| `min-child-width` | Minimum child width (e.g. 180px; bare numbers read as px): auto-fit + minmax derives the column count from container width (breakpoint-free fluid reflow); `columns` wins when both are set, and child span/offset are inert in this mode | `string` | — |
| `row-gap` | Independent row gap overriding the matching axis of gap (numbers as px) | `string` | — |

#### Events

| Event | Description |
| --- | --- |
| `oas-collapse` | Emitted on collapsed-rows tail toggle, `detail: { collapsed }` |

#### Slots

| Name | Description |
| --- | --- |
| default | Grid children (`oas-grid-item`) |

#### CSS Variables

| CSS Variable | Default |
| --- | --- |
| `--oas-grid-columns` | `repeat(${base ?? 1}, 1fr)` |

### oas-grid-item

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `flex` | Writes the CSS flex shorthand inline (unused by grid; passthrough for flex contexts) | `string` | — |
| `offset` | Number of columns offset on the left; supports breakpoint shorthand (e.g. `0 lg:4`: base value + space-separated `breakpoint:value`, breakpoints sm=640 / md=768 / lg=1024 / xl=1280) | `string` | `0` |
| `order` | Ordering weight (number, default 0); higher values appear later, for reordering columns (equivalent to offset-based push/pull scenarios) | `string` | `0` |
| `pull` | Shifts the start line left by N columns (same rules) | `string` | `0` |
| `push` | Shifts the start line right by N columns (net = push−pull on top of offset) | `string` | `0` |
| `span` | Number of columns spanned; supports `auto` (natural content width) and breakpoint shorthand (e.g. `24 md:12`: base value + space-separated `breakpoint:value`, breakpoints sm=640 / md=768 / lg=1024 / xl=1280) | `string` | `24` |

#### Slots

| Name | Description |
| --- | --- |
| default | Grid cell content |

#### CSS Variables

| CSS Variable | Default |
| --- | --- |
| `--oas-grid-item-column` | `${baseColumn}` |

`oas-grid` renders as CSS Grid where each child occupies a share of the 24 columns; `oas-grid-item` declares its footprint with `span`. Setting `columns` divides equally and ignores `span`, so plain elements can be placed directly.
