gridcn1.3.0

Styling & theming

The shadcn token contract, the built-in class surface, data attributes, and how to restyle the grid.

gridcn renders only existing shadcn tokens. It ships no design layer of its own, so restyling the grid is restyling your theme. Everything below answers three questions: which surfaces use which token, which classes the grid renders and can be overridden, and how deep you can go when you need something custom.

Name
Email
Age
Active
Role
Joined
Score
Silent Tiger
user0@example.com
63
Viewer
Jul 5, 2023
4
Quick Lion
user1@example.com
67
Viewer
Apr 3, 2021
89
Swift Wolf
user2@example.com
40
Manager
Oct 27, 2020
80
Eager Lion
user3@example.com
50
User
Oct 8, 2022
51
Eager Tiger
user4@example.com
45
Viewer
Oct 7, 2024
83
Silent Wolf
user5@example.com
24
User
Jun 1, 2020
15
Swift Lion
user6@example.com
34
Viewer
Oct 18, 2022
62
Eager Lion
user7@example.com
21
Manager
Oct 7, 2021
42
Eager Eagle
user8@example.com
25
Editor
Jun 11, 2024
35
Bold Tiger
user9@example.com
19
Editor
Mar 20, 2024
52
Bold Bear
user10@example.com
42
User
Apr 16, 2020
28
Swift Wolf
user11@example.com
47
Manager
Aug 4, 2022
9

See examples: Styling · Loading states · I18n & RTL

The color scheme: shadcn tokens

Every color in the grid resolves to a shadcn theme token (--background, --primary, and so on in your :root / .dark blocks). The map below is the complete token-to-surface contract:

TokenSurfaces it paints
backgroundThe root frame and every data cell's base fill, and the marker cells.
borderThe root frame border, the bottom border of every cell, header, and marker cell, the pinned-row band separator, and skeleton row separators.
mutedHeader cells and the marker header, the row hover tint (50% over background), the selected marker cell, the pinned-row band tint (30%), and the loading skeleton bars.
muted-foregroundThe empty-state text, marker row numbers, the sort indicator, the row-reorder drag handle, and the column-menu trigger.
accent / accent-foregroundThe column-menu trigger hover.
primaryThe selection fill (10%) and border (40%), the active-cell ring, the resize-handle indicator line, the column-reorder drop indicator, the fill preview and handle, the loading bar, and the post-commit flash pulse (24%).
destructiveInvalid cells (tint + inset ring) and the validation error tooltip.
search-highlight (optional)The quick-search match tint (60%, 25% in dark). Without it, the grid tints matches with primary. See CSS variables.

Changing the color scheme means changing the tokens in your theme. The grid follows, in both light and dark, with no grid-specific configuration:

:root {
  /* selection, active ring, resize handle, fill, and loading bar all re-brand from this one token */
  --primary: oklch(0.6 0.15 250);
  --primary-foreground: oklch(0.99 0 0);
}
.dark {
  --primary: oklch(0.7 0.15 250);
  --primary-foreground: oklch(0.2 0.05 250);
}

--muted re-tints every header and the hover/selection banding in one move. --destructive re-tints validation errors. --background / --border re-skin the grid body. When one surface needs to differ from its token, the overrides are per-surface: data-attribute CSS (below), class merges (below), or the root className.

CSS variables

  • --grid-row-height: set per instance by the rowHeight prop or the density preset (compact 28px, default 36px, comfortable 44px). It sizes every row track.
  • --grid-pin-shadow: the color of the shadow at pinned column and row bands, and the default color of the edge shadows. The CLI install adds it to your CSS for light and dark mode. Without the variable, the grid uses a light-mode default.
  • --grid-column-border: the color of the vertical lines between columns. Defaults to --color-border. transparent hides the lines.
  • --grid-scroll-shadow and --grid-shadow-size: the edge shadow color and strip size. See Scroll shadows.
  • --grid-scroll-left: internal. The grid writes the live horizontal scroll offset so the marker column and header layer can counter-translate. Read-only for consumers.
  • --search-highlight: an optional theme color for the quick-search tint (60% over the background, 25% in dark). The CLI install adds it for light and dark mode. Without it, the grid tints matches with a light mix of --primary. Define it (for example an amber that does not collide with the primary selection tint) to set your own color. A color set only on --color-search-highlight also works, and --search-highlight wins when both exist. The [data-search-match] attribute is stamped either way.
  • --presence-color: internal to the data-grid-presence add-on: each remote user's highlight overlay and name chip are mixed from that user's color, set through the presence API, not CSS.
