Grid
Equal-width column layout. Tracks use minmax(0, 1fr) so a long word in one cell cannot blow out the row.
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall and import
Grid is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { Grid } from "@dovetail-ds/react";
Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.
Guidelines
Equal-width column layout. Tracks use minmax(0, 1fr) so a long word in one cell cannot blow out the row.
Use it when
- Card grids, feature rows, dashboard tiles, any repeating set.
Don't use it when
- There are two or three items in a row that should size to content. Use
Inline.
Example
<Grid minColumnWidth="240px" gap="lg">
{plans.map((p) => <Card key={p.id} title={p.name} />)}
</Grid>
Prefer minColumnWidth over columns. It is responsive without a media query, and it degrades to one column on a phone automatically.
When the count can change
minColumnWidth uses auto-fit, which lets the columns that exist grow to fill the row. That is right for a grid whose count never changes, and wrong for one a filter can narrow: a single result stretches across the whole row. Pass track="fill" and the empty tracks are kept, so one card stays one card wide.
<Grid minColumnWidth="260px" track="fill">{results.map(renderCard)}</Grid>
Tokens
--dt-space-inline-*.
Props
The typed contract, from Grid.d.ts.
import * as React from "react";
/** Equal-width column grid. Pass minColumnWidth for a responsive grid with no media queries. */
export interface GridProps extends React.HTMLAttributes<HTMLElement> {
/** Fixed column count. Ignored when minColumnWidth is set. @default 2 */
columns?: number;
/** @default "md" */
gap?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
/** CSS length, e.g. "240px". Columns fit as many as will hold this width. */
minColumnWidth?: string;
/** With minColumnWidth: "fit" lets the columns that exist grow to fill the row;
* "fill" keeps the empty tracks, so a filter that lands on one result leaves
* one card at its intended width instead of stretching it. @default "fit" */
track?: "fit" | "fill";
align?: React.CSSProperties["alignItems"];
/** @default "div" */
as?: keyof React.JSX.IntrinsicElements;
children?: React.ReactNode;
}
export declare function Grid(props: GridProps): React.JSX.Element;
Tokens it reads
Every custom property Grid.jsx resolves, read from the source: 6 semantic. Component tier first, because that is the one to reach for.
- semantic Shared roles. Override one and everything using that role moves with it.
| Token | Tier | Declared as |
|---|---|---|
--dt-space-inline-2xl | semantic | var(--dt-dim-12) |
--dt-space-inline-lg | semantic | var(--dt-dim-6) |
--dt-space-inline-md | semantic | var(--dt-dim-4) |
--dt-space-inline-sm | semantic | var(--dt-dim-3) |
--dt-space-inline-xl | semantic | var(--dt-dim-8) |
--dt-space-inline-xs | semantic | var(--dt-dim-2) |
Source
React only, no dependencies, every value a token.
import React from "react";
const GAPS = { xs: "var(--dt-space-inline-xs)", sm: "var(--dt-space-inline-sm)", md: "var(--dt-space-inline-md)", lg: "var(--dt-space-inline-lg)", xl: "var(--dt-space-inline-xl)", "2xl": "var(--dt-space-inline-2xl)" };
export function Grid({ columns = 2, gap = "md", minColumnWidth, track = "fit", align, as: Tag = "div", children, style, ...rest }) {
const template = minColumnWidth
? `repeat(${track === "fill" ? "auto-fill" : "auto-fit"}, minmax(min(${minColumnWidth}, 100%), 1fr))`
: `repeat(${columns}, minmax(0, 1fr))`;
return (
<Tag style={{ display: "grid", gridTemplateColumns: template, gap: GAPS[gap] || GAPS.md, alignItems: align, ...style }} {...rest}>
{children}
</Tag>
);
}
In context
Grid also appears on the primitives reference cards, alongside the rest of its family.