Data Table Side Filters
An always-visible side filter panel for e-commerce-style listings — covers every Docyrus field type and emits the same RuleGroupType JSON as the Query Builder.
Installation
pnpm dlx @docyrus/cli add @docyrus/ui-data-table-side-filtersnpx shadcn@latest add avatar badge button calendar checkbox command input popover sliderUsage
DataTableSideFilters is built on top of the same ColumnConfig schema used by DataTableFilter, so a single column definition can power both the popover-style filter bar and the persistent side panel. The side panel emits its filter state as a RuleGroupType (the JSON shape react-querybuilder expects), so the same payload feeds the Query Builder, your saved-views layer, and your back end.
import {
DataTableSideFilters,
useDataTableSideFilters
} from '@docyrus/ui/components/data-table-side-filters';
const columnsConfig = [
{
id: 'category',
displayName: 'Category',
icon: ShoppingBagIcon,
type: 'option' as const,
accessor: (row: Product) => row.category,
options: categoryOptions
},
{
id: 'tags',
displayName: 'Features',
icon: StarIcon,
type: 'multiOption' as const,
accessor: (row: Product) => row.tags,
options: tagOptions
},
{
id: 'vendorId',
displayName: 'Vendor',
icon: UserIcon,
type: 'multiOption' as const,
accessor: (row: Product) => [row.vendorId],
asyncOptions: {
load: async ({ search, page, pageSize, signal }) => {
const res = await fetch(`/api/vendors?q=${search}&page=${page}&size=${pageSize}`, { signal });
return res.json(); // → { items: ColumnOption[], hasMore: boolean }
}
}
},
{
id: 'price',
displayName: 'Price',
icon: DollarSignIcon,
type: 'number' as const,
accessor: (row: Product) => row.price,
min: 0,
max: 500
},
{
id: 'releasedAt',
displayName: 'Released',
icon: CalendarIcon,
type: 'date' as const,
accessor: (row: Product) => row.releasedAt
},
{
id: 'inStock',
displayName: 'In stock',
icon: CircleCheckIcon,
type: 'boolean' as const,
accessor: (row: Product) => row.inStock
}
] as const;
function ProductFilters({ products }: { products: Array<Product> }) {
const [query, setQuery] = useState<RuleGroupType>({ combinator: 'and', rules: [] });
const { columns, filters, actions, strategy } = useDataTableSideFilters({
strategy: 'client',
data: products,
columnsConfig,
query,
onQueryChange: setQuery
});
return (
<DataTableSideFilters
columns={columns}
filters={filters}
actions={actions}
strategy={strategy}
title="Filter products"
searchable="name"
variant="bordered" />
);
}Render mode resolution
Each column picks its UI by column.type, with overrides via the defaults prop:
| Column type | Default render mode | Notes |
|---|---|---|
text | Text input | Debounced; auto-applies the contains operator |
number | Slider + Min/Max inputs | Validates min ≤ max, clamps to column min/max |
date | Date range picker | Quick presets (Today / Last 7 days / This month / etc.) |
boolean | Tri-state row (Any / True / False) | "Any" removes the filter |
option (≤ threshold) | Inline checkbox list | Shows faceted counts inline |
option (large list) | Inline checkbox list with Show more | Selected items stay pinned above the fold |
multiOption | Same as option | Add to/remove from the value list |
option / multiOption with asyncOptions | Dropdown popover with chips | Debounced server search + paginated load |
Set defaults[columnId].mode = 'dropdown-chips' to force a long option list into a popover with a chip strip below the trigger — useful for select-style fields with many values that you don't want to inline.
Sections, sticky filters & Show more
Group filters into named sections with the sections prop, and pin "always-visible" filters above other sections via defaults[id].sticky:
<DataTableSideFilters
columns={columns}
filters={filters}
actions={actions}
strategy={strategy}
defaults={{
inStock: { sticky: true },
freeShipping: { sticky: true },
tags: { showMoreThreshold: 6 },
rating: { collapsed: true }
}}
sections={[
{ id: 'shop', title: 'Shop', columnIds: ['category', 'brand', 'vendorId'] },
{ id: 'pricing', title: 'Pricing & Quality', columnIds: ['price', 'rating'] },
{ id: 'features', title: 'Features', columnIds: ['tags', 'releasedAt'] }
]} />Query JSON output
Whenever the user changes a value, the panel emits a RuleGroupType like:
{
"combinator": "and",
"rules": [
{ "field": "category", "operator": "in", "value": ["audio", "wearables"] },
{ "field": "price", "operator": "between", "value": [50, 250] },
{ "field": "tags", "operator": "containsAll", "value": ["wireless", "noise-cancelling"] },
{ "field": "vendorId", "operator": "in", "value": ["v-1", "v-3"] },
{ "field": "releasedAt", "operator": "between", "value": ["2024-01-01T00:00:00Z", "2024-12-31T23:59:59Z"] },
{ "field": "inStock", "operator": "=", "value": true }
]
}This is the exact shape the Query Builder consumes. You can wire both components to the same query / setQuery state and let the user switch between e-commerce-style facet picking and rule-builder editing without translating data.
Override the operator vocabulary with the operatorMap option if your back end expects different operator strings:
useDataTableSideFilters({
// ...
operatorMap: {
...DEFAULT_OPERATOR_MAP,
text: { contains: 'ilike', 'does not contain': 'not_ilike' }
}
});Features
- Field-aware UI — text, number range, date range, boolean, options, multi-options, and async relations all render with the right control.
- Show-more pattern — long option lists collapse to a configurable threshold; selected items stay pinned so they're never hidden.
- Chip strips for chosen values — every popover-driven filter (relation, async, or large select) renders selected values as removable chips below the trigger.
- Built-in search input — pass
searchableto surface a debounced text search at the top of the panel. - Sticky / collapsible / hidden sections — fine-grained control via the
defaultsprop. - Active-chip strip & clear actions — global "Clear all" plus per-section "Clear" out of the box.
- QueryBuilder-compatible JSON — emits
RuleGroupTypewith operator translation; reverse translator rehydrates a side-panel from any saved query. - Controlled & uncontrolled — pass
defaultQueryfor uncontrolled, orquery+onQueryChangefor full external control. - Reuses DataTableFilter operators & faceted helpers — no duplicate operator metadata or counting logic.
API Reference
useDataTableSideFilters
The main hook. Manages internal FiltersState, translates to/from RuleGroupType, and returns the values you pass into <DataTableSideFilters>.
| Prop | Type | Default | Description |
|---|---|---|---|
strategy | 'client' | 'server' | — | Filter strategy (mirrors DataTableFilter) |
data | Array<TData> | — | Row data (used for client-side faceting) |
columnsConfig | ReadonlyArray<ColumnConfig<TData>> | — | Column configuration array |
defaultQuery | RuleGroupType | — | Initial query (uncontrolled) |
query | RuleGroupType | — | Controlled query value |
onQueryChange | (query: RuleGroupType) => void | — | Emitted whenever the user changes a filter |
combinator | 'and' | 'or' | 'and' | Top-level combinator on the emitted RuleGroup |
operatorMap | SideFilterOperatorMap | DEFAULT_OPERATOR_MAP | Override DTF→QB operator mapping |
defaults | SideFilterDefaults | — | Per-column UI hints |
sections | ReadonlyArray<SideFilterSectionGroup> | — | Optional named groupings |
options | Partial<Record<...>> | — | Server-supplied options for option/multiOption columns |
faceted | Partial<Record<...>> | — | Server-supplied faceted counts / min-max tuples |
calculateFacets | boolean | true | Compute facets from data (turn off when supplying server-side) |
Returns:
| Field | Type | Description |
|---|---|---|
columns | Array<Column<TData>> | Enriched column objects |
filters | FiltersState | Internal flat filter state |
actions | DataTableFilterActions | Same actions as useDataTableFilters |
strategy | FilterStrategy | Active strategy |
query | RuleGroupType | Currently emitted RuleGroup |
defaults | SideFilterDefaults | undefined | Pass-through for the panel |
sections | ReadonlyArray<SideFilterSectionGroup> | undefined | Pass-through for the panel |
reset | () => void | Clear all filters |
DataTableSideFilters
| Prop | Type | Default | Description |
|---|---|---|---|
columns | Array<Column<TData>> | — | From useDataTableSideFilters |
filters | FiltersState | — | From useDataTableSideFilters |
actions | DataTableFilterActions | — | From useDataTableSideFilters |
strategy | FilterStrategy | — | From useDataTableSideFilters |
defaults | SideFilterDefaults | — | Per-column UI hints |
sections | ReadonlyArray<SideFilterSectionGroup> | — | Named groupings |
locale | Locale | 'en' | i18n locale (shared with DataTableFilter) |
title | ReactNode | 'Filters' | Panel header title |
showActiveChips | boolean | true | Show the active-filter chip strip |
showClearAll | boolean | true | Show "Clear all" in the header |
searchable | boolean | string | false | Show a search input above the sections; pass a column id to bind to a specific text column |
variant | 'default' | 'bordered' | 'compact' | 'default' | Visual style |
clearAllLabel | string | 'Clear all' | Label for the global clear button |
clearLabel | string | 'Clear' | Label for per-section clear |
className | string | — | Extra classes on the panel root |
SideFilterColumnDefaults
| Field | Type | Description |
|---|---|---|
mode | 'auto' | 'text-input' | 'inline-checkbox' | 'dropdown-chips' | 'date' | 'date-range' | 'numeric-range' | 'boolean' | Force a specific render mode |
showMoreThreshold | number | Collapse inline checkbox list above N items (default 8) |
collapsed | boolean | Section starts collapsed |
sticky | boolean | Pin section above all unpinned sections |
title | ReactNode | Override the section title (defaults to column.displayName) |
hidden | boolean | Hide the column from the panel completely |
SideFilterSectionGroup
| Field | Type | Description |
|---|---|---|
id | string | Unique group id |
title | ReactNode | Group heading |
columnIds | ReadonlyArray<string> | Column ids that belong to this group, in render order |
withDivider | boolean | Render a divider above the group title (default true) |
Translator helpers
Standalone utilities exposed for advanced use (e.g. saved-views, hydrating from URL):
| Export | Signature | Description |
|---|---|---|
filtersStateToRuleGroup | (state: FiltersState, combinator?, operatorMap?) => RuleGroupType | Convert internal filters to a QueryBuilder rule group |
ruleGroupToFiltersState | (group: RuleGroupType, columns, operatorMap?) => FiltersState | Reverse: parse a rule group into internal filters (drops rules whose field doesn't match a known column) |
DEFAULT_OPERATOR_MAP | SideFilterOperatorMap | The default DTF → QB operator mapping (contains, =, between, in, etc.) |
Sub-components
The panel is composed of building blocks you can reach for directly when you need a custom layout. They're all exported from the same entry point:
| Component | Description |
|---|---|
SideFilterSection | Collapsible section wrapper used per column |
SideFilterActiveChips | Active-filter chip strip with per-chip remove |
SideFilterSearch | Debounced text-search bound to a specific text column |
SideFilterClear | "Clear all" button (shows count) |
SideFilterText | Single-column text input controller |
SideFilterCheckboxList | Inline checkbox list with "Show more" |
SideFilterOptionsDropdown | Static-options dropdown with chips |
SideFilterAsyncOptions | Async-loaded dropdown with chips |
SideFilterDateRange | Date-range picker with presets |
SideFilterNumericRange | Numeric range slider + min/max inputs with validation |
SideFilterBoolean | Tri-state radio (Any / True / False) |
Type Exports
| Type | Description |
|---|---|
DataTableSideFiltersProps | Props for <DataTableSideFilters> |
DataTableSideFiltersOptions | Options for useDataTableSideFilters() |
UseDataTableSideFiltersReturn | Return type of useDataTableSideFilters() |
SideFilterColumnDefaults | Per-column UI overrides |
SideFilterDefaults | Record<string, SideFilterColumnDefaults> |
SideFilterSectionGroup | Section grouping descriptor |
SideFilterRenderMode | All render-mode strings |
SideFilterCombinator | 'and' | 'or' |
SideFilterOperatorMap | DTF → QB operator override map |