VariableEffectDefault
--presence-label-foreground Newtext color of a presence name chipblack or white, derived from the user color
--grid-flash-durationduration of the post-commit write pulse. 0s disables the pulse1.2s
--grid-flash-intensitycolor-mix() percentage of --grid-flash-color at the pulse's start24%
--grid-flash-colorthe pulse's tint colorvar(--color-primary)
--grid-loading-sweep-durationduration of the loading bar's sweep animation1.2s
<DataGridProvider {...grid} columns={columns}>
  <DataGridRoot density="compact" />
</DataGridProvider>

or an explicit height, which wins over density when both are set:

<DataGridProvider {...grid} columns={columns}>
  <DataGridRoot rowHeight={32} />
</DataGridProvider>

Both density and rowHeight live on DataGridRoot (or the DataGrid wrapper), not on DataGridProvider.

The built-in class surface

These are the exact classes the grid renders on its own elements, so you know what to expect in the DOM and what you can beat. Where a surface lists a merge point, the built-in classes come first in cn(), so a conflicting consumer utility wins.

Surface (selector)Built-in classesOverride point
Grid root ([role="grid"])relative select-none overflow-auto rounded-md border border-border bg-background text-smclassName on DataGridRoot / DataGrid.
Header cell ([role="columnheader"])group relative flex items-center border-b border-border bg-muted px-2 font-medium select-none plus the column linePer-column headerClassName.
Data cell ([role="gridcell"])relative flex items-center overflow-hidden border-b border-border px-2 outline-none plus the column line and conditionals: an opaque background with an opaque hover tint on pinned cells, the search tint on [data-search-match], tabular-nums for number/date, the alignment class, the invalid-cell tint + inset ringgetRowClassName/getCellClassName (grid level) then cellClassName (per column), merged after the built-ins.
Row ([role="row"])group/row data-hovered:bg-muted/50; the row under the pointer carries data-hovered, and the tint waits for the scroll to settle. Pinned and marker cells stay opaque, so they do not show a background set here.getRowClassName.
Marker cell ([data-grid-marker-cell])group flex items-center justify-center border-b border-border bg-background, the column line, cursor-grab when row reorder is on, the selected-row tintrenderMarker replaces the content. For the cell chrome itself, see Override recipes.
Marker header ([data-grid-marker-header])flex items-center justify-center border-b border-border bg-muted plus the column lineInternal.
Selection overlay ([data-grid-selection-overlay])pointer-events-none border border-primary/40 bg-primary/10--primary. A plugin Overlay for your own layer.
Active-cell ring ([data-grid-active-cell-overlay])pointer-events-none z-10 ring-2 ring-inset ring-primary--primary or --ring.
Edge shadow ([data-grid-pin-shadow])8px gradient strip on each side, opacity-0 to 100 via data-scrolled-*, at the grid edge or the pinned band boundary. Each strip spans only the scrolling area between the pinned bands. Color from --grid-pin-shadowOverride --grid-pin-shadow per theme.
Column line1px line at the end side of each column, and at the start side of a right-pinned column. Color from --grid-column-border--grid-column-border. transparent hides the lines.
Empty state ([data-grid-empty-state])pointer-events-none absolute inset-0 flex items-center justify-center text-sm text-muted-foregroundThe emptyState prop (node or function) or labels.emptyState.
Loading skeleton ([data-grid-loading-skeleton])One h-4 animate-pulse rounded bg-muted bar per column per row, rows separated by border-b border-borderThe loading flag. The skeleton renders only while loading is true.
Loading bar ([data-grid-loading-bar])2px track bg-primary/20 with a bg-primary sweep (static under prefers-reduced-motion)--primary. --grid-loading-sweep-duration for the sweep speed.
Custom loading state ([data-grid-loading-state])A full-width container positioned directly under the headerThe loadingState prop replaces the skeleton (zero rows) or the bar (rows present) while loading is true.
Cell error tooltiprounded-sm border border-destructive/20 bg-destructive/10 px-1.5 py-0.5 text-xs text-destructive, placed below the cell while editing--destructive.
Resize handle ([data-grid-resize-handle])8px column, a 3px after line in bg-primary revealed on hover and while [data-resizing]--primary.
Column drop indicator ([data-grid-drop-indicator])2px line in var(--color-primary) at the drag boundary--primary.
Sort indicator ([data-grid-sort-indicator])text-muted-foreground, 14px icon, multi-sort rank badge text-[10px] tabular-numsEmbed DataGridSortIndicator in a custom header. --muted-foreground.
Fill preview ([data-grid-fill-preview])pointer-events-none border border-dashed border-primary bg-primary/5. Handle: size-2 border border-background bg-primary cursor-crosshair--primary.
Presence overlay ([data-grid-presence-overlay])Border + 12% fill mixed from the user's --presence-color. Name chip rounded-sm px-1.5 py-0.5 text-xs shadow-smPer-user color via the presence API. --presence-label-foreground for the chip text. A plugin Overlay for your own layer.
Pinned-row band ([data-grid-pinned-row-band])Band separator border-border. Band cells get an opaque 30% muted tint + font-medium--muted, --border.
Built-in editorstext/number: a borderless transparent Input filling the cell. select/date: your ui/* Select/Popover/Calendar primitives with the cell chrome strippedRetheme the ui/* primitives, or pass a custom cell type.
Write pulse ([data-flash])One-shot fade from --grid-flash-intensity of --grid-flash-color to transparent. Keyframes injected by the grid--grid-flash-duration, --grid-flash-intensity, --grid-flash-color.

How deep you can go, in order of effort:

  1. Theme tokens: re-color a whole semantic group at once. Both light and dark follow.
  2. CSS variables: --grid-pin-shadow and the optional --search-highlight. The rowHeight and density props set the row height. The grid writes --grid-row-height itself.
  3. Data-attribute CSS: style interaction state (active, pinned, selected, searching, invalid) globally, with no React involved.
  4. Class merges: className on the root, getRowClassName/getCellClassName on the grid, cellClassName/headerClassName on a column. Your utilities win conflicts.
  5. Content replacement: per-column renderCell, renderHeaderCell on DataGridHeader, custom header, renderMarker, emptyState, loadingState. Replace what is drawn, not how it is styled.
  6. Plugin overlays: a plugin's Overlay member, through plugins on DataGridProvider, for your own layer in the overlay stack (see Plugins).

Data-attribute contract

The grid stamps state on the DOM as data attributes, so you can style with the Tailwind data-[attr]: variant or plain CSS without touching React. This is the stable contract for restyling interaction state. The utility classes in the table above can change between versions. The attributes do not.

AttributeWhereMeaning
data-activecellThe active (focused) cell.
data-hoveredrowThe row under the pointer. It stays on its row while the grid scrolls and moves to the row under the pointer when the scroll settles.
data-editingcellCurrently in edit mode.
data-pinned="left" | "right"cell, header, overlayWhich pin zone the element belongs to.
data-readonlycellNot editable (column or grid-wide readOnly).
data-typecellThe cell's type key (text, number, and so on), useful for type-specific overrides.
data-row-selectedmarker cellRow is in the rows selection channel (checkbox marker mode).
data-search-matchcellThe cell matches the current quick-search text.
data-flashcellThe one-shot write pulse after a committed edit.
data-invalid (+ aria-invalid)cellThe cell carries a validation error.
data-grid-pinned-rowcellThe cell belongs to a pinned-row band.
data-draggingmarker cell, header cellThat element is the reorder-drag source.
data-filteredheader cellThe column has an active filter.
data-skeletoncellThe cell is a lazy-hole or loading skeleton.
data-resizingheader cellIts column's resize handle is being dragged.
aria-sortheaderascending, descending, or none, when headerClickBehavior="sort".

Range, row, and column selection render as separate overlay elements, not cell attributes: a bg-primary/10 fill with a border-primary active-cell ring. So selecting never re-renders a cell.

Structural attributes

The grid also stamps one attribute per structural element, as a selector for the element itself (useful for targeting an add-on's chrome or debugging the DOM):

AttributeElement
data-grid-header-layerThe sticky header row layer.
data-grid-rows-canvasThe scrolling row-canvas container.
data-grid-marker-headerThe row-marker column's header (select-all checkbox).
data-grid-marker-cell / data-grid-marker-numberThe marker column's per-row cell / its row number.
data-grid-marker-checkbox / data-grid-reorder-handleThe checkbox glyph / drag handle inside a marker cell.
data-grid-resize-handle / data-grid-header-menu-triggerA column's resize handle / column-menu trigger.
data-grid-selection-handle="start" | "end" / data-grid-column-reorder-handleA touch selection handle (the corner it drags) / the header drag handle that arms a column reorder under a coarse pointer.
data-grid-header-filter-slot / data-grid-header-filter-trigger / data-grid-header-filter-popupThe header filter slot's container / its button / its popup portal.
data-grid-header-menu-popup / data-grid-header-dropdown / data-grid-context-menuThe header-menu portal / dropdown / cell context-menu content.
data-grid-sort-indicator="asc" | "desc"A header's own sort-direction arrow.
data-grid-drop-indicatorThe row- and column-reorder drop-position line.
data-grid-row-indexEvery data row (its view index).
data-grid-pin-shadow="left" | "right" | "top" | "bottom"The edge shadow of that side, visible while more content lies beyond it. See Scroll shadows.
data-scrolled-left / -right / -top / -bottomThe viewport, while scrolled away from (or still able to scroll toward) that edge.
data-grid-selection-overlay / data-grid-active-cell-overlayThe selection-range overlay(s) / the active-cell focus ring.
data-grid-loading-skeleton / data-grid-loading-bar / data-grid-empty-stateThe loading skeleton / progress bar / empty-state container.
data-grid-cell-editorA cell-type's own editor popup content (date, select).
data-grid-pinned-row-band="top" | "bottom" / data-grid-pinned-row-indexA pinned-row sticky band / a pinned row's position within its band.
data-grid-fill-preview / data-grid-fill-handleThe dashed fill-drag preview rect / its corner handle.
data-grid-presence-overlay / data-grid-presence-labelA remote user's highlight range / name chip.
data-grid-toolbar / data-grid-columns-menu / data-grid-search / data-grid-search-count / data-grid-filter-menu / data-grid-filter-count / data-grid-filter-row / data-grid-sort-list / data-grid-sort-row / data-grid-sort-countThe toolbar and its sub-widgets: columns menu, quick-search input + match badge, filter menu + active-count badge + its rows, sort list + its rows + active-sort badge.
data-grid-paginationThe data-grid-pagination add-on's footer bar container. See Pagination.
data-column-idEvery cell and header (the column's id, your value).
[data-active] { /* ring styling already applied; extend here if needed */ }
[data-type="number"] { @apply tabular-nums text-right; }
[data-search-match] { background-color: var(--search-highlight); }

