Skip to content
ShineUI
Install

FilterPanel

Stage filters, apply once.

<sui-filter-panel>DataStable

About

Filter surface for a ledger or list: a "Filters" trigger with an applied count, a panel with one control per filter, "Clear all" and "Apply filters", and a row of chips that clear one value at a time. Edits in the panel are a draft until "Apply filters"; chips and "Clear all" change the applied set at once.

Usage

<sui-filter-panel aria-label="Filters, Basic"></sui-filter-panel> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-filter-panel"); el.filters = [ { key: "status", label: "Status", options: ["Open", "Overdue", "Paid"], value: "Open" }, { key: "period", label: "Period", options: ["This month", "Last 90 days", "YTD"] }, { key: "customer", label: "Customer", options: ["All", "Silver Hills Church", "Riverbend Dental Group"] }, ]; /* SuiFilterDef[] */ el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiFilterPanel } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const filters = [ { key: "status", label: "Status", options: ["Open", "Overdue", "Paid"], value: "Open" }, { key: "period", label: "Period", options: ["This month", "Last 90 days", "YTD"] }, { key: "customer", label: "Customer", options: ["All", "Silver Hills Church", "Riverbend Dental Group"] }, ]; /* SuiFilterDef[] */ return <SuiFilterPanel aria-label="Filters, Basic" filters={filters} onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiFilterPanel } from "@shineui/vue"; import "@shineui/core/shineui.css"; const filters = [ { key: "status", label: "Status", options: ["Open", "Overdue", "Paid"], value: "Open" }, { key: "period", label: "Period", options: ["This month", "Last 90 days", "YTD"] }, { key: "customer", label: "Customer", options: ["All", "Silver Hills Church", "Riverbend Dental Group"] }, ]; // SuiFilterDef[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiFilterPanel aria-label="Filters, Basic" :filters="filters" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiFilterPanel } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiFilterPanel], template: `<sui-filter-panel aria-label="Filters, Basic" [(open)]="open" [filters]="filters" (suiChange)="onChange($event)"></sui-filter-panel>`, }) export class Example { open = false; filters = [ { key: "status", label: "Status", options: ["Open", "Overdue", "Paid"], value: "Open" }, { key: "period", label: "Period", options: ["This month", "Last 90 days", "YTD"] }, { key: "customer", label: "Customer", options: ["All", "Silver Hills Church", "Riverbend Dental Group"] }, ]; // SuiFilterDef[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

filters is a property; each entry seeds the applied set with its value. The trigger is a button with aria-expanded and the panel is a region labeled Filters.

<sui-filter-panel aria-label="Filters, Basic"></sui-filter-panel>
// setup(root): properties and events setup(root) { root.querySelector('sui-filter-panel').filters = ledgerFilters(); }

Open state

The element writes open itself as it opens and closes, so a screen that starts with the panel showing sets it once. Apply, an outside pointer, Escape and Tab past either end close it. While open, the panel paints in the top layer, so a toolbar or a card around it cannot clip it.

<sui-filter-panel aria-label="Filters, Open state" id="fp-open" open></sui-filter-panel>
// setup(root): properties and events setup(root) { root.querySelector('#fp-open').filters = ledgerFilters(); }

Several values

multiple draws a multi-select and gives each applied value its own chip. The count on the trigger stays a count of filters, not values.

<sui-filter-panel aria-label="Filters, Several values" id="fp-multi"></sui-filter-panel>
// setup(root): properties and events setup(root) { root.querySelector('#fp-multi').filters = [ { key: 'status', label: 'Status', options: ['Open', 'Overdue', 'Paid', 'Draft'], multiple: true, value: ['Open', 'Overdue'] }, { key: 'period', label: 'Period', options: ['This month', 'Last 90 days', 'YTD'] }, ]; }

A filter with kind "text" draws a search box beside the option filters. It stages as the user types, applies on Enter as any search box does, and carries its typed value on a chip like every other filter; an empty box is not applied.

<sui-filter-panel aria-label="Filters, Text search" id="fp-text" open></sui-filter-panel>
// setup(root): properties and events setup(root) { root.querySelector('#fp-text').filters = [ { key: 'q', label: 'Search', kind: 'text', placeholder: 'Customer, memo or reference', value: 'Riverbend' }, { key: 'status', label: 'Status', options: ['Open', 'Overdue', 'Paid'], value: 'Open' }, { key: 'period', label: 'Period', options: ['This month', 'Last 90 days', 'YTD'] }, ]; }

