Documentation

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

themeCharacter
quartzMinimal chrome — no column rules, hairline row rules, roomy radii. For product UIs where the grid sits among cards.
balhamDense and fully ruled — small type, tight padding, near-square corners. For the case where the grid is the application.
materialTall rhythm, rows-only rules, a medium-weight tracked header, pill controls and real elevation.
alpineAiry but fully framed, with a doubled rule anchoring the header band.
kanuniThe KanuniLabs house theme: an ink header carrying the brand gold, over a warm near-white body.
kanuni-midnightThe 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} … />
paletteCharacter
nordArctic, desaturated blue-grey. The calmest — a dense grid of numbers does not vibrate.
draculaNear-black violet with high-chroma purple and pink.
solarizedThe fixed-lightness classic, designed as a matched light/dark pair.
gruvboxWarm retro, high contrast — the only genuinely warm light mode here.
catppuccinPastel, with very close surface tones. Easy over a long reading session.
tokyo-nightDeep 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:

compactnormalcomfortable
balham20px rows, 11px type25px, 12px32px, 13px
quartz26px, 12px36px, 13px44px, 14px
material32px, 13px40px, 14px52px, 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.