Theming
Try it live — the theme and density pickers above the grid switch every combination on this page.
Two props:
<DataGrid
dataSource={rows}
rowKey="id"
columns={columns}
theme="quartz"
density="compact"
dark={isDark}
/>
Both are optional. Omit theme and the grid renders exactly as it always
has — the packaged look is not one of the six, it is the default, and it is
not going to move under you.
Switching is a class swap on the grid root, so it repaints without rebuilding anything: no re-render of a cell, no reload, and nothing to configure. Bind it to a user preference and it changes as fast as the user clicks.
The six
theme | Character |
|---|---|
quartz | Minimal chrome — no column rules, hairline row rules, roomy radii. For product UIs where the grid sits among cards. |
balham | Dense and fully ruled — small type, tight padding, near-square corners. For the case where the grid is the application. |
material | Tall rhythm, rows-only rules, a medium-weight tracked header, pill controls and real elevation. |
alpine | Airy but fully framed, with a doubled rule anchoring the header band. |
kanuni | The KanuniLabs house theme: an ink header carrying the brand gold, over a warm near-white body. |
kanuni-midnight | The house theme in dark. Already dark — it ignores the dark flag. |
Every theme ships a light and a dark palette. dark picks between them, so
one flag drives all six:
<DataGrid theme="material" dark={prefersDark} … />
Nothing here is a third-party stylesheet. The palettes, metrics and line work are ours; the names are the shared vocabulary for a grid's chrome and density, in the same way a typeface gets called a grotesque.
Palettes
A second, independent axis. The theme decides the structure — where the rules are drawn, how tall a row is, how round a control is. A palette decides only the colours:
<DataGrid theme="balham" palette="nord" dark={isDark} … />
palette | Character |
|---|---|
nord | Arctic, desaturated blue-grey. The calmest — a dense grid of numbers does not vibrate. |
dracula | Near-black violet with high-chroma purple and pink. |
solarized | The fixed-lightness classic, designed as a matched light/dark pair. |
gruvbox | Warm retro, high contrast — the only genuinely warm light mode here. |
catppuccin | Pastel, with very close surface tones. Easy over a long reading session. |
tokyo-night | Deep navy with violet and cyan; the most saturated dark. |
The two multiply: theme="balham" palette="nord" is Balham's dense ruled grid
in Nord's arctic blues, and theme="material" palette="nord" is Material's tall
airy one in the same blues. Six themes and six palettes is thirty-six looks out
of twelve class names. A palette on its own recolours the packaged default.
Each ships a light and a dark set, so dark keeps working across all of them.
These are the colour schemes people already know from their editors. The hues are theirs; the mapping onto the grid's tokens — which colour becomes the header, the stripe, the accent, the thing that has to stay readable on the accent — and the contrast work that mapping needs are ours. No third-party stylesheet is involved. Where a scheme publishes only a dark set, the light counterpart is our own reading of the same hue family, and the stylesheet says so at that block.
Density
density is 'compact' | 'normal' | 'comfortable', and it is not a font-size
switch. Row height, header height, cell padding, control heights, toolbar and
pager padding and the filter field all move together, so the proportions hold:
compact | normal | comfortable | |
|---|---|---|---|
balham | 20px rows, 11px type | 25px, 12px | 32px, 13px |
quartz | 26px, 12px | 36px, 13px | 44px, 14px |
material | 32px, 13px | 40px, 14px | 52px, 15px |
Each theme scales from its own normal rather than a shared table — Material starts at 40px and Balham at 25px, and one common compact value would have flattened the presets into each other at that density.
An explicit rowHeight still wins over both props. Density is a shorthand, not
an override.
Charts follow the theme
When a theme is set, the chart panel derives its series colours from that theme's accent instead of a fixed palette — so a Material grid does not draw its first bar in someone else's blue. The first series is the accent; the rest are hue rotations of it that keep its saturation and lightness, which is what makes a dark theme's series come out light enough for a dark canvas.
The colours are resolved to literals rather than left as var(), for two
reasons that both bite: recharts writes the colour as an SVG presentation
attribute, which does not support var(), and PNG/SVG export serializes the
chart into a standalone file with no stylesheet behind it.
Without a theme, the packaged eight are used, unchanged.
Writing your own
A theme here is a set of CSS custom property overrides and nothing else.
Everything the stylesheet draws with — every colour, metric, shadow and border —
is read through a --dg-* property, so a theme never restyles a component and
can never fall behind one. Yours is written the same way:
.my-grid-theme {
--dg-surface: #fffdf8;
--dg-surface-header: #f4efe4;
--dg-border: #e4dccb;
--dg-text: #1b1811;
--dg-primary: #8a6d2f;
/* What sits ON the accent. Set it: white on a light accent is unreadable,
and this is the token that decides a chip label and a primary button. */
--dg-on-primary: #ffffff;
--dg-row-height: 30px;
--dg-header-height: 34px;
/* A whole shorthand, so a theme can drop a rule entirely rather than fade it. */
--dg-cell-border: none;
}
<DataGrid className="my-grid-theme" … />
One caveat with --dg-row-height: the virtualizer computes every row offset and
the scroll extent from the row height, so it has to reach the engine as a
number. Set rowHeight alongside your CSS, or use density, and the two stay
in agreement.
Where the tokens have to be declared
Popups are portalled to <body>, so they are not descendants of the grid and
cannot inherit your class through the cascade. Declare the block on all three
roots the packaged themes use, or the filter popup and the dialogs open in the
default palette:
.kanuni-datagrid-root.my-grid-theme,
.kanuni-datagrid-enterprise.my-grid-theme,
.kanuni-datagrid-detached.my-grid-theme { … }
That is also why theme is a prop rather than something you set yourself: the
component forwards it to every detached surface for you.
The tokens
Surfaces — --dg-bg, --dg-surface, --dg-surface-hover,
--dg-surface-stripe, --dg-surface-header, --dg-group-bg, --dg-footer-bg,
--dg-selected-bg, --dg-overlay
Header band — --dg-header-bg, --dg-header-text,
--dg-header-text-muted, --dg-header-hover, --dg-header-weight,
--dg-header-transform, --dg-header-tracking, --dg-header-font-size
The header is deliberately separate from --dg-surface-header: they default to
the same value, but an ink header still wants a light group panel, filter row
and footer.
Text and accent — --dg-text, --dg-text-muted, --dg-primary,
--dg-primary-soft, --dg-on-primary, --dg-focus-ring, --dg-danger,
--dg-danger-text, --dg-on-danger, --dg-warning
Line work — --dg-border, --dg-border-strong, --dg-cell-border,
--dg-row-border, --dg-header-cell-border, --dg-header-border
Metrics — --dg-row-height, --dg-header-height, --dg-cell-padding-x,
--dg-font-size, --dg-radius, --dg-radius-control, --dg-control-height,
--dg-control-font-size, --dg-filter-input-height, --dg-chrome-font-size,
--dg-chrome-padding-y
Elevation — --dg-shadow-popup, --dg-shadow-popup-sm,
--dg-shadow-menu, --dg-shadow-card, --dg-shadow-panel,
--dg-shadow-dialog, --dg-shadow-pinned, --dg-shadow-pinned-right
Scrollbar — --dg-scrollbar-thumb, --dg-scrollbar-track. Only applied
when a theme is set; an unthemed grid keeps the platform scrollbar.
Contrast
Every packaged theme and palette clears WCAG AA (4.5:1) on the surfaces that carry text, measured rather than assumed, and the floor is pinned by a test so a colour cannot be nudged for looks and quietly drop below it.
Three palettes needed a correction to get there, which is the point of measuring rather than trusting the source scheme: Nord's muted red left its own label at 4.31:1, Gruvbox's light yellow sits at 3.33:1 on its cream ground, and Tokyo Night Day's keyword blue is a keyword colour rather than a body ink and measured 4.03:1 on the header. Each was moved within its own hue family.
--dg-on-primary is the one to be careful with in a theme of your own. It is
what sits on the accent — a group chip's label, a primary button's text — and
the house theme is the case that proves it: the KanuniLabs gold is #c5a55a,
and white on it measures 1.9:1. Set the token, do not inherit it.
Cost
None on the render path. The browser resolves custom properties during style computation, so a themed grid does no more work per cell than an unthemed one, and there is no per-cell React work of any kind — the theme is one class on the root, composed once per grid.