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 toolbar with search, per-column filters, grouping controls, summary aggregations, and a column visibility menu. Which columns take part is decided on the column, not on the bar: a column with an accessor is searched and gets its own filter entry unless it carries searchable: false, and its dataType decides which operators that filter offers (Column Configuration).

Smart Filter Bar

Search across all searchable columns. Add per-column filters via the filter button. searchDebounceMs waits out every edit, clearing included: the term reaches the table 300ms after the last keystroke.
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 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 splits into a column list plus a separate direction control rather than offering every column×direction pair, and summaries become one aggregation choice per summable column (which is why this demo declares two numeric columns).

The sheet mirrors the capsule exactly, including what is missing from it: grouping drops out while the table is virtualized, column visibility when enableColumnVisibility is off. The demo below has all five tools.

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 sheet is a bottom Drawer, so it spans the window, not the bar: here a 360px table opens a full-width sheet. On a phone the two are usually the same width, though a bar inside a card is narrower than the sheet there 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

This is not the table's own layout switch. Rows become cards below cardsBelow of the table's container (48rem by default), which is why the demo above is a card list at every width the slider reaches. The bar's 28rem is separate from it and has no prop: set cardsBelow to move the rows-to-cards step, and the tools keep switching where they always did.

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 threshold is one 28rem container step, the same one that stacks the search field above the tools, and it resolves against your root font size: at a larger text size the same bar moves its tools into the sheet at a larger pixel width.

Only layout="responsive", the bar's default, switches. horizontal and vertical are explicit instructions and are left alone at any width; all three live on SmartFilterBar, which you render yourself through the table's toolbar snippet when you need one of them. 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, at most five: one each for active filters (however many), a sort column, a grouping, a summary row that is switched on, and hidden columns. 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, because 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. -->
<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. The six operators below are the whole of FilterOperator; which of them the menu offers is driven by the column's dataType, and a type with no set of its own (boolean, email, url) gets the text operators. 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-insensitive; on a date column, the whole UTC daytext, 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 value filters everything out instead of matching everything. An empty value is not an assertion at all and keeps every row, whichever operator it carries; the filter menu never produces one, but a seeded or stored filter can.

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

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, and users can still remove or add filters. The same array is what view.filters holds, so assigning to it drives filters from your own UI the way the next section drives the search term.

The search field matches a case-insensitive substring, and both it and the filters read the column accessor's output rather than 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.