Theming

The component renders inside a Shadow DOM, so page CSS cannot reach its internals. There are three sanctioned routes in, in increasing order of effort:

  1. A built-in theme — one attribute.
  2. CSS custom properties — the --tlv-* tokens. Inherited, so they pierce the shadow boundary.
  3. ::part() — direct styling of named internal elements.

Reach for the highest one that does the job. Custom properties survive upgrades; ::part() selectors survive them too but are more specific to the current markup.


1. Built-in themes

<bm-treelistview theme="dark"></bm-treelistview>

Light: light · ocean · forest · sunset · grape · slate · executive · amber · neon · mint (0.11)

Dark: dark · aurora (0.11) · royal (0.11) · ocean-dark · sunset-dark · forest-dark · slate-dark · amber-dark (the five -dark names are 0.11)

Follows the system: auto (0.2) — reads prefers-color-scheme, and needs no JavaScript.

The -dark names

ocean and ocean-dark are the same hue at opposite ends of the scale, and the same goes for the other four pairs. They exist as separate names rather than as one theme that flips, because a grid that changed colour when a customer upgraded would be a breaking change in everything but name — so the light originals are exactly as they were, and the dark readings sit alongside them.

Pick by the page the grid sits in: a dark page wants ocean-dark, a light one wants ocean. If you would rather follow the operating system than choose, use auto.

Contrast. Every built-in theme is checked against WCAG 1.4.3 for the pairings a user actually reads — body text on the ground, the alternate row and the hover row; header text on the header and its hover; selected-row text; muted text; the filter highlight; and badge text. A couple of values in the 0.11 themes are deliberately darker than the equivalent colour on the Binarymission site, because the site uses them as decoration and the grid uses them as body-sized text.

Density

<bm-treelistview density="compact"></bm-treelistview>
Density Row height Font size
compact 32px 13px
comfortable (default) 38px 14px
spacious 48px 15px

If you virtualise, set row-height to match the density you chose.


2. CSS custom properties

Set them on the element, or on any ancestor:

bm-treelistview {
  --tlv-font-family: 'IBM Plex Sans', system-ui, sans-serif;
  --tlv-radius: 8px;
  --tlv-header-bg: #172554;
  --tlv-header-fg: #ffffff;
  --tlv-node-hover-bg: #eff6ff;
  --tlv-node-selected-bg: #1d4ed8;
  --tlv-node-selected-fg: #ffffff;
  --tlv-focus-ring: #f59e0b;
}

They cascade normally, so one rule can theme every grid on a page, and a more specific rule can override a single one.

Typography and shape

Token Default
--tlv-font-family Inter, Segoe UI, Roboto, Arial, sans-serif
--tlv-font-size 14px
--tlv-radius 12px

Surfaces

Token
--tlv-bg component background
--tlv-fg body text
--tlv-muted-fg secondary text: counts, labels, blanks

Borders

Token
--tlv-border-color outer border
--tlv-border-thickness default 1px
--tlv-cell-border-color between cells
--tlv-header-border-color under the header
Token
--tlv-header-bg, --tlv-header-fg
--tlv-header-hover-bg
--tlv-header-height
--tlv-header-font-weight default 700

Rows

Token
--tlv-node-bg, --tlv-node-fg
--tlv-node-bg-alt zebra stripe
--tlv-node-hover-bg, --tlv-node-hover-fg
--tlv-node-selected-bg, --tlv-node-selected-fg
--tlv-node-disabled-opacity default 0.55
--tlv-row-height
--tlv-selected-left-bar-width accent bar on the selected row

Cells and expanders

Token
--tlv-cell-padding-y, --tlv-cell-padding-x
--tlv-expander-size, --tlv-expander-radius
--tlv-expander-bg, --tlv-expander-fg
--tlv-expander-hover-bg, --tlv-expander-hover-fg
--tlv-expander-font-size

Selection and focus

Token
--tlv-checkbox-size, --tlv-checkbox-accent
--tlv-focus-ring focus outline colour
--tlv-checkbox-column-width default 46px

Frozen and pinned columns

Token
--tlv-frozen-column-shadow
--tlv-frozen-column-border-color
--tlv-frozen-column-z-index, --tlv-frozen-header-z-index

Shadows

Token
--tlv-component-shadow, --tlv-component-shadow-hover
--tlv-row-hover-shadow, --tlv-row-hover-shadow-inset
--tlv-row-hover-transform default none

Scrollbars

Token
--tlv-scrollbar-track
--tlv-scrollbar-thumb, --tlv-scrollbar-thumb-hover
--tlv-scrollbar-size default 12px

Cell kinds

Token
--tlv-badge-bg, --tlv-badge-fg
--tlv-progress-track, --tlv-progress-value
--tlv-link-fg
--tlv-chip-bg, --tlv-chip-fg 0.2
--tlv-rating-filled, --tlv-rating-empty 0.2

