Lattice Grid Buy a licence

developer guide

Data Grid Theming with CSS Custom Properties

The grid exposes its colours, spacing and type as custom properties, so a theme is a block of values rather than a pile of overrides. Four themes ship, the dark one can follow the operating system, and your own is the same tokens with different values.

Developer guide › Theming and density › Data Grid Theming with CSS Custom Properties

Styling

Cells, columns and rows can all carry classes and inline styles, static or computed.

By scope

// Cells and columns: declared on the column
{ field: 'margin', cell: {
  class: 'tabular',
  classWhen: { 'is-loss': (p) => p.value < 0 },
  style: (p) => ({ fontWeight: p.value > 1e6 ? 650 : 400 }),
}}

// Rows: on the grid
rowClass: (p) => p.data.slaBreached ? 'row-breach' : null,
rowStyle: (p) => p.data.region === 'AMER' ? { borderLeft: '3px solid #7c3aed' } : null,

All of these are re-evaluated on every repaint and remove what they added last time first. That is not caution. Rows and cells come from pools, so an element that carried a class for one row will later carry a different row, a class written once and left alone smears down the grid as the user scrolls.

Theming

The stylesheet is custom properties throughout. Override the tokens, not the rules. Every public token, its default in each theme, what it affects and whether a preset must set it, may set it, or should leave it alone is in the theme token contract.

A house palette

.lattice {
  --lattice-accent: #7c3aed;
  --lattice-font-size: 13px;
  --lattice-space: 8px;
  --lattice-border-color: #e6e8eb;
}

Four themes ship. Density is separate: compact, standard, comfortable or spacious, and combines with any of them.

One switch, every module

Dark is one decision for a whole screen, not one per widget. Any of three switches makes the grid and every module the package ships dark together: the grid body, header, rows, selection, editors, menus, tool panels and filter row; layout window chrome, title bars, minimised windows and window bodies; chart plots, axes, legends, ramp legends, tooltips and the annotation rail; KPI tiles and their bands; tab strips; kanban columns and cards; gantt bars, labels and grid lines.

SwitchWhat it does
data-theme="dark" on any ancestorOn <html>, a dashboard container, anything. Everything below it that has no theme of its own turns dark.
theme: 'dark' in a grid's or a layout's configWritten to data-theme on that grid's or layout's own root, so a layout's theme reaches every window in it and everything mounted there.
theme: 'auto' (or data-theme="auto")Follows the viewer's prefers-color-scheme, live.

Precedence: the nearest data-theme wins. A grid's or layout's own theme is written on its own root, so it beats anything above it: theme: 'light' keeps a grid light inside a dark page, and a dark layout inside a light page is dark. With no theme and no themed ancestor the grid is light, as it has always been; a page that never asks does not follow the desktop: that is what 'auto' is for. Charts, KPI panels, tab strips, kanban boards and gantt charts have no theme of their own: they inherit it, because their colours are the grid's --lattice-* tokens. The alarms module draws nothing, so there is nothing to theme; its wall is a grid. The tokens live in lattice-grid.css, so load it on any page that uses a module, even one with no grid.

A dark dashboard, one line

createLayout(el, { theme: 'dark', windows });   // or <html data-theme="dark">
createGrid(layout.payload('orders'), { rows, columns }); // dark, with no theme of its own

Pinning a theme, at build time or at runtime

createGrid(el, { theme: 'high-contrast' });
grid.set('theme', 'terminal');
grid.set('theme', null);   // back to following the page
ThemeWhat it is for
lightThe default, when nothing above the grid says otherwise.
darkThe same palette inverted, with the accent and status hues re-picked for a dark ground rather than reused.
high-contrastNot "dark with more contrast". Text is 21:1 and borders 6.1:1 against the background, where the other themes sit near 1.3:1 on borders, WCAG 1.4.11 asks for 3:1 on the boundaries a user has to find. Cell borders are drawn rather than implied, selected rows carry an outline as well as a fill, and every status pill has a solid border so it does not depend on hue alone.
terminalA phosphor console: one hue on near-black, monospaced throughout. Status is carried by brightness rather than colour, so the palette stays a palette.

Design-system presets

modules/presets ships two routes onto the same token contract: static variable files - material3.css and fluent2.css - and themeFromMui(theme), which drives the contract from a real MUI theme. See the presets reference for all three.

Forced colours

Windows High Contrast Mode replaces the palette outright: that is the point of it, and no stylesheet should fight it. What the grid does instead is translate every piece of meaning it normally carries in a background tint into something the mode preserves.

A selected row takes the system's own selection colours. A pinned region loses its shadow, which forced colours do not render, and gains a rule in its place. Status pills, fill decorations, progress tracks and histogram bars each gain a border, because a fill with no edge is invisible once its colour is discarded. Diff states stop depending on hue altogether: added, removed and changed are told apart by border style: solid, dashed and doubled : since the mode offers no way to keep four distinct colours.

Two things deliberately keep their colour, declared with forced-color-adjust: a colour swatch, where the colour is the value being shown, and a collaborator's presence colour, which is how one person is told from another. Replacing those would destroy the meaning rather than translate it. Both gain a border so they stay visible against either ground.

Every theme is the same token set with different values, so an override you write against .lattice applies to all of them, and one written against .lattice[data-theme="terminal"] applies to that one. theme writes the attribute on the grid's own root element; a dark or light theme set on an ancestor instead is honoured too, and the nearest one wins.

The grid and your page's CSS

Every selector in the stylesheet is namespaced under .lattice, so the grid cannot restyle your page. From 1.4.0 the reverse is also true: the grid gives the elements it builds a floor for the properties a page is most likely to set on a bare tag: margin, padding, border, radius, background, shadow, text transform and letter spacing, plus type and colour on form controls, which inherit neither.

Why this is needed at all. A grid is mounted inside somebody else's stylesheet. A rule as ordinary as section { padding: 5.5rem 0 }, a marketing page, a CMS theme, a Tailwind preflight: matches by tag name, and the grid builds parts of its own interface from those tags: the tool panel's filter rows are <section> elements. Without the reset, 5.5rem of somebody else's padding lands on every one of them.

The reset uses no !important. It is specificity (0,1,1) and every rule that dresses a grid element is (0,2,0) or higher, so the grid's own styling always wins and the reset only fills a gap. Yours wins too, on the same terms: a rule aimed at a Lattice class, .lattice .lat-cell { … }: outranks it, so overriding the grid deliberately works exactly as before. Only bare-tag rules are shut out.

It touches box model and decoration only. Nothing in it sets display, position or any dimension: those belong to the renderer, and a reset that reached them would break virtualisation rather than protect it.

hidden on a host

hidden works on every viewer host. The grid, the kanban board, the calendar, the KPI panel, the Gantt, the tab strip and the dashboard layout all honour the HTML hidden attribute - and any [hidden] ancestor - on the element you mount them into: a hidden host paints nothing and takes no space. Show it again and each viewer re-measures through its own resize observer (or plain CSS reflow), so no refresh() call is needed to draw at the new size.