Events

suiApply fires on every apply, clear and chip removal; suiChange only when the set really differs. suiClose carries why the panel closed, and preventDefault on it keeps the panel open.

status: Open
<div style="display:grid;gap:10px"> <sui-filter-panel aria-label="Filters, Events" id="fp-events"></sui-filter-panel> <code id="fp-out" style="font:460 12px/1.6 var(--font-sans);color:var(--ink-muted)">status: Open</code> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#fp-events'); const out = root.querySelector('#fp-out'); el.filters = [ { key: 'status', label: 'Status', options: ['Open', 'Overdue', 'Paid'], value: 'Open' }, { key: 'owner', label: 'Owner', options: ['Dana Whitfield', 'Marcus Lee'] }, ]; const set = (v) => Object.entries(v).map(([k, val]) => `${k}: ${[].concat(val).join(', ')}`).join(' ยท ') || 'no filters'; el.addEventListener('suiApply', (e) => { out.textContent = `suiApply: ${set(e.detail)}`; }); el.addEventListener('suiChange', (e) => { out.textContent = `suiChange: ${set(e.detail)}`; }); el.addEventListener('suiClose', (e) => { out.textContent = `suiClose (${e.detail}). Applied: ${set(el.values || {})}`; }); }

Keyboard

KeyAction
EnterIn a text filter, applies the staged filters and closes the panel.
EscapeCloses the panel and returns focus to the Filters trigger.
TabFrom the Filters trigger of an open panel, moves into the panel. Past the last control in the panel, closes it and moves on from the trigger.
Shift+TabBefore the first control in the panel, closes it and returns focus to the Filters trigger.

API

<sui-filter-panel>

Properties

PropertyAttributeTypeDefaultDescription
filtersproperty onlySuiFilterDef[][]Filter definitions. A multiple filter draws a sui-multi-select and puts each of its values on its own chip; a kind: 'text' filter draws a search box beside the option filters. Property, not attribute.
openopenbooleanfalseShows the panel. Mutable; the element opens and closes itself, closing on apply, outside pointer, Escape and Tab past either end of the panel, each through the cancelable suiClose. An open panel paints in the browser's top layer, so a transformed, contained or clipping ancestor cannot move it or cut it off; it renders right after the trigger, so Tab from the trigger goes into it.
valuesproperty onlySuiFilterValuesundefined | { [x: string]: string | string[] | undefined; }The applied set, key to value. Mutable; seeded from each filter's value when not supplied. Property, not attribute.

Events

EventDetailReactVueDescription
suiApplySuiFilterValues{ [x: string]: string | string[] | undefined; }onSuiApply@sui-applyFires on every apply, clear all and chip removal, whether or not anything changed; detail is the applied set.
suiChangeSuiFilterValues{ [x: string]: string | string[] | undefined; }onSuiChange@sui-changeFires only when the applied set actually differs from before; detail is the applied set.
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the panel closes itself; detail is 'trigger' | 'pick' | 'outside' | 'escape' ('pick' is the close from "Apply filters"; 'escape' also returns focus to the trigger; 'outside' covers a pointer elsewhere and Tab past either end of the panel, which hands focus to the trigger first). Cancelable: preventDefault() keeps it open. It does not bubble, so a listener on the filter panel never hears a select closing inside it.
suiOpenvoidonSuiOpen@sui-openFires after the panel opens itself. It does not bubble.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Focuses the Filters button. el.focus() on the host does the same.

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiFilterDef interfaceOne filter: a key, its label and the values it can take. value seeds the applied set.
keystring
labelstring
options?string[]The options a select filter offers. A text filter ignores it.
value?string | string[]Initially applied value; an array when multiple.
multiple?booleanAccepts several values at once ("Status: Open and Overdue"). The panel draws a sui-multi-select instead of a sui-select and each applied value gets its own chip.
kind?'select' | 'text'select (the default) offers options; text is a free-text search box beside them, for a name, a reference or a memo. A text filter stages as the user types, applies on Enter, and carries its typed value on a chip like any other. multiple does not apply to it.
placeholder?stringPlaceholder inside the control. Defaults to "Any" on a select and "Search" on a text filter.
SuiFilterValues typeThe applied set: filter key to chosen option, or to the chosen options of a multiple filter (undefined, missing or an empty array = not applied).
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also