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.
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:
| Token | Surfaces it paints |
|---|---|
background | The root frame and every data cell's base fill, and the marker cells. |
border | The root frame border, the bottom border of every cell, header, and marker cell, the pinned-row band separator, and skeleton row separators. |
muted | Header 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-foreground | The empty-state text, marker row numbers, the sort indicator, the row-reorder drag handle, and the column-menu trigger. |
accent / accent-foreground | The column-menu trigger hover. |
primary | The 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%). |
destructive | Invalid 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 therowHeightprop or thedensitypreset (compact28px,default36px,comfortable44px). 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.transparenthides the lines.--grid-scroll-shadowand--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 theprimaryselection tint) to set your own color. A color set only on--color-search-highlightalso works, and--search-highlightwins when both exist. The[data-search-match]attribute is stamped either way.--presence-color: internal to thedata-grid-presenceadd-on: each remote user's highlight overlay and name chip are mixed from that user's color, set through the presence API, not CSS.
| Variable | Effect | Default |
|---|---|---|
--presence-label-foreground New | text color of a presence name chip | black or white, derived from the user color |
--grid-flash-duration | duration of the post-commit write pulse. 0s disables the pulse | 1.2s |
--grid-flash-intensity | color-mix() percentage of --grid-flash-color at the pulse's start | 24% |
--grid-flash-color | the pulse's tint color | var(--color-primary) |
--grid-loading-sweep-duration | duration of the loading bar's sweep animation | 1.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 classes | Override point |
|---|---|---|
Grid root ([role="grid"]) | relative select-none overflow-auto rounded-md border border-border bg-background text-sm | className 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 line | Per-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 ring | getRowClassName/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 tint | renderMarker 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 line | Internal. |
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-shadow | Override --grid-pin-shadow per theme. |
| Column line | 1px 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-foreground | The 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-border | The 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 header | The loadingState prop replaces the skeleton (zero rows) or the bar (rows present) while loading is true. |
| Cell error tooltip | rounded-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-nums | Embed 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-sm | Per-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 editors | text/number: a borderless transparent Input filling the cell. select/date: your ui/* Select/Popover/Calendar primitives with the cell chrome stripped | Retheme 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:
- Theme tokens: re-color a whole semantic group at once. Both light and dark follow.
- CSS variables:
--grid-pin-shadowand the optional--search-highlight. TherowHeightanddensityprops set the row height. The grid writes--grid-row-heightitself. - Data-attribute CSS: style interaction state (active, pinned, selected, searching, invalid) globally, with no React involved.
- Class merges:
classNameon the root,getRowClassName/getCellClassNameon the grid,cellClassName/headerClassNameon a column. Your utilities win conflicts. - Content replacement: per-column
renderCell,renderHeaderCellonDataGridHeader, customheader,renderMarker,emptyState,loadingState. Replace what is drawn, not how it is styled. - Plugin overlays: a plugin's
Overlaymember, throughpluginsonDataGridProvider, 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.
| Attribute | Where | Meaning |
|---|---|---|
data-active | cell | The active (focused) cell. |
data-hovered | row | The 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-editing | cell | Currently in edit mode. |
data-pinned="left" | "right" | cell, header, overlay | Which pin zone the element belongs to. |
data-readonly | cell | Not editable (column or grid-wide readOnly). |
data-type | cell | The cell's type key (text, number, and so on), useful for type-specific overrides. |
data-row-selected | marker cell | Row is in the rows selection channel (checkbox marker mode). |
data-search-match | cell | The cell matches the current quick-search text. |
data-flash | cell | The one-shot write pulse after a committed edit. |
data-invalid (+ aria-invalid) | cell | The cell carries a validation error. |
data-grid-pinned-row | cell | The cell belongs to a pinned-row band. |
data-dragging | marker cell, header cell | That element is the reorder-drag source. |
data-filtered | header cell | The column has an active filter. |
data-skeleton | cell | The cell is a lazy-hole or loading skeleton. |
data-resizing | header cell | Its column's resize handle is being dragged. |
aria-sort | header | ascending, 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):
| Attribute | Element |
|---|---|
data-grid-header-layer | The sticky header row layer. |
data-grid-rows-canvas | The scrolling row-canvas container. |
data-grid-marker-header | The row-marker column's header (select-all checkbox). |
data-grid-marker-cell / data-grid-marker-number | The marker column's per-row cell / its row number. |
data-grid-marker-checkbox / data-grid-reorder-handle | The checkbox glyph / drag handle inside a marker cell. |
data-grid-resize-handle / data-grid-header-menu-trigger | A column's resize handle / column-menu trigger. |
data-grid-selection-handle="start" | "end" / data-grid-column-reorder-handle | A 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-popup | The header filter slot's container / its button / its popup portal. |
data-grid-header-menu-popup / data-grid-header-dropdown / data-grid-context-menu | The header-menu portal / dropdown / cell context-menu content. |
data-grid-sort-indicator="asc" | "desc" | A header's own sort-direction arrow. |
data-grid-drop-indicator | The row- and column-reorder drop-position line. |
data-grid-row-index | Every 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 / -bottom | The viewport, while scrolled away from (or still able to scroll toward) that edge. |
data-grid-selection-overlay / data-grid-active-cell-overlay | The selection-range overlay(s) / the active-cell focus ring. |
data-grid-loading-skeleton / data-grid-loading-bar / data-grid-empty-state | The loading skeleton / progress bar / empty-state container. |
data-grid-cell-editor | A cell-type's own editor popup content (date, select). |
data-grid-pinned-row-band="top" | "bottom" / data-grid-pinned-row-index | A pinned-row sticky band / a pinned row's position within its band. |
data-grid-fill-preview / data-grid-fill-handle | The dashed fill-drag preview rect / its corner handle. |
data-grid-presence-overlay / data-grid-presence-label | A 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-count | The 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-pagination | The data-grid-pagination add-on's footer bar container. See Pagination. |
data-column-id | Every 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).
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 tintstext-destructiveandbg-destructive/10, 90 and above tintstext-primaryandbg-primary/10.getRowClassName(per row): mutes and strikes throughterminatedrows as a whole.cellClassName(per column, string form): every Salary cell getsfont-medium tabular-numsfor right-aligned emphasis.headerClassName(per column): the Performance header is tintedtext-primaryto 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} />;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:
| Variable | Effect | Default |
|---|---|---|
--grid-scroll-shadow | color of the edge shadows. transparent hides them, and keeps the shadows at pinned bands | --grid-pin-shadow |
--grid-pin-shadow | color of the shadows at pinned bands | set by the CLI for light and dark mode |
--grid-shadow-size | width of the left and right shadows, and height of the top and bottom shadows | 8px |
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.
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-pulsebars, one per column) replaces the empty state entirely. TheemptyStateprop or label never shows whileloadingis 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 uselabels.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.