Override recipes

Every built-in class in the class surface above is a Tailwind utility, generated into the utilities cascade layer (@import "tailwindcss" in app/global.css declares @layer theme, base, components, utilities;, and Tailwind's generated utilities live in that last layer). A plain CSS rule you write, with no @layer around it, is unlayered. An unlayered rule always wins over any layered rule, regardless of selector specificity or source order. So a stylesheet rule targeting one of the selectors below overrides the grid's built-in utility with no !important and no specificity fight:

/* selection range: swap the fill and border */
[data-grid-selection-overlay] {
  background-color: color-mix(in oklch, var(--color-accent) 20%, transparent);
  border-color: var(--color-accent);
}

/* active-cell ring: thicker, dashed */
[data-grid-active-cell-overlay] {
  outline: 2px dashed var(--color-ring);
  outline-offset: -2px;
}

/* marker cell: a colored left rail instead of the default background */
[data-grid-marker-cell] {
  background-color: transparent;
  border-inline-start: 2px solid var(--color-border);
}
[data-grid-marker-cell][data-row-selected] {
  border-inline-start-color: var(--color-primary);
}

/* invalid cell: a left rail instead of the inset ring */
[data-invalid] {
  box-shadow: none;
  border-inline-start: 2px solid var(--color-destructive);
}

/* search match: a solid underline instead of a background tint */
[data-search-match] {
  background-color: transparent;
  text-decoration: underline solid var(--search-highlight, currentColor) 2px;
}

Programmatic per-row/per-cell styling

Grid-level getRowClassName and getCellClassName props, on DataGrid or DataGridRoot, plus per-column cellClassName and headerClassName on ColumnDef, give value-driven styling without a new cell type or a renderCell override. All of them merge through cn() after the built-in classes, so a conflicting utility wins over the built-in one. Column-level classes apply last, so a per-column class always wins over a grid-wide default on the same utility:

// Module scope (or useCallback) — see the stable-identity callout below.
const getRowClassName: GetRowClassName<Row> = (row) => (row.status === "archived" ? "opacity-50" : undefined);
const getCellClassName: GetCellClassName<Row> = ({ value, column }) =>
  column.id === "balance" && typeof value === "number" && value < 0 ? "text-destructive" : undefined;

<DataGrid
  data={rows}
  columns={columns}
  getRowId={(r) => r.id}
  getRowClassName={getRowClassName}
  getCellClassName={getCellClassName}
/>
const columns = defineColumns<Row>()([
  {
    id: "balance",
    header: "Balance",
    accessorKey: "balance",
    type: "number",
    // string form applies to every cell in the column; the function form gets the same
    // { value, row, rowId, columnId, viewRowIndex, column } context as the grid-level getCellClassName.
    cellClassName: (ctx) => (ctx.value < 0 ? "text-destructive" : undefined),
    headerClassName: "text-right",
  },
] as const);

This complements, and does not replace, the attribute contract above (data-active, data-row-selected, data-pinned, data-type, and more) for state-based styling. It also complements renderCell for display overrides that need more than a class (see Editing & cell types).

Name
Dept.
Salary
Perf.
Status
Priya Nair
Engineering
128000
94
Active
Marcus Cole
Sales
76000
41
Active
Dana Whitfield
Support
58000
88
Active
Ilya Petrov
Engineering
141000
96
Active
Grace Owusu
Marketing
69000
47
On leave
Tom Bracewell
Sales
71000
33
Terminated
Yuki Tanaka
Engineering
118000
91
Active
Leah Fischer
Support
54000
62
Active
Omar Haddad
Marketing
73000
78
Active
Sophie Renard
Engineering
135000
45
On leave

An HR dataset shows all four styling surfaces at once, each suited to a different scope:

  • getCellClassName (per value): colors the cells of the Performance column against a threshold: below 50 tints text-destructive and bg-destructive/10, 90 and above tints text-primary and bg-primary/10.
  • getRowClassName (per row): mutes and strikes through terminated rows as a whole.
  • cellClassName (per column, string form): every Salary cell gets font-medium tabular-nums for right-aligned emphasis.
  • headerClassName (per column): the Performance header is tinted text-primary to match the semantics of the cells beneath it.

Pass a stable function identity

Pass getRowClassName and getCellClassName as stable references (module scope or useCallback; the same rule applies to columns and data. See Performance): a fresh arrow function on every render makes every row and cell re-render on every parent render, and a dev-mode console warning fires if the identity changes across renders.

Styling a whole column

Keying getCellClassName on column.id, and ignoring value and row, styles every cell in one column uniformly, for example tinting a currency column to stand out from the rest of the row:

const getCellClassName: GetCellClassName<Order> = ({ column }) =>
  column.id === "total" ? "bg-primary/5 text-right font-medium tabular-nums" : undefined;

<DataGrid {...grid} columns={columns} getCellClassName={getCellClassName} />;

Prefer cellClassName for a column-only rule

When the rule never depends on value or row, as above, the per-column cellClassName string form on that one ColumnDef (see Programmatic per-row/per-cell styling above) says the same thing with less code, with no grid-level function to keep stable. Reach for getCellClassName when the same column-targeting rule also needs to fold in value or row, for example tinting the currency column and flagging negative amounts within it.

Fully styling a column or row

The demos above tint or dim. A "fully styled" column or row goes further, with background, border, and text treatment together, still with no separate CSS file and no forked component:

const columns = defineColumns<Deal>()([
  // ...
  {
    id: "region",
    header: "Region",
    accessorKey: "region",
    type: "text",
    // Constant across every cell/header in the column — background tint, a separating border,
    // and a bold/tinted header treat the whole column as a distinct "brand" band.
    cellClassName: "bg-primary/5 border-r border-r-primary/30 font-medium",
    headerClassName: "bg-primary/10 border-r border-r-primary/30 font-semibold text-primary",
  },
] as const);

const getRowClassName: GetRowClassName<Deal> = (row) =>
  row.stage === "at-risk"
    ? "border-l-2 border-l-destructive bg-destructive/5 text-destructive"
    : undefined;

<DataGrid {...grid} columns={columns} getRowClassName={getRowClassName} />;
Account
Region
Owner
Amount
Stage
Northwind Traders
AMER
Priya Nair
84000
Committed
Fabrikam Group
EMEA
Marcus Cole
41500
At risk
Contoso Retail
AMER
Dana Whitfield
120000
Committed
Tailspin Toys
APAC
Ilya Petrov
27800
Prospecting
Wide World Importers
EMEA
Grace Owusu
63200
At risk
Adatum Corp
APAC
Tom Bracewell
95400
Committed
Lucerne Publishing
AMER
Yuki Tanaka
18900
Prospecting
Proseware Inc
EMEA
Leah Fischer
52700
At risk

The Region column is fully styled through the per-column cellClassName and headerClassName string form: background, header treatment, and border together, with no per-row logic needed and no grid-level function to keep stable. At-risk deals get a full-row treatment through getRowClassName that goes beyond a background tint: border, background, and text color on every cell in the row at once. The density selector of the same demo is covered in Density and row height below.

Styling by state with CSS only

The data-attribute contract above needs no getCellClassName or getRowClassName at all: pure CSS, keyed off attributes that the grid already sets on every render. This is the right tool when the styling is driven by interaction state (active, pinned, selected), not by row or column data:

/* global.css, or any stylesheet loaded after the grid's base styles */
[data-active] {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}
[data-pinned="left"] {
  border-inline-end: 1px solid var(--color-border);
}
[data-row-selected] {
  background-color: color-mix(in oklch, var(--color-primary) 10%, var(--color-background));
}

Or use the Tailwind data-[attr]: variant form inline on a wrapper, if you prefer not to add a stylesheet rule:

<DataGridRoot className="**:data-[pinned='right']:border-l-2 **:data-[pinned='right']:border-l-primary/40">

Small screens

The grid scrolls sideways inside its own box, so a wide table never widens the page. Give the box a width of 100% and a height, and the columns keep their widths. The toolbar is a single row that scrolls sideways too. To let its buttons wrap onto a second line on a narrow screen, pass flex-wrap:

<DataGridToolbar className="flex-wrap">
  <DataGridSearch />
  <DataGridFilterMenu />
</DataGridToolbar>

Columns with flex share the free width. Give a column a minWidth to stop it from shrinking below a readable size.

Density and row height

density (compact, default, or comfortable) and the rowHeight override are themselves a styling surface. They resize every data row through the single --grid-row-height CSS variable (see CSS variables above), with no getRowClassName needed:

const [density, setDensity] = useState<DensityMode>("default");

<DataGridRoot density={density}>

The demo above wires this to a Select. Switching it live re-measures --grid-row-height for every mounted row, with no re-render of cell content, only the row track size. Pass an explicit rowHeight={32} instead of density when you need a value that the three presets do not cover. It always wins over density when both are set.

The header row does not follow density. Set the height of one header row with the headerHeight prop (default 36). Column groups add header rows of the same height:

<DataGridRoot rowHeight={40} headerHeight={44} />

Row and header heights are props, not CSS variables. The grid needs the numbers to compute the visible rows, and it writes --grid-row-height itself. The inline value wins over a CSS rule. An !important override resizes the rows but not the window of mounted rows, so the grid shows gaps. Pass the height as a prop.

Scroll shadows

The grid shows a soft shadow on each side where more content lies beyond the visible area: at the right edge while more columns follow, and at the bottom edge while more rows follow. After you scroll, the left and top shadows appear too. The top shadow sits under the header. When a column or row band is pinned, the shadow on that side sits at the band boundary instead.

Configure the shadows with CSS variables. Set them on the grid or on any ancestor:

VariableEffectDefault
--grid-scroll-shadowcolor of the edge shadows. transparent hides them, and keeps the shadows at pinned bands--grid-pin-shadow
--grid-pin-shadowcolor of the shadows at pinned bandsset by the CLI for light and dark mode
--grid-shadow-sizewidth of the left and right shadows, and height of the top and bottom shadows8px

To hide the edge shadows and keep the pinned-band shadows:

.my-grid {
  --grid-scroll-shadow: transparent;
}

For a full redesign, select the shadow elements. Each one has data-grid-pin-shadow with its side, and a shadow at a pinned band also has data-pinned:

/* edge shadows only, right side */
[data-grid-pin-shadow="right"]:not([data-pinned]) {
  background-image: linear-gradient(to left, var(--primary), transparent);
}

Loading state

Toggle both switches: Loading with no rows shows a viewport-filling skeleton, Loading with rows present shows a slim bar under the header instead, and rows-off + loading-off is the ordinary empty state.

Name
Email
Role
Score
Silent Tiger
user0@example.com
Viewer
4
Quick Lion
user1@example.com
Viewer
89
Swift Wolf
user2@example.com
Manager
80
Eager Lion
user3@example.com
User
51
Eager Tiger
user4@example.com
Viewer
83
Silent Wolf
user5@example.com
User
15
Swift Lion
user6@example.com
Viewer
62
Eager Lion
user7@example.com
Manager
42
Eager Eagle
user8@example.com
Editor
35
Bold Tiger
user9@example.com
Editor
52

loading (on DataGrid or DataGridRoot) is a presentational flag for an ordinary "fetch then render" consumer that has no other built-in way to show the grid as busy. Without it, a grid with zero rows renders the empty state, which reads as "your query matched nothing" while the request is still in flight:

const [loading, setLoading] = useState(true);

<DataGrid data={rows} columns={columns} getRowId={(r) => r.id} loading={loading} />
  • Zero rows: a viewport-filling skeleton (muted animate-pulse bars, one per column) replaces the empty state entirely. The emptyState prop or label never shows while loading is true.
  • Rows already present (a background refresh): the rows stay visible, and a slim indeterminate bar appears pinned under the header instead, so the grid does not blank out mid-session.
  • aria-busy="true" is set on the grid root either way. The skeleton region and the progress bar both use labels.grid.loading ("Loading…" default) as their aria-label. See i18n.
  • The sweep of the progress bar respects prefers-reduced-motion. Reduced motion renders a static full-width bar instead of an animated one, with no JS or rAF involved either way.

The default is false, so existing consumers who never pass it see no change in behavior.

data-grid-lazy has its own per-range skeletons

loading is a whole-grid flag with one look. It is unrelated to the per-row skeleton cells of data-grid-lazy for individually unloaded rows (see Lazy loading). Use loading for an ordinary fully-controlled fetch, and the lazy add-on when you stream in row windows on scroll.

i18n via labels

Every user-facing string, in the core and in every add-on, lives in one typed DataGridLabels object with English defaults. gridcn deep-merges it when you pass a partial override. See the dedicated i18n page for the merge model, per-group coverage, and a full translation example.

Visual defaults

Selected rows and columns highlight across their full extent, not only the range rectangle. An edge shadow appears on each side where more content lies beyond the visible area. It sits at the grid edge or at the boundary of a pinned column or row band. Hover, focus rings, and border rhythm follow the shadcn base style that you initialized with (the style in components.json). There is no gridcn-specific design layer on top to override it.

The grid's editors and menus are built from your project's own ui/* primitives (Input, Select, Popover, Calendar, and more), so re-theming those primitives restyles the grid too. If your shadcn theme supports dark mode, the grid does too, with no separate dark-mode configuration.

On this page