Themes

← Docs index

Pass a theme object to override any default value. → Live light/dark example

const grid = new JHGrid({
  container: '#grid',
  fetchMeta,
  fetchData,
  theme: {
    headerBg:       '#1e3a5f',
    headerText:     '#ffffff',
    rowEven:        '#ffffff',
    rowOdd:         '#f0f4ff',
    cellText:       '#111827',
    scrollbarThumb: '#6b7280',
  },
});

Grid re-themed with dark canvas tokens

A fuller dark theme. The canvas tokens colour the grid itself and the overlay* tokens colour the filter panel, menus, dialogs and editors, so a dark grid usually sets both. Everything not listed keeps its default:

const darkTheme = {
  // the grid
  headerBg: '#1f2937', headerText: '#e5e7eb', headerBorder: '#374151',
  rowEven: '#111827', rowOdd: '#0f172a', cellBorder: '#1f2937', cellText: '#e5e7eb',
  loadingText: '#6b7280', skeletonBar: '#1f2937', skeletonSheen: 'rgba(255,255,255,0.06)',
  selectionColor: '#60a5fa', selectionFill: 'rgba(96,165,250,0.15)', selRowBg: 'rgba(96,165,250,0.18)',
  hoverRowBg: 'rgba(255,255,255,0.055)',            // a light wash: the default is dark
  scrollbarBg: '#111827', scrollbarThumb: '#4b5563', frozenBorder: '#4b5563',
  readonlyCellBg: '#1e293b',
  // panels, menus, dialogs, editors
  overlayBg: '#1f2937', overlayBorder: '#374151', overlayHeaderBg: '#111827', overlayDivider: '#374151',
  overlayText: '#f9fafb', overlayMutedText: '#9ca3af', overlayHintText: '#9ca3af',
  overlayHoverBg: '#374151', overlayItemHoverBg: '#374151',
  overlayAccentText: '#0b1220',                    // text on the light-blue Apply button
  pagerBg: '#111827', pagerText: '#e5e7eb', pagerBorder: '#374151',
  pagerButtonBg: '#1f2937', pagerButtonBorder: '#4b5563',
};

new JHGrid({ container: '#grid', data: rows, theme: darkTheme });

theme is read when the grid is created. To switch themes at runtime, destroy() the grid and build a new one with the other object (the live demo does exactly that).

Full Theme Reference

Canvas tokens

Everything below is painted directly on the <canvas>: the grid body, header, selection, scrollbars, and cell state indicators. theme is the only way to set most of them; two (selectionColor, fontFamily) are also bridged to a --jhg-* CSS variable because DOM overlays (the filter panel’s “Apply” button, cell editors) borrow them for visual consistency with the canvas; see the note under the table below.

