DataTableSideFilters
E-commerce-style facet filters for React Native — a "Filters (n)" trigger that opens a large bottom sheet (or an inline panel on tablets), covering every column type and emitting the same RuleGroupType JSON as the Query Builder. API-aligned with the web DataTableSideFilters.
Installation
pnpm dlx @docyrus/cli add @docyrus/rn-data-table-side-filterspnpm add @react-querybuilder/core date-fns tailwind-variantsDataTableSideFilters is built on the same ColumnConfig schema as DataTableFilter, so one column definition powers both the filter wizard and the facet panel. useDataTableSideFilters wraps useDataTableFilters and translates the internal FiltersState to and from a react-querybuilder RuleGroupType, so the emitted query feeds the Query Builder, saved views and your back end unchanged.
Usage
import { useState } from 'react';
import {
DataTableSideFilters,
useDataTableSideFilters,
type RuleGroupType
} from '@/components/docyrus-native/data-table-side-filters';
const columnsConfig = [
{
id: 'category',
displayName: 'Category',
icon: 'fal bag-shopping',
type: 'option' as const,
accessor: (row: Product) => row.category,
options: categoryOptions
},
{
id: 'vendorId',
displayName: 'Vendor',
icon: 'fal user',
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: 'fal dollar-sign',
type: 'number' as const,
accessor: (row: Product) => row.price,
min: 0,
max: 500
},
{
id: 'releasedAt',
displayName: 'Released',
icon: 'fal calendar',
type: 'date' as const,
accessor: (row: Product) => row.releasedAt
},
{
id: 'inStock',
displayName: 'In stock',
icon: 'fal circle-check',
type: 'boolean' as const,
accessor: (row: Product) => row.inStock
}
] as const;
export function ProductFilters({ products }: { products: 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" />
);
}Presentation
presentation="sheet"(default): renders an outlineFilters (n)button. Pressing it opens a large-detentActionSheetwith Clear all on the left and Done on the right. The search input and the active-filter chips sit in the sheet's fixed header, and the sections scroll below them. Filters apply live, so Done only closes the sheet. Replace the button withrenderTrigger, and control visibility withopen/defaultOpen/onOpenChange.presentation="inline": renders the panel in place (tablets, a dedicated filter screen). Withcollapsible, the header gets a collapse button and the collapsed state is a full-width bar (Filters · n) that expands on press.
<DataTableSideFilters
{...filterProps}
presentation="inline"
variant="bordered"
collapsible
defaultExpanded={false} />Render mode resolution
Each column picks its UI by column.type. You can override the mode with defaults[columnId].mode:
| Column type | Default render mode | Native UI |
|---|---|---|
text | text-input | Debounced input (300 ms), applies contains. |
number | numeric-range | Min / Max inputs, clamped to the faceted or column.min / column.max bounds. Shows an error when min > max. |
date | date-range | Preset chips (Today, Yesterday, Last 7 days, Last 30 days, This week, This month, This year) plus From / To DateTimePickers. |
boolean | boolean | Any / True / False ToggleGroup. "Any" removes the filter. |
option / multiOption | inline-checkbox | Checkbox rows with faceted counts. Past showMoreThreshold (default 8) the list collapses behind Show more, with selected options pinned above the fold. |
option / multiOption with asyncOptions | dropdown-chips | A trigger that opens a searchable, paginated picker sheet, with the selection shown as chips. |
Set defaults[columnId].mode = 'dropdown-chips' to move a long static option list into a picker sheet.
Sections, sticky filters and Show more
<DataTableSideFilters
{...filterProps}
defaults={{
inStock: { sticky: true },
tags: { showMoreThreshold: 6 },
rating: { collapsed: true },
internalCode: { hidden: true }
}}
sections={[
{ id: 'shop', title: 'Shop', columnIds: ['category', 'brand', 'vendorId'] },
{ id: 'pricing', title: 'Pricing & Quality', columnIds: ['price', 'rating'] }
]} />Without sections, sticky columns render first and the rest follow. With sections, columns you did not list fall into an Other group at the end.
Query JSON output
useDataTableSideFilters emits a RuleGroupType through onQueryChange whenever a filter changes, and returns it as query. Operators are mapped through DEFAULT_OPERATOR_MAP (for example contains → like, is between → between, is any of → in, include any of → contains any). Pass operatorMap to use a different vocabulary. Date values are serialized as timezone-naive yyyy-MM-dd'T'HH:mm:ss strings. For date-only columns, the start side is snapped to 00:00:00 and the end side to 23:59:59, so the whole boundary day is included.
API Reference
DataTableSideFiltersProps
| Prop | Type | Default | Description |
|---|---|---|---|
columns | Column<TData>[] | — | Columns from useDataTableSideFilters() (required). |
filters | FiltersState | — | Current filter state (required). |
actions | DataTableFilterActions | — | Filter actions from the hook (required). |
strategy | FilterStrategy | — | 'client' or 'server' (required). |
defaults | SideFilterDefaults | — | Per-column UI hints (mode, threshold, collapsed, sticky, title, hidden). |
sections | ReadonlyArray<SideFilterSectionGroup> | — | Group sections under named headings. |
locale | Locale | 'en' | Bundled filter-core string catalog. UiTranslationProvider keys win. |
variant | 'default' | 'bordered' | 'compact' | 'default' | Panel style (bordered = rounded card with padding). |
className | string | — | Classes for the trigger button (sheet), the panel (inline) or the collapsed bar. |
title | ReactNode | t('ui.dataTableSideFilters.title', 'Filters') | Header title. Pass null to hide it. The sheet title only shows string titles. |
showActiveChips | boolean | true | Show removable chips for the active filters. |
showClearAll | boolean | true | Show Clear all (n) while any filter is active. |
searchable | boolean | string | false | true drives the first text column with a search input; a column id drives that column. The searched column is removed from the sections. |
clearAllLabel | string | t('ui.dataTableSideFilters.clearAll', 'Clear all') | "Clear all" label. |
clearLabel | string | t('ui.dataTableSideFilters.clear', 'Clear') | Per-section "Clear" label. |
presentation | 'sheet' | 'inline' | 'sheet' | Native: trigger + bottom sheet, or the panel in place. |
triggerLabel | string | title, then 'Filters' | Native (sheet): trigger label. The active count is appended as (n). |
renderTrigger | (props: { open: () => void; activeCount: number; label: string }) => ReactNode | — | Native (sheet): replace the trigger button. |
open | boolean | — | Native (sheet): controlled sheet visibility. |
defaultOpen | boolean | false | Native (sheet): initial sheet visibility (uncontrolled). |
onOpenChange | (open: boolean) => void | — | Native (sheet): called when the sheet opens or closes. |
collapsible | boolean | false | Inline: the panel can collapse to a compact bar. |
expanded | boolean | — | Inline: controlled expanded state. |
defaultExpanded | boolean | true | Inline: uncontrolled initial expanded state. |
onExpandedChange | (expanded: boolean) => void | — | Inline: called when the user collapses or expands the panel. |
collapsedWidth | number | string | — | Accepted for web parity. The collapsed bar is full-width on native. |
expandLabel | ReactNode | title, then 'Filters' | Inline: label of the collapsed bar. |
collapseAriaLabel | string | t('ui.dataTableSideFilters.collapse', 'Collapse filters') | Accessibility label of the collapse button. |
expandAriaLabel | string | t('ui.dataTableSideFilters.expand', 'Expand filters') | Accessibility label of the collapsed bar. |
useDataTableSideFilters options (DataTableSideFiltersOptions)
Extends the useDataTableFilters options (without defaultFilters / filters / onFiltersChange, which the hook owns).
| Option | Type | Default | Description |
|---|---|---|---|
strategy | FilterStrategy | — | 'client' (in-memory facets) or 'server' (required). |
data | TData[] | — | Rows used for faceting (required; pass [] for server strategy). |
columnsConfig | ReadonlyArray<ColumnConfig<TData>> | — | Column definitions (required). |
calculateFacets | boolean | true | Compute faceted counts / min-max from data. |
options | Partial<Record<OptionColumnId, ColumnOption[]>> | — | External options per option column. |
faceted | Partial<Record<ColumnId, Map<string, number> | [number, number]>> | — | External facets per column. |
defaultQuery | RuleGroupType | — | Uncontrolled initial query, translated to FiltersState on mount. |
query | RuleGroupType | — | Controlled query. Pair it with onQueryChange. |
onQueryChange | (query: RuleGroupType) => void | — | Receives the translated query on every change. |
combinator | 'and' | 'or' | 'and' | Top-level combinator of the emitted group. |
operatorMap | SideFilterOperatorMap | DEFAULT_OPERATOR_MAP | Filter operator → query-builder operator vocabulary. |
defaults | SideFilterDefaults | — | Passed through in the result so you can spread it onto the component. |
sections | ReadonlyArray<SideFilterSectionGroup> | — | Passed through in the result. |
The hook returns { columns, filters, actions, strategy, query, defaults, sections, reset }. reset() clears every filter.
SideFilterColumnDefaults
| Field | Type | Description |
|---|---|---|
mode | SideFilterRenderMode | 'auto' | 'text-input' | 'inline-checkbox' | 'dropdown-chips' | 'date' | 'date-range' | 'numeric-range' | 'boolean'. |
showMoreThreshold | number | Options shown before Show more (default 8). |
collapsed | boolean | The section starts collapsed. |
sticky | boolean | Pin the section above the unpinned sections. |
title | ReactNode | Override the section title (default column.displayName). |
hidden | boolean | Remove the column from the panel. |
SideFilterSectionGroup
| Field | Type | Description |
|---|---|---|
id | string | Group id. |
title | ReactNode | Group heading. |
columnIds | ReadonlyArray<string> | Columns in this group, in order. |
withDivider | boolean | Divider above the heading (default true). |
Building blocks
| Component | Props |
|---|---|
SideFilterSection | id, title, icon?, activeCount? (0), defaultCollapsed? (false), onClear?, clearLabel? ('Clear'), children |
SideFilterActiveChips | filters, columns, actions, clearAllLabel?, layout? ('scroll' | 'wrap', default 'scroll') |
SideFilterClear | count, onClick, label? ('Clear all'), className?. Renders nothing while count is 0. |
SideFilterSearch | columnId, columns, filters, actions, locale?, placeholder? |
SideFilterText | column, filter?, actions, locale?, placeholder? |
SideFilterNumericRange | column, filter?, actions, locale? |
SideFilterDateRange | column, filter?, actions, locale?, withPresets? (default true) |
SideFilterBoolean | column, filter?, actions, locale? |
SideFilterCheckboxList | column, filter?, actions, locale?, threshold? |
SideFilterOptionsDropdown | column, filter?, actions, locale?, placeholder? |
SideFilterAsyncOptions | column, filter?, actions, locale?, placeholder? |
Native differences
- A persistent side column does not fit a phone, so the default presentation is a trigger plus a large bottom sheet. The web layout is available as
presentation="inline". - Numeric ranges use Min / Max inputs without a slider.
- Option pickers (
dropdown-chipsand async options) open a picker sheet instead of a popover. - New native-only props:
presentation,triggerLabel,renderTrigger,open,defaultOpen,onOpenChange, andlayoutonSideFilterActiveChips.collapsedWidthis accepted but ignored. - Column
icons areDocyrusIconnames (for example'fal tag'), elements or components, instead of Lucide icons.
Translation keys
Panel strings use ui.dataTableSideFilters.* with English fallbacks: title, clearAll, clear, other, done, expand, collapse, any, selected, showMore, showLess, minMaxOrder, asyncNotConfigured. Filter-core strings (search, min / max, true / false, date presets) come from the ui.dataTableFilter.* keys shared with DataTableFilter.
Components
| Component | Description |
|---|---|
DataTableSideFilters | Trigger + sheet, or the inline panel. |
SideFilterSection | Collapsible per-column section with active count and Clear. |
SideFilterActiveChips | Removable chips for every active filter (usable outside the panel). |
SideFilterClear | "Clear all (n)" link. |
SideFilterSearch | Debounced search bound to one text column. |
SideFilterText / SideFilterNumericRange / SideFilterDateRange / SideFilterBoolean / SideFilterCheckboxList / SideFilterOptionsDropdown / SideFilterAsyncOptions | Per-mode controllers. |
Helpers
| Export | Description |
|---|---|
useDataTableSideFilters(options) | Filter state + query translation. |
filtersStateToRuleGroup(filters, combinator, operatorMap, columns) | FiltersState → RuleGroupType. |
ruleGroupToFiltersState(query, columns, operatorMap) | RuleGroupType → FiltersState. |
DEFAULT_OPERATOR_MAP | Default operator vocabulary. |
dataTableSideFiltersVariants | tv() variants of the panel. |
Type Exports
| Type | Description |
|---|---|
DataTableSideFiltersProps | Component props. |
DataTableSideFiltersPresentation | 'sheet' | 'inline'. |
DataTableSideFiltersOptions | Hook options. |
UseDataTableSideFiltersReturn | Hook result. |
SideFilterRenderMode | Render modes. |
SideFilterColumnDefaults / SideFilterDefaults | Per-column hints. |
SideFilterSectionGroup | Section grouping. |
SideFilterCombinator | 'and' | 'or'. |
SideFilterOperatorMap | Operator vocabulary map. |
RuleGroupType | Re-exported from @react-querybuilder/core. |