Skip to main content
Urbicon UI

Filtering & Search

Built-in search, column filters, summary controls, and column visibility via the SmartFilterBar — plus an external search field wired through the table's view.

Smart Filter Bar

Enable enableSmartFilter to get a full-featured toolbar with search, per-column filters, grouping controls, summary aggregations, and a column visibility menu.

Smart Filter Bar

Search across all searchable columns. Add per-column filters via the filter button. Debounce controls request frequency.
Name
Role
Department
Location
Emma Wilson
Staff Engineer
Platform
Berlin
Liam Chen
Product Designer
Design
Hamburg
Sofia Martinez
Engineering Manager
Platform
Munich
James Park
Frontend Developer
Product
Remote
Aisha Patel
Data Scientist
Data
Berlin
Noah Kim
DevOps Engineer
Platform
Hamburg
<Table
  {items}
  {columns}
  enableSmartFilter={true}
  searchPlaceholder="Search employees..."
  searchDebounceMs={300}
  viewDefaults={{ pageSize: 6 }}
/>

Narrow Bar & Tools Sheet

The bar's tools — filters, sort, grouping, summaries and column visibility — normally sit in a capsule beside the search field. Below 28rem (448px) of bar content box that capsule is gone: the tools move into a bottom sheet reached from a single button. Nothing is taken away, only relocated — each tool becomes a section of the sheet, rebuilt as a form instead of a menu. Sort, for instance, splits into a column list plus a separate direction control rather than offering every column×direction pair; summaries become one aggregation choice per summable column, which is why this demo declares two numeric columns.

There are up to five of them, not always five, and the sheet mirrors the capsule exactly: grouping drops out while the table is virtualized, column visibility when enableColumnVisibility is off. Three is the floor. The demo below has all five.

Drag the slider to squeeze the container past the threshold, then open the sheet. The switch is automatic — there is no prop for it. The readout is measured off the live bar rather than computed from the slider, so if the page is too narrow to give the demo the width it asks for, it says so instead of printing a number the bar never had. Note also that the sheet is a bottom Drawer, so it spans the window, not the bar: here a 360px table opens a full-width sheet. That gap closes when the bar is roughly the width of the window, which is the common phone case — but a bar inside a card is narrower than the sheet on a phone too.

Container width
Measuring the bar…
Name
Department
Salary
Projects
Sofia Martinez
Platform
165000
15
Alexander Novak
Data
148000
7
Emma Wilson
Platform
142000
12
Isabella Singh
Design
140000
18
Mia Zhang
Product
135000
14

The width is measured on the bar, not on the window. A filter bar can sit in a card, a drawer, a split pane or a dashboard tile, so a viewport media query would happily leave a 400px bar in the wide layout it has no room for. The bar observes its own content box with a ResizeObserver instead — the box a @container query measures. Reading clientWidth would include the bar's own padding and leave a dead band (24px at size="md") where neither this switch nor the row/stack one fires.

There is only one threshold, and CSS owns it. The same 28rem step the bar's @container rules use for the stacked/row switch also decides this one — it sets a custom property, and the component reads which side of it the bar is on. So the capsule can never be left standing in a layout too narrow to hold it, at any root font size. It could before: the tool switch compared a hardcoded 448px while @container resolved 28rem against the root, so raising the browser's text size opened a band where the bar had already stacked and the capsule was still there.

Only layout="responsive" — the default — switches. horizontal and vertical are explicit instructions from the consumer and are left alone at any width. Growing back past the threshold closes the sheet, so a bar that is narrowed again does not re-open it unprompted.

The badge on the button counts tools, not results. With the sheet shut, the lit triggers inside it are invisible, so the button carries the number of things currently acting on the grid: one each for active filters (however many), a sort column, a grouping, a summary row that is switched on, and hidden columns — at most five. The demo starts at 1 because its viewDefaults seed a sort; add a filter or a grouping in the sheet and watch it climb. Hidden columns count too — a column that is not on screen changes what the reader sees just as much as a filter does.

Reproducing It

Any container narrower than the threshold trips the switch — no prop, no media query, no viewport resize needed.
<!-- The bar measures its own CONTENT box, so the wrapper has to be
     narrower than 28rem plus the bar's padding and border (~474px at
     size="md") — max-w-md is exactly 28rem and still trips the switch.
     Nothing is opted into here; the bar decides by itself. -->
<div class="max-w-sm">
  <Table {items} {columns} enableSmartFilter />
</div>

Filter Operators

Per-column filters are added through the filter button in the SmartFilterBar. Each filter is a plain object — { column, operator, value } — and every active filter must match for a row to stay visible (AND semantics). value is always a string, even for the comparing operators — the comparison converts internally, which keeps filters serializable for persistence. Which operators the menu offers is driven by the column's dataType; columns with searchable: false (and synthetic columns without an accessor) do not appear in the filter menu at all.

OperatorMatchesOffered for
containsSubstring, case-insensitivetext (the default)
equalsThe whole stringified value, case-insensitivetext, number, date (labelled “on date”)
startsWithPrefix, case-insensitivetext
endsWithSuffix, case-insensitivetext
greaterThanNumbers when both sides convert, instants otherwisenumber, date (labelled “after”)
lessThanNumbers when both sides convert, instants otherwisenumber, date (labelled “before”)

Comparing operators resolve in two steps. If the cell value and the filter value both convert via Number(), they are compared as numbers. Otherwise both sides are read as instants — Date objects, numbers (epoch milliseconds) and ISO-8601 strings (2021-03-15, 2021-03-15T09:00, 2021-03-15T09:00:00Z). Any other string format never matches, so a malformed or empty value filters everything out instead of matching everything.

A date column's filter input emits a bare calendar date (YYYY-MM-DD), and for that shape after/before compare on UTC day boundaries: "after 2021-03-15" starts at the following midnight UTC and "before 2021-03-15" ends at that day's midnight UTC — a row stamped 2021-03-15T09:00Z matches neither. A filter value that carries a time of day compares instants strictly. Since a date-only string parses as UTC midnight while a date-time string without an offset parses as local time, a Date built from local parts (new Date(2021, 2, 15)) can fall into the neighbouring UTC day — store ISO strings or UTC-constructed dates for day-exact filtering.

To start with filters active, pass viewDefaults={{ filters: […] }} — an array of the same { column, operator, value } objects. That is the view's baseline: the chips show them, users can still remove or add filters, and bindViewToStorage(view, { key }) applies a stored set over it after hydration.

Both search and filters match against the column accessor's output — not against what a custom cell renders. With source={{ processing: 'server', … }} the table does not filter locally: the active filters arrive as filters on the view your backend is given — see Server Processing.