Key Default Description
headerBg #E8E8E8 Header background color
headerText #3D3D3D Header text color
headerBorder #C0C0C0 Header column separator color
rowEven #FFFFFF Even row background
rowOdd #FFFFFF Odd row background
cellBorder #D9D9D9 Cell border color
cellText #212121 Cell text color
loadingText #BFBFBF Text color while chunk is loading
skeletonBar #E4E7EB Bar drawn in a cell whose row hasn’t arrived yet, instead of leaving it blank
skeletonSheen rgba(255,255,255,0.65) The lighter band that sweeps across those skeleton bars. Held still under prefers-reduced-motion: reduce
cellPadding 10 Horizontal padding inside cells (px)
selectionColor #2E75B6 Cell selection / range border color
selectionFill rgba(46,117,182,0.10) Range selection background overlay
selRowBg rgba(46,117,182,0.12) Background overlay for selected rows (when rowSelection is active)
hoverRowBg rgba(0,0,0,0.045) Wash over the row under the pointer. Neutral rather than the selection blue so the two stay tellable apart on the same row; use a light value on a dark theme, or a falsy value to disable
frozenBorder #B0B0B0 Separator line between frozen and scrollable columns
scrollbarBg #F0F0F0 Scrollbar track background
scrollbarThumb #C0C0C0 Scrollbar thumb color
scrollbarArrow #6B6B6B Glyph color for the step-arrow buttons at each end of a scrollbar track
scrollbarRadius 4 Scrollbar thumb border radius (px)
filterIconBg rgba(245,158,11,0.18) Background of the filter icon on a header with an active filter
filterIconColor #C87B00 Filter icon color
sortIconBg rgba(46,117,182,0.12) Background of the sort icon on a header with an active sort
sortIconColor #2E75B6 Sort icon color
headerIconColor rgba(0,0,0,0.28) Color of other header icons (when no filter/sort is applied)
hiddenColIndicator #94A3B8 Border color of the Excel-style double-line drawn on headers adjacent to a hidden column
editableCellBg undefined (no tint) Background tint applied to editable cells when editableCols is not '*'
readonlyCellBg #F5F5F5 Background tint applied to readonly cells when editableCols is not '*'
imagePlaceholderBg #F1F5F9 CellRenderers.image() cell background while an image is loading
imageErrorBg #FEE2E2 CellRenderers.image() cell background on load failure
imageErrorIcon #DC2626 CellRenderers.image() “broken image” X glyph color
fontSize 13 Font size for all text (px)
headerFontSize defaults to fontSize Header cell font size (px)
fontFamily -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif Font family for all text
locale 'en-US' Default BCP-47 tag used by CellRenderers.number/date/currency when a column doesn’t pin its own locale. Not a visual token; this is set via JHGridOptions.locale
invalidCellBorder #dc2626 Outline drawn around a cell whose value fails its column’s validation
deletedRowFill rgba(239,68,68,0.10) Background of a row marked deleted by deleteRow() but not yet committed
deletedRowStrike rgba(239,68,68,0.55) Strikethrough color for text in a row marked deleted
dragGhostShadow rgba(15,23,42,0.30) Drop shadow under the ghost that follows the pointer during a column-header drag
dragIndicatorFill rgba(59,130,246,0.12) Insertion-point fill shown while dragging a column/row to reorder
dragIndicatorLine #3b82f6 Insertion-point line shown while dragging a column/row to reorder

dragIndicatorFill/dragIndicatorLine/deletedRowFill/deletedRowStrike/ invalidCellBorder are automatically remapped to system colors in high-contrast (forced-colors) mode; visibility in high-contrast mode is preserved even with a custom theme applied.

selectionColor doubles as the DOM overlays’ accent color via --jhg-accent (e.g. the filter panel’s “Apply” button and the sort-direction toggle), and fontFamily is mirrored to --jhg-font-family so overlay text matches the canvas font without being set twice.

DOM surface tokens

These style the parts of the grid that are real DOM rather than canvas: the filter panel, context menus, the column chooser, cell editors, and the pager. theme reaches them the same way as the canvas tokens above, but each is also bridged to a --jhg-* CSS custom property (see Styling with your own CSS below); set the variable in your own stylesheet and it wins over whatever theme says, since every one of these is emitted as var(--jhg-…, <theme value>).

Key CSS variable Default Description
overlayBg --jhg-overlay-bg #FFFFFF Background of panels, menus and dialogs
overlayBorder --jhg-overlay-border #D0D0D0 Their outer border
overlayHeaderBg --jhg-overlay-header-bg #F5F5F5 Header strip inside a panel or dialog
overlayDivider --jhg-overlay-divider #E0E0E0 Hairline between sections of a panel
overlayText --jhg-overlay-text #212121 Primary text on those surfaces
overlayMutedText --jhg-overlay-muted-text #595959 Secondary text (labels, counts)
overlayHintText --jhg-overlay-hint-text #6E6E6E Placeholder and hint text
overlayHoverBg --jhg-overlay-hover-bg #F5F5F5 Hover background for a row in a list, such as a filter checklist
overlayItemHoverBg --jhg-overlay-item-hover-bg #F0F0F0 Hover (and keyboard-focus) background for a menu item
overlayShadow --jhg-overlay-shadow 0 4px 16px rgba(0,0,0,0.15) Shadow under panels and dialogs
overlayMenuShadow --jhg-overlay-menu-shadow 0 4px 12px rgba(0,0,0,0.15) Shadow under context menus, which sit closer to the surface
overlayAccentText --jhg-overlay-accent-text #FFFFFF Text on an accent-filled control, such as the filter panel’s apply button
pagerBg --jhg-pager-bg #f8fafc Pager bar background. Only rendered with pagination enabled
pagerText --jhg-pager-text #1e293b Pager bar text
pagerBorder --jhg-pager-border #e2e8f0 Border between the pager bar and the grid
pagerButtonBg --jhg-pager-button-bg #ffffff Page-button background
pagerButtonBorder --jhg-pager-button-border #cbd5e1 Page-button border

