Grid

Equal-column CSS grid with responsive column counts.

import { Grid } from "@ps1ui/core";

Columns

01
02
03
04
05
06
<Grid columns={3} gap="md">
  <div>01</div>
  <div>02</div>
  <div>03</div>
  <div>04</div>
  <div>05</div>
  <div>06</div>
</Grid>

Responsive columns

01
02
03
04
05
06
<Grid columns={{ base: 1, sm: 2, md: 3 }} gap="md">
  <div>01</div>
  <div>02</div>
  <div>03</div>
  <div>04</div>
  <div>05</div>
  <div>06</div>
</Grid>

Column span with GridItem

hero
02
03
04
05
06
<Grid columns={4} gap="md">
  <GridItem colSpan={2}>hero</GridItem>
  <GridItem>02</GridItem>
  <GridItem>03</GridItem>
  <GridItem>04</GridItem>
  <GridItem>05</GridItem>
  <GridItem>06</GridItem>
</Grid>

Responsive column span

hero
02
03
04
05
<Grid columns={4} gap="md">
  <GridItem colSpan={{ base: 4, md: 2, lg: 4 }}>hero</GridItem>
  <GridItem>02</GridItem>
  <GridItem>03</GridItem>
  <GridItem>04</GridItem>
  <GridItem>05</GridItem>
</Grid>

Semantic element

Pass as to render a tag other than the default <div>. A card grid is usually a list, so <Grid as="ul"> beats layeringrole="list" onto a <div> — and because a <ul> may only contain <li>, GridItem carries the same prop. Change the two together.

Everything else is unchanged: columns, gap, colSpan and queryContainer behave identically on any tag, the rendered element's own attributes become available, and as also accepts a component rather than a tag name.

As a list

  • hero
  • 02
  • 03
{/* A card grid is a list — say so in the markup rather than
    with role="list" on a <div>. Pair the tags: <ul> takes <li> children. */}
<Grid as="ul" columns={3} gap="md" aria-label="cards">
  <GridItem as="li" colSpan={2}>hero</GridItem>
  <GridItem as="li">02</GridItem>
  <GridItem as="li">03</GridItem>
</Grid>

Query container

Responsive props resolve against the nearest ancestor that is a container-query context — normally the PS1Root wrapping your app. Pass queryContainer to make aGrid that context instead. This is also what makes a childGridItem's responsive colSpan track the Grid's own width rather than the page's.

It is opt-in because container-type: inline-size erases the element's intrinsic width: an opted-in Grid collapses to zero inside any shrink-to-fit parent (a row Stack, an auto grid track, a float). Give it a definite inline size whenever you turn it on.

Query container

01
02
03
{/* The inner Grid — and any GridItem colSpan inside it —
    now measures the 320px outer Grid instead of the page. */}
<Grid queryContainer columns={1} style={{ width: 320 }}>
  <Grid columns={{ base: 1, md: 3 }} gap="md">
    <div>01</div>
    <div>02</div>
    <div>03</div>
  </Grid>
</Grid>

Props

PropTypeDefaultDescription
asElementType"div"

Element or component to render instead of the default <div> — e.g. "ul" for a card grid, paired with `<GridItem as="li">`.

columnsnumber1

Number of equal-width columns.

gap"none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl""md"

Gap between cells on the space scale.

queryContainerbooleanfalse

Make this Grid a container-query context so descendants' responsive props — including child `GridItem`s' `colSpan` — resolve against its width instead of the nearest ancestor container. Costs the Grid its intrinsic width — it collapses to 0 in shrink-to-fit parents (row flex, auto grid track, float), so give it a definite inline size when opting in.

Also accepts the attributes of the element rendered via as (default: <div>).

GridItem Props

PropTypeDefaultDescription
asElementType"div"

Element or component to render instead of the default <div> — pair it with the parent Grid's `as` (e.g. `as="li"` inside `<Grid as="ul">`) so the markup stays valid.

colSpannumber1

Number of grid columns the item spans.

Also accepts the attributes of the element rendered via as (default: <div>).