Skip to main content

Theming and icons

Themes are immutable per-grid values. Semantic parameters fan out to related CSS variables, while a typed vars escape hatch handles precise customization.

Loading interactive example…
React
const theme = themeDark.withParams({
accentColor: "#2fd2e2",
backgroundColor: "#0a172b",
headerBackgroundColor: "#0f2140",
rowHeight: 40,
spacing: 10,
});

<Grid
rowData={rows}
columnDefs={columns}
theme={theme}
zebraRows
columnHover
highlightActiveCell
/>

Semantic parameters​

Start with themeLight or themeDark, then call withParams(). Common parameters cover accent, surfaces, text, borders, hover/selection colors, typography, row height, padding, scrollbars, aggregates, and sparklines.

import { themeLight } from "@agility-workbench/grid";

const theme = themeLight.withParams({
accentColor: "#7c3aed",
backgroundColor: "#ffffff",
headerBackgroundColor: "#f5f3ff",
rowHeight: 42,
spacing: 10,
fontFamily: "Inter, sans-serif",
});

const options = { theme } satisfies GridOptions;

rowHeight and headerHeight are the two layout params: rows and header levels are sized in JavaScript, so the grid reads the theme's values when it is created and uses them as the row height and the header-level height. They take pixels (42 or "42px"); an explicit rowHeight / headerHeight grid option wins over them (and leafHeaderHeight / parentHeaderHeight fall back to the resolved header height). Swapping the theme on a mounted grid resizes neither the rows nor the header.

One parameter derives rather than fans out: findMatchColor turns a single color into the quick filter's three find-highlight variables — the tint on every matching cell (35% alpha), the stronger tint on the active match (65%), and that match's outline (the color as given).

const theme = themeLight.withParams({ findMatchColor: "#38bdf8" });

Because it reads the color's channels, it accepts hex or rgb() / rgba() only. A named color or another color space is ignored with a console warning rather than guessed at — set the three variables through vars for those, or to choose the alphas yourself.

Themes apply inline to one grid and its portaled popups, so two grids on one page can look different. vars overrides semantic fan-out for any individual --pte-* variable:

const theme = themeDark.withParams({
accentColor: "#22d3ee",
vars: {
"--pte-scrollbar-thumb-color": "#475569",
"--pte-selected-bg-color": "#164e63",
},
});

Icons​

Icons accept a URL, data URI, CSS image value, or inline SVG. Theme icons form the base; the grid-level icons map wins for matching names.

const options = {
icons: {
filter: "<svg viewBox='0 0 24 24' aria-hidden='true'>…</svg>",
export: "/icons/download.svg",
},
} satisfies GridOptions;

Stylesheet delivery and CSP​

The base stylesheet is injected automatically — the first attached grid injects it once per document or shadow root. Under a strict Content Security Policy, supply the page's nonce:

const options = { styleNonce: cspNonce } satisfies GridOptions;

// Or inject explicitly, e.g. into a shadow root:
import { areGridStylesInjected, injectGridStyles } from "@agility-workbench/grid";
injectGridStyles(document, { nonce: cspNonce });

Popups outside the grid root​

Menus, the column panel and default (in-root) tooltips and ActionFrame popovers live inside the grid root, so the theme reaches them by ordinary inheritance. A few layers must leave the root: the column drag ghost (it follows the pointer across the page), and tooltips and ActionFrame popovers with escapeRootClip: true. Those are styled exactly as if they were inside the root:

  • Theme. The grid's inline --pte-* variables — from themeDark, createTheme({...}) params, withParams, raw vars and icon overrides alike — are mirrored onto the portaled element while it is mounted, and kept in sync when the theme changes.
  • Shadow DOM. A grid mounted inside a shadow root has its stylesheet delivered into that shadow root, not the page. Its portaled layers therefore mount in the same shadow root rather than document.body, so they are styled and nothing grid-related leaks into the page.

One trade-off under a shadow root: the layer is position: fixed inside the shadow tree, so it escapes every overflow clip (the grid root's and the host's included) but not a transform, filter or contain on the shadow host or one of its ancestors — those make the ancestor the containing block — and its z-index stacks within the host's stacking context. Avoid transforms on the host if you rely on escapeRootClip.

Import @agility-workbench/grid/styles.css and set suppressStyleInjection only when your build pipeline needs to own CSS delivery — opting out avoids two copies of the stylesheet competing in the cascade:

import "@agility-workbench/grid/styles.css";

const options = { suppressStyleInjection: true } satisfies GridOptions;