Skip to content
ShineUI
Install

FilterBuilder

Any column, And or Or.

<sui-filter-builder>DataStable

About

The advanced filter for sui-data-grid, sui-data-table and sui-tree-table: conditions on any column, joined by And or Or, in groups nested up to max-depth, moved by their grips and copied with Duplicate. Edits stay a draft until Apply. It sits outside the rows: in a grid's header slot, in a toolbar with target, or inline in a drawer.

Usage

<sui-filter-builder size="sm"></sui-filter-builder> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-filter-builder"); el.columns = []; /* SuiGridColumn[] | undefined */ el.value = []; /* SuiGridFilterModel | undefined */ el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiFilterBuilder } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const columns = []; /* SuiGridColumn[] | undefined */ const value = []; /* SuiGridFilterModel | undefined */ return <SuiFilterBuilder size="sm" columns={columns} value={value} onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiFilterBuilder } from "@shineui/vue"; import "@shineui/core/shineui.css"; const columns = []; // SuiGridColumn[] | undefined const value = []; // SuiGridFilterModel | undefined const onChange = (e) => console.log(e.detail); </script> <template> <SuiFilterBuilder size="sm" :columns="columns" :value="value" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiFilterBuilder } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiFilterBuilder], template: `<sui-filter-builder size="sm" [(open)]="open" [columns]="columns" [value]="value" (suiChange)="onChange($event)"></sui-filter-builder>`, }) export class Example { open = false; columns = []; // SuiGridColumn[] | undefined value = []; // SuiGridFilterModel | undefined onChange(e: CustomEvent) { console.log(e.detail); } }

In a grid

In the header slot of sui-data-grid the builder filters the grid it sits in, beside the Columns button. The count on the button is every condition applied. Conditions one column can hold go back into the column filters; the rest, such as an Or across columns, stay in the model as tree.

<div style="height: 380px; min-width: 0"> <sui-data-grid id="fb-grid" show-column-toggle filter-display="menu"> <sui-filter-builder slot="header" size="sm"></sui-filter-builder> </sui-data-grid> </div>
// setup(root): properties and events setup(root) { const g = root.querySelector('#fb-grid'); g.columns = columns(); g.rows = deals(4000); // The prop rather than setFilter(): a method does not exist before the grid loads, so only // the prop reaches a grid rendered on a server. g.filterModel = warning(); }

Pointed at a table

target takes the id of a grid or a table, so the builder can live in any toolbar on the page. It writes through the table's setFilter method, which emits suiFilter just as the table's own filter controls do.

<div style="display: grid; gap: 10px"> <div style="display: flex; align-items: center; gap: 12px"> <sui-filter-builder target="fb-table"></sui-filter-builder> <span id="fb-table-count" style="font-size: 12.5px; color: var(--ink-muted)"></span> </div> <sui-data-table id="fb-table" page-size="8"></sui-data-table> </div>
// setup(root): properties and events setup(root) { const t = root.querySelector('#fb-table'); const count = root.querySelector('#fb-table-count'); const rows = deals(120); t.columns = columns().map((c) => ({ ...c, flex: undefined })); t.rows = rows; t.rowKey = (r) => r.id; count.textContent = `${rows.length} deals`; t.addEventListener('suiFilter', (e) => { const n = Object.keys(e.detail.rules || {}).length + (e.detail.tree ? 1 : 0); count.textContent = n ? 'Filtered' : `${rows.length} deals`; }); }

Inline, as in a drawer

inline draws the builder in place with its Apply bar. Below 560px of its own width each condition stacks its column, condition and value beside the joiner. Without a target it edits value and reports suiChange, so it can drive a server query.

Apply to see the model.
<div style="display: grid; gap: 12px; max-width: 420px"> <sui-filter-builder inline id="fb-inline"></sui-filter-builder> <code id="fb-json" style="font: 460 12px/1.6 var(--font-sans); color: var(--ink-muted); word-break: break-word">Apply to see the model.</code> </div>
// setup(root): properties and events setup(root) { const b = root.querySelector('#fb-inline'); const out = root.querySelector('#fb-json'); b.columns = columns(); b.value = warning(); b.refresh(); b.addEventListener('suiChange', (e) => { out.textContent = JSON.stringify(e.detail); }); }

One level

max-depth="0" leaves out Add group, for a flat list of conditions with one And or Or, the shape MUI and most admin screens use.