Styling with your own CSS

The grid body, header, selection and scrollbars are painted on a <canvas>, so CSS cannot reach them; those are configured through theme above. Everything around the grid is real DOM and can be styled directly.

Each DOM surface carries a stable class, and the theme object is mirrored onto --jhg-* custom properties scoped to the grid. The floating surfaces (.jhg-panel, .jhg-menu, .jhg-dialog) are drawn directly under <body>, outside the grid’s own element, so a container that clips its contents cannot cut them off; select them by their own class rather than through your grid container. They carry .jhg-root, so the variable overrides below reach them. Overriding a variable in your own CSS wins over the built-in value:

.jhg-root, .jhg-pager {
  --jhg-overlay-bg: #1f2937;
  --jhg-overlay-text: #f9fafb;
  --jhg-overlay-border: #374151;
  --jhg-accent: #60a5fa;
}
Class Surface
.jhg-root Grid wrapper (variable scope)
.jhg-overlay Any floating surface; carries the entrance animation
.jhg-panel Filter / sort panel
.jhg-dialog Column visibility chooser
.jhg-menu / .jhg-menu-item Context menus and their rows
.jhg-btn Buttons inside overlays
.jhg-pager / .jhg-pager-btn Pagination bar and its buttons
.jhg-editor Cell editors and the filter search input
.jhg-chip / .jhg-chip-contains The picked-value chips of the tag filter, and its “contains” chip
.jhg-tree-group / .jhg-tree-toggle A group row of the column visibility dialog, and its expand arrow
.jhg-loading / .jhg-empty / .jhg-tooltip Loading overlay, empty state, cell tooltip

These strings are also exported as GRID_CLASSES, for code that wants to target a surface without hardcoding the class name:

import { GRID_CLASSES } from '@jh-grid/jhgrid-js';

document.querySelectorAll(`.${GRID_CLASSES.menuItem}`);

Motion is deliberately restrained (≈140 ms, opacity/transform only) and is disabled automatically under prefers-reduced-motion: reduce. Retime it globally without rewriting rules:

.jhg-root { --jhg-motion-fast: 90ms; --jhg-motion-slow: 120ms; }

The keyboard focus ring drawn around focused buttons, inputs and pager controls follows the accent colour (selectionColor). Give it a colour of its own with --jhg-focus-ring:

.jhg-root, .jhg-pager { --jhg-focus-ring: #f59e0b; }

Only motion and focus rules live in the injected stylesheet (one <style id="jhgrid-styles"> per document); colors and layout stay inline so a missing stylesheet degrades to “no animation” rather than an unstyled grid.

Canvas motion

The grid body is painted into a <canvas>, so none of the CSS above reaches it. What it does have is a row hover highlight: the row under the pointer takes the hoverRowBg wash, and the highlight fades as it appears and disappears.

The row it sits on moves instantly while only the opacity eases. Fading the movement itself would make the highlight trail the pointer while you scan down a column, which reads as the grid being slow, and would briefly light two rows at once.

The selection box also travels to its new cell or range instead of jumping (selectionMoveMs). It deliberately snaps rather than easing whenever easing would put the border somewhere the user is actively aiming:

  • during a range drag or fill drag: the box has to stay under the pointer;
  • when the selection appears from nothing or clears;
  • when a move arrives before the previous one has landed. Held arrow keys repeat far faster than the box can cross a cell, so the first step eases and the rest are exact.

Dragging a column header lifts its header cell out and carries it under the cursor, and the reorder happens live: as the pointer crosses each boundary the columns it displaces slide aside (columnSlideMs), so the grid previews the arrangement it will actually end up in instead of rearranging on drop. Outside the drag handler it is still one move: onColumnReorder fires once with the final order, one undo step covers the whole drag, and Esc puts the column back.

Mouse-wheel scrolling glides to where the gesture is heading (scrollEaseMs) rather than teleporting a notch at a time. Deltas smaller than one row are applied immediately instead: a precision trackpad emits a dense stream of few-pixel deltas that is already smooth, and easing it would add lag the hardware had already removed. The split is by step size, not by trying to identify the device.

new JHGrid({
  // …
  hoverFadeMs: 0,                                  // instant, no fade
  selectionMoveMs: 0,                              // selection box jumps
  scrollEaseMs: 0,                                 // wheel deltas applied immediately
  theme: { hoverRowBg: 'rgba(255,255,255,0.055)' } // light wash for a dark theme
});

Set theme.hoverRowBg to a falsy value to turn the highlight off entirely; the grid then does no hover tracking or repainting at all. Both durations are forced to 0 under prefers-reduced-motion: reduce, which removes the easing but keeps the highlight and the box.


Conditional Styling (rowHighlighter / cellBackground)

Engineering rows tinted blue by rowHighlighter, with low-score cells additionally tinted red by cellBackground

const grid = new JHGrid({
  container: '#grid',
  fetchMeta, fetchData,
  rowHighlighter: (row) => row?.status === 'ERROR' ? 'rgba(239,68,68,0.12)' : null,
  cellBackground: (row, rowIndex, field) =>
    row && field === 'score' && row.score < 60 ? '#fee2e2' : null,
});

Both callbacks are called on every render (for visible rows/cells), so keep them lightweight; exceptions thrown in cellBackground are logged and ignored, and don’t block rendering.

Cell Decorator / Tooltip (cellDecorator / cellTooltip)

cellTooltip supplies hover text for a cell; cellDecorator draws directly on the canvas on top of a cell, for a small mark, icon, badge or bar that the cell’s own renderer doesn’t provide.

Score cells showing plain numbers; hovering a cell shows the tooltip "47 out of 100"

const grid = new JHGrid({
  container: '#grid',
  data: [
    { name: 'User 1', score: 30 },
    { name: 'User 2', score: 64 },
    // ...
  ],
  columnDefs: [
    { field: 'name',  label: 'Name' },
    { field: 'score', label: 'Score', align: 'right', width: 180 },
  ],

  // Shown at once while the pointer rests on a Score cell.
  cellTooltip: (rowData, rowIndex, field) =>
    field === 'score' ? `${rowData?.score} out of 100` : null,
});

A decorator is a function that receives the canvas context and the cell’s position, so it can draw anything on top of the value. For example, a thin bar along the bottom edge of each Score cell, as long as the score:

new JHGrid({
  // ...
  cellDecorator: (ctx, { x, y, w, h, rowData, field }) => {
    if (field !== 'score') return;
    const score = Number(rowData?.score);
    if (!Number.isFinite(score)) return;
    ctx.fillStyle = '#2E75B6';
    ctx.fillRect(x + 1, y + h - 4, (w - 2) * Math.min(100, Math.max(0, score)) / 100, 3);
  },
});

A decorator draws on plain ctx with the cell box it is given, and the canvas state it inherits is whatever the cell drawn just before it left behind. Unlike a column’s renderer it does not start from a reset state and has no args.text(), so set textAlign, textBaseline and fillStyle yourself before any fillText. Leaving textAlign to chance puts the text wherever the previous column’s alignment says, which for a right-aligned neighbour means drawing it to the left of the coordinate you passed.

cellDecorator runs for every visible, loaded cell on every render, so keep it cheap and let it return early for cells it has nothing to say about. cellTooltip returns text (or null for none), replaces the built-in overflow tooltip for that cell, and is itself overridden by a validation error message on the same cell. If the decoration depends on host state that changed outside the grid, call repaint() and it is drawn again on the next frame. An exception thrown by either callback is logged and does not interrupt rendering.

Changing the Theme at Runtime (setTheme / getTheme)

setTheme(patch) changes the theme of a grid that is already running. The keys in the patch replace theirs, everything else stays, and the grid keeps the unsaved edits, the selection and the scroll position it had - which is what a dark-mode toggle needs.

const DARK = {
  headerBg: '#1F2937', headerText: '#E5E7EB', headerBorder: '#374151',
  rowEven: '#111827', rowOdd: '#0F172A', cellText: '#E5E7EB', cellBorder: '#1F2937',
  scrollbarBg: '#111827', scrollbarThumb: '#4B5563',
};

document.querySelector('#dark-toggle').addEventListener('change', (e) => {
  grid.setTheme(e.target.checked ? DARK : JHGrid.DEFAULT_THEME);
});

grid.getTheme();   // a copy of the theme the grid is drawing with
  • Open panels and menus close when the theme changes; they are rebuilt in the new colours the next time they open.
  • fontSize and cellPadding change how many lines a wrapped cell takes, so a theme change re-measures the rows under autoRowHeight.
  • Everything in the theme object can be patched; see the table at the top of this page.

This site uses Just the Docs, a documentation theme for Jekyll.