Grid

Equal-width column layout. Tracks use minmax(0, 1fr) so a long word in one cell cannot blow out the row.

Primitives Grid.jsx Grid.d.ts Grid.md

Playground

Change a prop and watch it. The code below follows along.

Install 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.
TokenTierDeclared as
--dt-space-inline-2xlsemanticvar(--dt-dim-12)
--dt-space-inline-lgsemanticvar(--dt-dim-6)
--dt-space-inline-mdsemanticvar(--dt-dim-4)
--dt-space-inline-smsemanticvar(--dt-dim-3)
--dt-space-inline-xlsemanticvar(--dt-dim-8)
--dt-space-inline-xssemanticvar(--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.