<div style="display: grid; gap: 10px; max-width: 820px"> <sui-filter-builder inline max-depth="0" id="fb-flat"></sui-filter-builder> </div>
// setup(root): properties and events setup(root) { const b = root.querySelector('#fb-flat'); b.columns = columns(); b.value = { rules: {}, tree: { operator: 'or', items: [{ key: 'stage', matchMode: 'in', value: ['Negotiation'] }, { key: 'amount', matchMode: 'gte', value: 150000 }] } }; b.refresh(); }

Keyboard

KeyAction
Enter SpaceOn the Filters button, opens the builder and moves focus to the first column select.
TabWalks the grip, joiner, column, condition, value, duplicate and remove controls of each row, then Add condition, Add group, Clear all and Apply. Past the last control it closes the popover and carries on from the button.
Enter in a text or number fieldApplies the draft.
Space Enter on a gripLifts the condition or group. Pressed again, drops it where it is now.
Up Arrow Down ArrowMoves a lifted row one place, into a group and out of it as it passes. Home and End move it to the first or last place.
EscapePuts a lifted row back where it was. Otherwise closes an open list or calendar first, then the popover, without applying; focus returns to the Filters button.

API

<sui-filter-builder>

Properties

PropertyAttributeTypeDefaultDescription
columnsproperty onlySuiGridColumn[]SuiGridColumn[] | undefinedThe columns to offer. Unset, the target's columns that take filter: true, or all of them when none does. Property, not attribute.
inlineinlinebooleanfalseDraws the builder in place, with its Apply bar, instead of a Filters button and a popover: for a drawer, a bottom sheet or a settings page. Below 560px of its own width it stacks each condition's fields.
labellabelstringstring | undefinedThe trigger's text. Defaults to "Filters".
maxDepthmax-depthnumber2How deep groups nest: 0 allows none, 2 (the default) allows a group inside a group, the three levels Notion and Airtable offer.
openopenbooleanfalseShows the popover. Mutable; the element opens and closes itself, closing on Apply, outside pointer and Escape, each through the cancelable suiClose.
sizesize"md" | "sm"'md'The trigger's height: sm 28 beside a grid's Columns button, md 34 in a page toolbar.
targettargetHTMLElement | string | undefinedThe grid or table this builder filters: the element, or its id. Unset, it filters the grid or table it sits in, which is how the toolbar slot of sui-data-grid uses it. Without either it edits value alone.
valueproperty onlySuiGridFilterModelSuiGridFilterModel | undefinedThe applied filter when there is no target. With a target it mirrors the target's filterModel. Mutable: Apply and Clear all write it before suiChange fires. A write from the host at any time, v-model and ngModel included, redraws the builder's conditions from it, unless it brings back the filter the builder already holds. Property, not attribute.

Events

EventDetailReactVueDescription
suiChangeSuiGridFilterModelonSuiChange@sui-changeFires on Apply and on Clear all; detail is the whole filter model now applied.
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the popover closes; detail is why. Cancel it to keep the popover open.
suiOpenvoidonSuiOpen@sui-openFires when the popover opens.

Methods

