Theming and styling
There is no grid stylesheet to import or override. Every colour is one of your shadcn tokens, every size is a Tailwind class, and the source is in your project, so the grid already looks like the rest of your app and changes the way the rest of your app does.
| Ananya Nguyen | Research | Active | Enterprise | $711.00 | Apr 26, 2023 | |
| Ishaan Nguyen | Growth | Active | Pro | $48.00 | Nov 25, 2024 | |
| Nia Nair | Design | Paused | Enterprise | $4,819.00 | Jun 1, 2023 | |
| Freya Kim | Sales | Active | Enterprise | $4,424.00 | Jun 28, 2026 | |
| Aarav Silva | Data | Active | Enterprise | $2,528.00 | Sep 22, 2023 | |
| Leila Khan | Research | Active | Free | $0.00 | Apr 4, 2025 | |
| Omar Iyer | Growth | Active | Free | $0.00 | Jul 11, 2026 | |
| Arjun Okafor | Data | Invited | Pro | $36.00 | May 6, 2024 | |
| Kwame Das | Platform | Active | Free | $0.00 | Sep 12, 2023 | |
| Amelia Silva | Growth | Invited | Team | $609.00 | Jun 8, 2023 |
import { DataGrid, createDataGridColumns, useDataGrid } from "@/components/ui/data-grid";
import { makePeople, PLAN_OPTIONS, STATUS_OPTIONS, type Person,} from "@/lib/data-grid-demo-data";
const col = createDataGridColumns<Person>();
const columns = col.columns([ col.select(), col.text("name", { header: "Name", size: 150 }), col.text("team", { header: "Team", size: 110 }), col.enum("status", { header: "Status", options: STATUS_OPTIONS, size: 100 }), col.enum("plan", { header: "Plan", options: PLAN_OPTIONS, size: 110 }), // Per-column classes for the header cell and the body cells. col.currency("mrr", { header: "MRR", currency: "USD", size: 110, headerClassName: "text-indigo-700 dark:text-indigo-300", cellClassName: (v) => v > 500 ? "font-semibold text-indigo-700 dark:text-indigo-300" : undefined, }), col.date("joinedAt", { header: "Joined", size: 120 }),]);
const data = makePeople(60, 53);
/** Striped, compact, a brand-coloured header row and a tinted selected row: all Tailwind classes. */export default function DataGridTheming() { const grid = useDataGrid<Person>({ key: "theming", data, columns, getRowId: (r) => r.id, defaultDensity: "compact", pagination: { pageSize: 10 }, }); return ( <DataGrid grid={grid} striped tableClassName="rounded-xl border-indigo-200 dark:border-indigo-900 [&_thead_th]:bg-indigo-50 [&_thead_th]:text-indigo-950 dark:[&_thead_th]:bg-indigo-950/60 dark:[&_thead_th]:text-indigo-50" getRowClassName={(row) => row.getIsSelected() ? "bg-indigo-500/10 hover:bg-indigo-500/15" : row.original.status === "churned" ? "text-muted-foreground" : undefined } /> );}The tokens it uses
Section titled “The tokens it uses”| Token | Where |
|---|---|
background |
the viewport, header cells, pinned cells, the fullscreen backdrop, editors |
foreground |
text; the dotted filler below the rows (at 14 % through color-mix) |
muted / muted-foreground |
row hover (bg-muted/40), stripes (bg-muted/20), grouped rows (bg-muted/30), the footer, secondary text, sort and menu icons |
accent |
a header that is a drop target while dragging (bg-accent/60), hovered rows in the columns popover |
primary |
the focused-cell ring, selected cells (bg-primary/10), selected rows (bg-primary/5, hover /10), range edges, the loading bar, links, the boolean “yes” pill, the active resize handle |
border |
cell and header borders, pinned-edge shadows, the resize handle |
ring |
focus-visible rings on header buttons |
popover |
the bulk-actions bar |
destructive |
an editor whose value was rejected |
Dark mode is whatever your theme does with .dark: the grid has no dark: variants of its own.
If your tokens pass contrast in both modes, so does the grid.
Density
Section titled “Density”The root sets --dg-row-height (32, 40 or 48 px) and data-density="compact" | "comfortable" | "spacious";
rows, cells and headers are h-(--dg-row-height). Change the three values in DENSITY_ROW_HEIGHT
(data-grid-types.ts) or target a density from CSS:
[data-slot="data-grid"][data-density="compact"] td { font-size: 0.8125rem; }Class hooks
Section titled “Class hooks”Without touching the source:
| Hook | Applies to |
|---|---|
className |
the root div (flex min-h-0 flex-col gap-3 text-foreground) |
tableClassName |
the viewport (relative flex min-h-0 flex-col rounded-lg border bg-background) |
striped |
odd rows get bg-muted/20 |
getRowClassName(row) |
a row; merged after the built-in classes, so bg-* you return wins |
headerClassName (column) |
that column’s <th> |
cellClassName (column) |
that column’s <td>; a string or (value, row) => string |
options[].className (enum, tags) |
the badge for that value (a status colour) |
align, mono, wrap, truncate (column) |
text alignment, monospace, wrapping |
className on any part |
in a composed layout |
Classes are merged with cn() (tailwind-merge), so a conflicting utility you pass replaces the
built-in one rather than fighting it. Arbitrary variants reach inside: the example above colours
the header row with [&_thead_th]:bg-indigo-50 on tableClassName.
Data attributes
Section titled “Data attributes”For CSS or tests:
| Attribute | On | Values |
|---|---|---|
data-slot |
the parts | data-grid, data-grid-viewport, data-grid-table, data-grid-toolbar, data-grid-search, data-grid-grouping-bar, data-grid-pagination, data-grid-bulk-actions, data-grid-filter |
data-density, data-fullscreen |
the root | the density; true while fullscreen |
data-row-id, data-index, data-state="selected", data-grouped, data-pinned |
rows | — / — / selected / true / top or bottom |
data-cell-id, data-column-id, data-kind, data-selected, data-focused, data-editing, data-editable |
cells | ids, the column kind, booleans |
data-column-id, data-pinned, data-dragging, data-drop-target |
headers | — |
[data-slot="data-grid"] tr[data-state="selected"] td { background: color-mix(in oklch, var(--primary) 8%, transparent); }[data-slot="data-grid"] td[data-kind="currency"] { font-variant-numeric: tabular-nums; }Editing the source
Section titled “Editing the source”The styles are ordinary class strings in the files the CLI copied. The ones people change first:
| What | File | Look for |
|---|---|---|
| row hover, stripes, selected and grouped tints, pinned rows | data-grid-table.tsx, DataGridRowView |
hover:bg-muted/40, bg-muted/20, bg-primary/5 hover:bg-primary/10, bg-muted/30 |
| cell padding, borders, focus ring, selection tint and edges | data-grid-table.tsx, DataGridCellView |
px-2, border-b, ring-2 ring-primary ring-inset, bg-primary/10, the four shadow-[inset…] |
| the dotted filler below the last row | data-grid-table.tsx, after </table> |
the aria-hidden div with bg-[radial-gradient(…)]; delete it for a plain background |
| header background, sort icons, drop-target tint, resize handle | data-grid-column-header.tsx |
bg-background, ChevronsUpDownIcon, bg-accent/60, after:bg-border / after:bg-primary |
| badges, boolean pill, links, tags | data-grid-cells.tsx |
Badge variant="secondary", bg-primary/10 text-primary, text-primary underline-offset-4 |
| the editor ring and error state | data-grid-editing.tsx |
ring-2 ring-primary ring-inset, ring-destructive |
| toolbar buttons and menus | data-grid-toolbar.tsx |
variant="outline" size="sm" |
| fullscreen frame, bulk-actions bar | data-grid.tsx |
fixed inset-0 z-50 bg-background p-4, bg-popover shadow-md |
Keep your own cells, parts and column kinds in files next to the grid where you can; then
shadcn add @kadal/data-grid --overwrite stays painless (updating).
Icons are lucide-react, imported at the top of each file (ArrowUpIcon, FilterIcon,
PinIcon, DownloadIcon, …). Swap a set by changing the imports; the grid sizes them with
size-3.5 and size-4 classes, so any component that accepts className fits.
Primitives
Section titled “Primitives”The menus, popovers, selects, checkboxes, dialogs and tooltips are your shadcn components on
Base UI, used through Base UI’s render prop: <PopoverTrigger render={<Button variant="outline" size="sm" />}>.
Swapping a primitive for your own means keeping that contract (and Base UI-specific props such as
closeOnClick on menu items, items on Select, indeterminate on Checkbox).
See also
Section titled “See also”- Density
- Accessibility: what the contrast depends on.
- Composition: your own layout from the parts.