Filter Panel

Pro

A premium faceted filter panel for product listings: collapsible groups for checkbox lists, color swatches, star ratings and a price range, with per-option counts, a live active-filter chip row and a one-click clear-all. Selection is exposed through two-way selected / price models and a consolidated change output.

</>sv-filter-panel filterfacetssidebarecommercesearchcatalog

Also belongs under Forms & Inputs — it is catalogued in Data Display and stays there.

Complexity tier: Application The interactive bar, plus a stated data-volume ceiling, a persistence contract, a bundle budget and an accessible parity surface.

import { SvFilterPanelComponent } from '@styloviz-pro/display';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Two-way selection

Pro

Plain variant with Apply

Pro

Stale counts and zero-result facets

Pro

URL sync

Pro

When to use

  • The sidebar of a product-listing or search-results page.
  • A filter drawer on mobile, paired with sv-drawer.
  • Any catalog view that needs faceted refinement with a live count.

Async states

  • loading
  • error
  • retry

Inputs

NameTypeDefaultDescription
groupsFilterGroup[][]Facet groups to render.
headingstring'Filters'Panel heading.
showClearAllbooleantrueShow the "Clear all" control when filters are active.
showCountsbooleantrueShow per-option result counts.
showActiveChipsbooleantrueShow the active-filter chip row.
showApplybooleanfalseBatch the selection and emit only when Apply is pressed. THE BUTTON EXISTED AND CHANGED NOTHING. `filterChange` fired on every toggle regardless, so on mobile — where this mode matters most — each tap still cost a request and the Apply button was a decoration over a panel that had already applied. Turning it on now genuinely SUPPRESSES the per-toggle emission: the selection is a draft until Apply, which is the only thing that makes a transaction mode worth having.
countsStalebooleanfalseTrue while the server is recomputing the facet counts. A FACET COUNT THAT DOES NOT REFLECT THE CURRENT SELECTION IS WORSE THAN NO COUNT. It is a number, so the reader trusts it, and it is wrong — it describes the results of the PREVIOUS query. Counts are dimmed and announced as recalculating rather than left standing as fact.
disableEmptyFacetsbooleantrueDisable facets whose count is zero, so a dead end cannot be selected. A ZERO-RESULT COMBINATION IS REACHABLE IN ONE TAP AND ESCAPABLE ONLY BY GUESSING WHICH FACET CAUSED IT. Preventing it is far kinder than the empty state afterwards. An already-SELECTED facet is never disabled, whatever its count says: deselecting has to stay possible, and a selected facet reading zero is usually how the user got into the dead end in the first place.
applyLabelstring'Apply filters'Apply button label.
clearLabelstring'Clear all'Clear-all label.
collapsiblebooleantrueWhether groups can collapse (per-group `collapsible` overrides).
variant'card' | 'plain''card'Visual style.
loadingbooleanfalseTrue while the facets are being fetched. Replaces the groups with a placeholder rather than showing an empty panel.
errorSvDataError | string | nullnullFailure loading the facets. Showing an empty filter panel instead would tell the user there is nothing to filter by.
selectedRecord<string, string[]> (two-way){}Selected values per group id (two-way: `[(selected)]`).
priceRecord<string, PriceRange> (two-way){}Chosen price range per price-group id (two-way: `[(price)]`).
urlParamPrefixstringSV_FILTER_PANEL_PARAM_PREFIXNamespace for the query parameters this panel owns. Override when two panels share one page; anything outside the prefix is left alone, so the host's own `?page=2&q=shoes` survives every facet change.
debounceMsnumber300Debounce delay for `filterChange`, in ms. `0` emits immediately.

Outputs

EventPayload typeDescription
filterChangeFilterChangeThe filter selection changed — refetch. Debounced by `debounceMs`. Renamed from `change`, a native DOM event name. Not `selectedChange` either — that one is owned by the `selected` model above.
applyFilterChangeEmitted when the Apply button is pressed.
clearvoidEmitted when everything is cleared.
retryvoidThe host should re-run the failed facet load.
selectedChangeRecord<string, string[]>Emits the new `selected` whenever it changes — the output half of `[(selected)]`.
priceChangeRecord<string, PriceRange>Emits the new `price` whenever it changes — the output half of `[(price)]`.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Pro

Bound to the names this component actually exposes.

Host class

Pro

Drive the three async inputs from your own fetch.

  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `groups`, `loading` and `error`.
  • `debounceMs` (default 300) collapses a burst of typing into one request. Set it to 0 only when the query is driven by clicks.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • Four facet types: checkbox, color, rating and price.
  • Collapsible groups with a per-group override and remembered state.
  • Toggle counts, active chips, clear-all and an Apply footer.
  • countsStale dims counts during recalculation; disableEmptyFacets (default true) disables zero-count options.
  • urlParamPrefix namespaces the query parameters serializeToUrl / restoreFromUrl own — override it when two panels share a page.
  • Card or plain variant to match the surrounding layout.

Best practices

  • Do: Drive results from the change output (or the two-way selected / price models) on your listing page.
  • Do: Provide per-option counts so shoppers can see how many results each facet yields.
  • Do: Use variant="plain" when embedding inside an existing card or drawer.
  • Do: Set [countsStale]="true" while the server recomputes facet counts: the numbers stay visible but are dimmed and announced as "recalculating", because a count describing the previous query reads as fact.
  • Do: Leave disableEmptyFacets on (the default): checkbox, colour and rating options with a count of 0 are disabled and announced ", no results" — never when already selected, so deselecting stays possible. An absent count is "unknown" and is never disabled.
  • Do: In apply mode ([showApply]="true") bind (apply) — not [(selected)] — to run the query. The selected / price models still update on every toggle because they hold the DRAFT; filterChange is suppressed until Apply is pressed, then fires once alongside apply.
  • Do: Sync to the URL with serializeToUrl(existing) → Router.navigate and restoreFromUrl(params) on load. Only parameters under urlParamPrefix (default "f.") are owned, so the host's own ?page / ?q survive; restoreFromUrl is all-or-nothing, does not emit filterChange, and returns a report when a shared link no longer matches the groups.
  • Don't: Do not mix unrelated facet types in one group — give each facet its own FilterGroup.

Accessibility

  • Group headers are buttons with aria-expanded; options use real checkboxes and pressed-state buttons.
  • Color swatches and rating rows expose descriptive labels and aria-pressed.
  • Zero-count options are disabled with an sr-only ", no results"; stale counts carry an sr-only ", recalculating".
  • All controls have visible focus rings in light and dark mode.

Feedback on Filter Panel

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.