MethodSignatureDescription
refresh()refresh() => Promise<void>Reads the applied filter again, after the target's filter was set from script.
setFocus()setFocus() => Promise<void>Focuses the Filters button, or, when inline, the builder's first control. 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.
SuiGridAlign typeCell text alignment. A side named here is a side of the screen in either reading direction. Left unset, a money or number column sits at the inline end and everything else at the inline start, so the default mirrors under dir="rtl".
No members.
SuiGridCellValue typeWhat a cell renderer may return. A string is text; an element is appended to the cell. Shared with utils/render-content.ts, which does the mounting.
No members.
SuiGridColumn interfaceOne column. key is both the field read from the row and the column's identity, so it must be unique across the set; field overrides the field when the two need to differ (two columns over the same field, or a computed column with no field at all).
keystringUnique identity, and the row field read unless field or value says otherwise.
headerstringColumn heading.
headerSr?stringHeading read by assistive technology when header is empty: an actions column shows no words and still names itself. Ignored when header has text.
field?stringRow field to read, when it is not key.
value?(row: SuiGridRow) => unknownComputed value for sorting, filtering and export. Takes precedence over field; a column with only a render and no value sorts and exports by its raw field, which is usually wrong.
render?(row: SuiGridRow, key: SuiKey) => SuiGridCellValueCell content. A string renders as text; an element is mounted into the cell. Called only for cells inside the rendered window. Property, not attribute.
type?SuiGridValueTypeDrives the default comparator, the default filter match modes and the default alignment.
align?SuiGridAlignA side of the screen to align to, in either reading direction. Leave it unset for reading order: text at the inline start, numbers and money at the inline end.
money?booleanAligned to the inline end, tabular numerals, weight 640. Implies type: 'number' for comparison unless the column says otherwise, because a money column holds an amount.
nowrap?booleanOne line, clipped with an ellipsis, and the full value on the cell's title.
width?numberStarting width in px. Omit to let flex share the space.
minWidth?number
maxWidth?number
flex?numberShare of the space left after fixed widths. flex: 1 on two columns splits it evenly.
sortable?boolean
resizable?booleanDraggable edge on the header. Off by default; a resized column drops its flex.
hidden?booleanExcluded from the render, from export and from the keyboard, but kept in the model so a column toggle can bring it back without the caller rebuilding the array.
pin?SuiGridPinPins the column to an edge. Pinned columns are never windowed away.
compare?(a: unknown, b: unknown) => numberCustom comparator; overrides type. Return negative, zero or positive.
format?(value: unknown, row: SuiGridRow) => stringFormats the value for text: the cell when there is no render, the export, and the clipboard. Keep it cheap; it runs once per visible cell per render. Without it a number or money column shows a plain figure grouped and marked in the page's locale (1,234.5, 1.234,5), while the export, the clipboard and an edit keep the plain figure (1234.5).
muted?booleanDrawn in --ink-muted: a memo, an account type, anything secondary.
headerTitle?stringHeader tooltip, for a column whose heading had to be short.
editable?booleanOpts a column out of in-cell editing on a grid that has it on. A column that draws its own cell is never editable: there is no text for an editor to stand in for.
cellClass?(value: unknown, row: SuiGridRow, index: number) => string | undefinedExtra classes for this column's cells, per row. Called once per visible cell per render, so keep it cheap: return a stable string rather than building one. The grid's own classes are always applied; this adds to them.
filter?booleanOffers this column a filter control, in the filter row or the filter menu. Ignored while the element's filterDisplay is none.
matchMode?SuiGridMatchModeMatch mode the column's filter opens with. Defaults by type: contains for text, equals for numbers and booleans, dateIs for dates.
filterOptions?readonly (string | { value: string; label: string; })[]The values a status-like column holds. With them, the column's filter menu is a checklist of these values, writing an in rule, instead of a list of conditions. A bare string is its own value and label. The filter row ignores them and keeps its text box.
editor?SuiGridEditorThe control this column opens for editing. A plain text input when it is absent. Named by value rather than by a string, so an application that edits only text never carries the date picker: see the note at the top of editing.ts.
validator?SuiGridValidatorChecked when an edit commits. Returning a message refuses the commit and keeps the editor open with the message beside it.
SuiGridEditor interfaceA control the grid mounts into a cell, focuses, and reads back when the edit commits. In sui-data-grid focus leaving the control commits, and focus in the list or calendar the control opened has not left it, so a ShineUI picker works as an editor.
mount(ctx: SuiGridEditorContext) => HTMLElementBuilds the control, populated and ready. The grid mounts it in the cell and focuses it.
read(el: HTMLElement) => stringThe text to commit, read from the element mount returned.
handlesKey?(e: KeyboardEvent, el: HTMLElement) => booleanOptional: keys this editor consumes itself, so the grid does not also act on them.
SuiGridEditorContext interfaceWhat an editor is given when it opens.
valueunknownThe raw value from the row.
textstringWhat the cell was showing, which is what a text editor seeds with.
columnSuiGridColumn
rowSuiGridRow
seed?stringSeed text when the editor was opened by a printable keystroke, else undefined.
SuiGridFilterCondition interfaceOne condition of an advanced filter: a column, a mode and a value, the same test a constraint makes, with the column it reads named beside it.
keystring
matchModeSuiGridMatchMode
value?unknown
SuiGridFilterConstraint interfaceOne test. value carries whatever the mode needs: a scalar, an array for in and notIn, a two-element [from, to] for between, and nothing at all for empty and notEmpty.
value?unknown
matchModeSuiGridMatchMode
SuiGridFilterGroup interfaceConditions and groups joined by one operator, to any depth: "Owner is Davon, and any of (health is Critical, no next meeting, no interaction)". What sui-filter-builder edits, and the one shape in the model that can join two columns with or.
operator'and' | 'or'
items(SuiGridFilterCondition | SuiGridFilterGroup)[]
SuiGridFilterModel interfaceThe whole filter state. Rules are keyed by column so a header can ask "is this column filtered" in one lookup; the global term is a sibling rather than a magic key inside the map, because it is a different shape and a map that holds two shapes cannot be typed. tree holds what only a tree can say, conditions across columns joined by or or nested; the three are joined by and.
rules?Readonly<Record<string, SuiGridFilterRule>>
global?SuiGridGlobalFilter
tree?SuiGridFilterGroup
SuiGridFilterRule interfaceEvery constraint on one column, and how they combine. and is the default a filter menu opens with; or is what a user reaches for when one column can hold two acceptable answers.
keystringThe column this rule filters. It is repeated inside the rule, rather than being left to the map key in SuiGridFilterModel, because a rule also travels alone in an event detail and in saved state, where there is no map key to read. The evaluator reads this field, so the two can never quietly disagree about which column a rule applies to.
operator'and' | 'or'
constraintsSuiGridFilterConstraint[]
SuiGridGlobalFilter interfaceOne term searched across several columns at once. keys is required rather than defaulting to every column, for the same reason PrimeVue makes globalFilterFields explicit: a computed column's value function would otherwise be called once per row per keystroke without anyone having asked for it.
valuestring
keysreadonly string[]
matchMode?SuiGridMatchModeDefaults to contains.
SuiGridMatchMode typeHow one constraint tests a value. Declared here rather than in filter.ts because a column names one and every element reads it, which is what this file is for; the evaluator that implements them is in filter.ts, which re-exports this so both can be imported together. The first sixteen are PrimeVue's FilterMatchMode verbatim. Three are additions, and they are additions rather than oversights: notIn because in without its negation is an odd half of a pair, and empty / notEmpty because "rows with no memo" is the filter a ledger reaches for most and PrimeVue cannot express it at all (an empty filter value means "no filter" there, so emptiness is exactly the one thing a value-carrying constraint can never ask about).
No members.
SuiGridPin typeWhich edge a column is pinned to, if any. The grid renders a pane per edge; the table uses position: sticky cells, because a native table cannot be split into panes.
No members.
SuiGridRow typeA row is the host's own record. The elements read fields by column key, print them and compare them; they never assume a shape beyond that.
No members.
SuiGridValidator typeReturns null when the value is acceptable, or the message to show when it is not.
No members.
SuiGridValueType typeHow a column's values are compared, formatted and filtered when the column does not say otherwise. text is the fallback for anything unrecognised.
No members.
SuiKey typeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
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.

