Grid
A 12-column responsive grid with span helpers
import { Grid } from '@/components/grid';
const Cell = ({ children }: { children: React.ReactNode }) => (
<div className="rounded-lg border border-border bg-background-secondary p-4 text-center text-foreground-secondary text-sm">
{children}
</div>
);
export default function GridPreview() {
return (
<Grid>
<Grid.Col span={4}>
<Cell>span 4</Cell>
</Grid.Col>
<Grid.Col span={4}>
<Cell>span 4</Cell>
</Grid.Col>
<Grid.Col span={4}>
<Cell>span 4</Cell>
</Grid.Col>
<Grid.Col span={6}>
<Cell>span 6</Cell>
</Grid.Col>
<Grid.Col span={6}>
<Cell>span 6</Cell>
</Grid.Col>
<Grid.Col span="full">
<Cell>span full</Cell>
</Grid.Col>
</Grid>
);
} Dependencies
Source Code
import type { VariantProps } from 'cva';
import { Slot } from '@/components/slot';
import { cn, cva } from '@/lib/utils/classnames';
const gridStyle = cva({
base: 'grid',
variants: {
cols: {
1: 'grid-cols-1',
2: 'grid-cols-1 sm:grid-cols-2',
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-2 sm:grid-cols-2 lg:grid-cols-4',
6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
12: 'grid-cols-4 sm:grid-cols-6 lg:grid-cols-12',
},
gap: {
none: 'gap-0',
xs: 'gap-2',
sm: 'gap-3',
md: 'gap-4 md:gap-6',
lg: 'gap-6 md:gap-8',
xl: 'gap-8 md:gap-12',
},
},
defaultVariants: {
cols: 12,
gap: 'md',
},
});
interface GridProps
extends React.ComponentPropsWithRef<'div'>,
VariantProps<typeof gridStyle> {
asChild?: boolean;
}
const Grid = ({ ref, className, cols, gap, asChild, ...rest }: GridProps) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
className={cn(gridStyle({ cols, gap }), className)}
{...rest}
/>
);
};
const colStyle = cva({
base: '',
variants: {
span: {
1: 'col-span-1',
2: 'col-span-2',
3: 'col-span-2 sm:col-span-3',
4: 'col-span-2 sm:col-span-4',
5: 'col-span-2 sm:col-span-5',
6: 'col-span-full sm:col-span-6',
7: 'col-span-full sm:col-span-7',
8: 'col-span-full sm:col-span-8',
9: 'col-span-full sm:col-span-9',
10: 'col-span-full sm:col-span-10',
11: 'col-span-full sm:col-span-11',
12: 'col-span-full',
full: 'col-span-full',
},
},
defaultVariants: {
span: 'full',
},
});
interface GridColProps
extends React.ComponentPropsWithRef<'div'>,
VariantProps<typeof colStyle> {
asChild?: boolean;
}
const GridCol = ({ ref, className, span, asChild, ...rest }: GridColProps) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp ref={ref} className={cn(colStyle({ span }), className)} {...rest} />
);
};
interface GridSubgridProps
extends React.ComponentPropsWithRef<'div'>,
VariantProps<typeof colStyle> {
asChild?: boolean;
}
/**
* Spans columns of the parent `Grid` while making its own children align to
* the parent's column tracks (`grid-template-columns: subgrid`). Use it to
* line up nested content — e.g. card title / body / footer — across a row
* regardless of each item's content length. Requires an explicit `span` so
* the item occupies tracks before subgrid inherits them.
*/
const GridSubgrid = ({
ref,
className,
span,
asChild,
...rest
}: GridSubgridProps) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
className={cn('grid grid-cols-subgrid', colStyle({ span }), className)}
{...rest}
/>
);
};
const CompoundGrid = Object.assign(Grid, {
Col: GridCol,
Subgrid: GridSubgrid,
});
export type { GridColProps, GridProps, GridSubgridProps };
export { CompoundGrid as Grid, colStyle, gridStyle }; Server component. No
'use client'— grid layout primitive; no hooks or own handlers.
Grid provides a 12-column responsive grid with sensible breakpoints.
Use Grid.Col to control the column span of children.
Use Grid.Subgrid when a grid item should both span parent columns and
align its own children to the parent’s column tracks — e.g. lining up card
title / body / footer across a row regardless of content length.
Anatomy
<Grid>
<Grid.Col span={6}>{...}</Grid.Col>
<Grid.Col span={6}>{...}</Grid.Col>
</Grid>
<Grid>
<Grid.Subgrid span={4}>
<h3>Title</h3>
<p>Body</p>
</Grid.Subgrid>
</Grid>
API Reference
Grid
Extends the div element.
| Prop | Default | Type |
|---|---|---|
cols | 12 | 1234612 |
gap | "md" | "none""xs""sm""md""lg""xl" |
asChild | - | boolean |
Grid.Col
Extends the div element.
| Prop | Default | Type |
|---|---|---|
span | "full" | 123456789101112"full" |
asChild | - | boolean |
Grid.Subgrid
Extends the div element. Spans parent columns via span and applies
grid-template-columns: subgrid so its children align to the parent grid.
| Prop | Default | Type |
|---|---|---|
span | "full" | 123456789101112"full" |
asChild | - | boolean |
Examples
Default
import { Grid } from '@/components/grid';
const Cell = ({ children }: { children: React.ReactNode }) => (
<div className="rounded-lg border border-border bg-background-secondary p-4 text-center text-foreground-secondary text-sm">
{children}
</div>
);
export default function GridPreview() {
return (
<Grid>
<Grid.Col span={4}>
<Cell>span 4</Cell>
</Grid.Col>
<Grid.Col span={4}>
<Cell>span 4</Cell>
</Grid.Col>
<Grid.Col span={4}>
<Cell>span 4</Cell>
</Grid.Col>
<Grid.Col span={6}>
<Cell>span 6</Cell>
</Grid.Col>
<Grid.Col span={6}>
<Cell>span 6</Cell>
</Grid.Col>
<Grid.Col span="full">
<Cell>span full</Cell>
</Grid.Col>
</Grid>
);
} Subgrid
Short
One line.
FooterMedium
A body with a couple more lines of supporting copy than the others.
FooterLong
The longest body in the row, with enough copy to push its natural height well beyond its siblings — yet every footer still aligns.
Footerimport { Grid } from '@/components/grid';
export const meta = { layout: 'padded' };
const Card = ({ title, body }: { title: string; body: string }) => (
// Three rows: title / body / footer. grid-rows-subgrid makes all cards in
// the row share row tracks, so footers align regardless of body length.
<Grid.Subgrid
span={4}
className="row-span-3 grid-rows-subgrid gap-2 rounded-lg border border-border bg-background-secondary p-4"
>
<h3 className="font-medium text-foreground text-sm">{title}</h3>
<p className="text-foreground-secondary text-sm">{body}</p>
<span className="text-foreground-secondary text-xs">Footer</span>
</Grid.Subgrid>
);
export default function GridSubgridPreview() {
return (
<Grid cols={12} className="grid-rows-[auto_1fr_auto]">
<Card title="Short" body="One line." />
<Card
title="Medium"
body="A body with a couple more lines of supporting copy than the others."
/>
<Card
title="Long"
body="The longest body in the row, with enough copy to push its natural height well beyond its siblings — yet every footer still aligns."
/>
</Grid>
);
} Previous
Flex
Next
Input