Skip to content

Filter panel

3 min read · Updated 8 Oct 2026

A filter panel holds the page’s filter objects behind the Filters button in the header. It opens as an overlay drawer, or as a docked column beside the board. A filter bar folds into it when its row runs out of width. The apply bar stays under the header in both variants; see Build an apply bar.

Filter panel: An overlay drawer concentrates filters while leaving the board behind it.

Illustrative example with synthetic data. An overlay drawer concentrates filters while leaving the board behind it.

  1. Is the window wide enough to keep the board’s widest tier beside the panel, and do readers filter while they read? Use the docked panel.
  2. Otherwise, use the overlay drawer.
Variant Layout Use cases Choose another when
Overlay drawer A modal panel at the right edge; the board keeps its width Many filters; filters set once per visit; any page below the docking width Readers change filters after every look and the window is wide
Docked panel A column left of the board; the board narrows Analysts adjusting region, product and channel repeatedly on a wide monitor The window is narrower than the docking width
<!-- Overlay drawer: a modal dialog outside the page's grid. -->
<dialog class="filter-panel filter-drawer" id="filter-drawer" aria-labelledby="filter-drawer-title" closedby="any">
<div class="filter-panel__head">
<h2 id="filter-drawer-title">Filters</h2>
<form method="dialog"><button type="submit">Close</button></form>
</div>
<div class="filter-panel__body">
<div class="filter-host" data-object-id="region-filter@panel"></div>
</div>
</dialog>
<!-- Docked panel: the first child of the page's grid, before the board. -->
<div class="page">
<aside class="filter-panel filter-dock" id="filter-dock" aria-labelledby="filter-dock-title" hidden>
<div class="filter-panel__head"><h2 id="filter-dock-title">Filters</h2></div>
<div class="filter-panel__body"><!-- filter hosts --></div>
</aside>
<main class="board">…</main>
</div>
  • <form method="dialog"> closes the drawer without script. For closedby="any" and its fallback, see Build it as a <dialog>.
  • The panel’s filters are their own instances (@panel), in the same selection group as the bar’s; see Instance identity.

The panel reads --surface, --ink and --line from the style.

:root {
--panel-w: 320px;
--panel-filter-h: 240px;
}
.filter-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--gap);
border-bottom: 1px solid var(--line);
}
.filter-panel__body {
display: grid;
gap: var(--gap);
align-content: start;
padding: var(--gap);
overflow-y: auto;
}
/* Size the host, not the filter. */
.filter-panel__body > .filter-host {
height: var(--panel-filter-h);
}
.filter-drawer {
box-sizing: border-box;
width: min(var(--panel-w), 100vw - 2 * var(--gap));
max-width: none;
height: 100svh;
max-height: none;
margin: 0 0 0 auto;
padding: 0;
border: 0;
border-left: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
}
/* Display rules on a dialog only while it is open. */
.filter-drawer[open] {
display: grid;
grid-template-rows: var(--header-h) minmax(0, 1fr);
}

margin: 0 0 0 auto places the dialog at the right edge; max-width and max-height: none remove the user-agent limits. See CSS rules that fail silently for why display is scoped to [open].

.page:has(> .filter-dock:not([hidden])) {
display: grid;
grid-template-columns: var(--panel-w) minmax(0, 1fr);
gap: var(--gap);
}
.filter-dock {
display: grid;
grid-template-rows: var(--header-h) minmax(0, 1fr);
box-sizing: border-box;
/* The same height as the board beside it. */
height: calc(100svh - var(--header-h) - var(--above-board, 0px) - 2 * var(--gap));
background: var(--surface);
border: 1px solid var(--line);
}
.filter-dock[hidden] {
display: none;
}

Dock only where the board keeps its widest tier. The docked panel takes --panel-w + --gap from the board: for a widest tier starting at 1280px, the docking width is 1280 + 320 + 16 = 1616px, plus a classic scrollbar when the page scrolls.

  • Closed means unmounted. Mount the panel’s filters when it opens and unmount them when it closes; see Hidden means unmounted.
  • Open first, then mount. A closed dialog has no size, so call showModal() before mounting.
  • Selections survive closing. They belong to the group, so the apply bar still shows them. Discard clears pending selections; closing does not.
  • Use listbox mode, which has room in the panel, and turn quick select off, as in Configure the filter objects.
// Panel is the lifecycle class from Hidden means unmounted; one per panel instance.
declare const panelFilters: { panel: Panel; host: HTMLElement }[];
const trigger = document.querySelector<HTMLButtonElement>('.filters-trigger');
const drawer = document.querySelector<HTMLDialogElement>('#filter-drawer');
if (!trigger || !drawer) throw new Error('Filters trigger or drawer is missing.');
trigger.addEventListener('click', () => {
drawer.showModal();
trigger.setAttribute('aria-expanded', 'true');
for (const { panel, host } of panelFilters) panel.show(host);
});
drawer.addEventListener('close', () => {
for (const { panel } of panelFilters) panel.hide();
trigger.setAttribute('aria-expanded', 'false');
});

For the docked panel, toggle hidden on .filter-dock from the same trigger: show its panels after removing hidden, hide them when setting it. With a filter bar on the page, set data-filters="docked" on .app at the docking width (a matchMedia change listener) and hide the bar’s filters; the bar then removes its row and its fold observer returns early. Below the docking width, hide the dock’s filters and remove the attribute; the next header resize runs the fold observer again.

showModal() moves focus into the drawer and makes the page inert; Escape and Close close it and return focus to the trigger. The docked panel is part of the page: Tab moves from the header into it, then to the board.

  • Filter bar: the inline row that folds into this panel
  • Dialog: other modal dialogs