Color22

--accentthe one chart highlight: the flagged series, down candles, the peak cell--accent-inkorange shaded for small text on light--accent-markthe accent drawn as a mark on a page: active bars, sort arrows, the logotype--accent-softorange 18% on white, warning fills, highlights--accent-soft-ink--bordercard + table outer borders--border-strongcontrol borders, emphasized rules--brand-softnavy 10% on white, quiet chips, selected rows--brand-soft-ink--control-borderthe resting edge of every field and control: 3:1 on its grounds--disabled-bg--focus-ring--hairlinerow separators--ink--ink-faintmetadata beside body ink: 4.8:1 on the page grounds (5.4:1 in dark)--ink-mutedsecondary text: 7:1 on the page grounds and 1.5 times faint--ink-placeholdera field's placeholder, not a text tier: 4.5:1 on the page grounds--scrim--surfacecards, tables, inputs--surface-hovera hovered row or quiet control on any ground: the ink, translucent--surface-raisedmenus, popovers, toasts: the card in light, a step above it in dark--surface-sunkentable headers, wells

Typography4

Ag--font-sans13.5--text-mdDEFAULT, table cells, controls, body in-app11.5--text-xspills, column headers (11px), captions0.06--tracking-wideuppercase column headers

Spacing and size3

34--control-mdDEFAULT toolbar/filter controls28--control-sm700--z-dropdown

Radius4

--radius-lgcards, tables, panels, dialogs--radius-mdALL controls: inputs, buttons, selects--radius-smlegend keys, small chips--radius-xscheckboxes, mini swatches

Elevation3

--focus-shadow--shadow-popdropdowns, popovers, toasts--shadow-raisedhover-lift, dragged rows

Motion3

--duration-fasthovers, presses, every exit--duration-overlayevery entrance: palette, popover, dialog, drawer, toast, menu, select list--ease-out

Every name above is listed with its value and its role in Foundations.

See also