Skip to content

Filter Styling

Enterprise

The per-column filter popup — the panel that opens from a column’s filter funnel — is theme-aware in core. It reads the grid’s font, a coherent accent, radius and shadow from a dedicated --zg-filter-* token group, so it matches your grid without any configuration. Retheming it is as simple as overriding those tokens:

.zg-grid {
--zg-filter-radius: 12px;
--zg-filter-accent: #7c3aed; /* focus rings, primary button, segmented toggle */
--zg-filter-condition-bg: #faf5ff;
}

That covers the common case. When you need to go further — target a single part, add your own classes, or replace a part’s DOM entirely — reach for filterTheme(), an enterprise helper that drives the popup’s customization surface at runtime:

import { filterTheme } from '@zengrid/enterprise/grid';
const theme = filterTheme(grid, {
tokens: { '--zg-filter-radius': '12px', '--zg-filter-accent': '#7c3aed' },
parts: { applyBtn: { class: 'my-apply' }, valueInput: { style: { fontWeight: '600' } } },
render: { title: (ctx) => myTitle(ctx.columnDef) },
});
// later: theme.update({ ... }) or theme.detach()

filterTheme() is license-gated (it asserts the filter-theme feature). It’s the ergonomic front-end to core’s filterUI.setCustomization extension point, so the deep customization lives in the library, not in your app’s CSS overrides.

  • tokens — CSS custom properties set on the popup root. The cleanest path: retint the whole popup by overriding --zg-filter-*.
  • parts — per-part class and inline style, composed over the defaults. Parts: popup, header, title, close, match, conditions, conditionRow, operatorSelect, valueInput, valueInputTo, addBtn, actions, applyBtn, clearBtn.
  • render — replace a part’s DOM outright. Return an element and it becomes that part; return nothing and the default is kept. Replacing a control part (operatorSelect / valueInput*) means your element becomes the control, so it must expose a .value.

Override --zg-filter-* to retint the whole popup. Open a column funnel, then edit the token values and re-open.

interface FilterPopupCustomization {
tokens?: Record<string, string>; // set on the popup root
parts?: Partial<Record<FilterPart, {
class?: string | string[];
style?: Record<string, string>;
}>>;
render?: Partial<Record<FilterPart, (ctx: FilterPartContext) => HTMLElement | void>>;
}
type FilterPart =
| 'popup' | 'header' | 'title' | 'close' | 'match' | 'conditions'
| 'conditionRow' | 'operatorSelect' | 'valueInput' | 'valueInputTo'
| 'addBtn' | 'actions' | 'applyBtn' | 'clearBtn';
interface FilterPartContext {
part: FilterPart;
columnDef: ColumnDef;
dataCol: number;
defaultElement: HTMLElement; // what the popup built by default
}
function filterTheme(grid, options?: FilterThemeOptions): FilterThemeHandle;
interface FilterThemeHandle {
update(options: FilterThemeOptions): void; // replace the customization
detach(): void; // restore the default popup
}
  • Core is already theme-aware. You only need filterTheme() for per-part or slot-level control — plain retinting is just --zg-filter-* in your stylesheet.
  • filterTheme() applies to the next open. It re-opens cleanly; a popup that’s already open closes so the new customization takes effect.
  • Replacing control parts is powerful but fragile — your element becomes the control and must expose a .value the popup can read on Apply.
  • Tokens over !important. Prefer token overrides and part classes to fighting the defaults with specificity.