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:
- A built-in theme — one attribute.
- CSS custom properties — the
--tlv-*tokens. Inherited, so they pierce the shadow boundary. ::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-heightto 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 |
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 |
Footer, toolbar and filter bar
| 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
- Keep body text at 4.5:1 contrast against its background, and large or bold text at 3:1 (WCAG 2.2 AA).
- Do not weaken
--tlv-focus-ringfor looks. It is the only thing telling a keyboard user where they are. - Selected rows must remain distinguishable by more than hue — the component adds a left accent bar for exactly this reason.
- The stylesheet already handles
prefers-reduced-motionand Windows High Contrast (forced-colors: active). Custom CSS that hard-codes colours can undo the second one; useforced-color-adjust: nonedeliberately, not by accident.
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));