Status

Token
--tlv-error-bg, --tlv-error-fg, --tlv-error-border
--tlv-loading-bg overlay behind the spinner
Token
--tlv-footer-bg, --tlv-footer-fg, --tlv-footer-border-color
--tlv-footer-height, --tlv-footer-font-size, --tlv-footer-gap
--tlv-toolbar-height, --tlv-filter-height

New in 0.2

Token
--tlv-group-bg, --tlv-group-fg, --tlv-group-border-color group header rows
--tlv-totals-bg, --tlv-totals-fg totals row
--tlv-filter-row-bg per-column filter row
--tlv-match-bg, --tlv-match-fg filter highlight
--tlv-drop-indicator, --tlv-drop-inside-bg drag-and-drop affordances
--tlv-editor-bg, --tlv-editor-fg, --tlv-editor-border, --tlv-editor-error-fg inline editing
--tlv-rollup-fg, --tlv-rollup-bg parent roll-up cells (0.3)
--tlv-band-bg, --tlv-band-fg, --tlv-band-height banded (two-row) header (0.3)
--tlv-cell-focus-ring, --tlv-range-bg, --tlv-range-border cell focus and range selection (0.6)
--tlv-pinned-row-bg, --tlv-pinned-row-border pinned rows (0.6)
--tlv-skeleton-bar, --tlv-skeleton-shine skeleton loading rows (0.7)
--tlv-data-bar-color conditional-formatting data bars (0.7)
--tlv-pager-fg, --tlv-pager-current-bg, --tlv-pager-current-fg pager (0.8)
--tlv-detail-bg, --tlv-detail-accent, --tlv-detail-accent-width, --tlv-detail-padding master/detail panels (0.9)
--tlv-menu-bg, --tlv-menu-shadow column chooser

Every 0.2 token falls back to a 0.1 token, so a theme that only sets the old variables still produces a coherent grid.


3. ::part()

bm-treelistview::part(header-cell) {
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: 0.78rem;
}

bm-treelistview::part(row-selected) {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
}

/* Style one column by id */
bm-treelistview::part(cell-budget) {
  font-variant-numeric: tabular-nums;
}

/* Style a badge by its value */
bm-treelistview::part(badge)[data-value='Blocked'] {
  background: #fee2e2;
  color: #991b1b;
}

/* A detail panel that reads as an inset, not as a second grid */
bm-treelistview::part(detail-panel) {
  background: #f8fafc;
  border-left-color: #0ea5e9;
}

The full part list is in the API guide.


A worked custom theme

bm-treelistview.corporate {
  --tlv-font-family: 'Source Sans 3', system-ui, sans-serif;
  --tlv-font-size: 13.5px;
  --tlv-radius: 6px;

  --tlv-bg: #ffffff;
  --tlv-fg: #0f172a;
  --tlv-muted-fg: #64748b;

  --tlv-border-color: #cbd5e1;
  --tlv-header-bg: #0f172a;
  --tlv-header-fg: #f8fafc;
  --tlv-header-hover-bg: #1e293b;
  --tlv-header-font-weight: 600;

  --tlv-node-hover-bg: #f1f5f9;
  --tlv-node-selected-bg: #0f172a;
  --tlv-node-selected-fg: #ffffff;
  --tlv-node-bg-alt: #fbfcfd;

  --tlv-focus-ring: #0ea5e9;
  --tlv-checkbox-accent: #0f172a;

  --tlv-component-shadow: 0 1px 2px rgb(15 23 42 / 6%), 0 8px 24px rgb(15 23 42 / 6%);
  --tlv-group-bg: #e2e8f0;
  --tlv-totals-bg: #0f172a;
  --tlv-totals-fg: #f8fafc;
}

bm-treelistview.corporate::part(header-cell) {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
}
<bm-treelistview class="corporate" theme="light"></bm-treelistview>

Keep a theme attribute set even when overriding: it establishes a coherent base that your overrides adjust rather than replace.

Light and dark in one custom theme

bm-treelistview.corporate { --tlv-bg: #ffffff; --tlv-fg: #0f172a; }

@media (prefers-color-scheme: dark) {
  bm-treelistview.corporate { --tlv-bg: #0b1220; --tlv-fg: #e2e8f0; }
}

Accessibility notes on theming

See Accessibility.


Printing

The stylesheet includes print rules: shadows are dropped, the scroll container expands, the demo badge and toolbar are hidden, and rows avoid breaking across pages.

One caveat: a virtualised grid prints only the rows currently in the DOM, because the others do not exist. To print everything, turn virtualisation off first:

window.addEventListener('beforeprint', () => (grid.virtualize = false));
window.addEventListener('afterprint',  () => (grid.virtualize = true));