Components

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.

Client Only

Installation

pnpm dlx @docyrus/cli add @docyrus/ui-data-table-side-filters
UI Primitives(9 components)
npx shadcn@latest add avatar badge button calendar checkbox command input popover slider

Usage

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 typeDefault render modeNotes
textText inputDebounced; auto-applies the contains operator
numberSlider + Min/Max inputsValidates min ≤ max, clamps to column min/max
dateDate range pickerQuick presets (Today / Last 7 days / This month / etc.)
booleanTri-state row (Any / True / False)"Any" removes the filter
option (≤ threshold)Inline checkbox listShows faceted counts inline
option (large list)Inline checkbox list with Show moreSelected items stay pinned above the fold
multiOptionSame as optionAdd to/remove from the value list
option / multiOption with asyncOptionsDropdown popover with chipsDebounced 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 searchable to surface a debounced text search at the top of the panel.
  • Sticky / collapsible / hidden sections — fine-grained control via the defaults prop.
  • Active-chip strip & clear actions — global "Clear all" plus per-section "Clear" out of the box.
  • QueryBuilder-compatible JSON — emits RuleGroupType with operator translation; reverse translator rehydrates a side-panel from any saved query.
  • Controlled & uncontrolled — pass defaultQuery for uncontrolled, or query + onQueryChange for 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>.

PropTypeDefaultDescription
strategy'client' | 'server'—Filter strategy (mirrors DataTableFilter)
dataArray<TData>—Row data (used for client-side faceting)
columnsConfigReadonlyArray<ColumnConfig<TData>>—Column configuration array
defaultQueryRuleGroupType—Initial query (uncontrolled)
queryRuleGroupType—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
operatorMapSideFilterOperatorMapDEFAULT_OPERATOR_MAPOverride DTF→QB operator mapping
defaultsSideFilterDefaults—Per-column UI hints
sectionsReadonlyArray<SideFilterSectionGroup>—Optional named groupings
optionsPartial<Record<...>>—Server-supplied options for option/multiOption columns
facetedPartial<Record<...>>—Server-supplied faceted counts / min-max tuples
calculateFacetsbooleantrueCompute facets from data (turn off when supplying server-side)

Returns:

FieldTypeDescription
columnsArray<Column<TData>>Enriched column objects
filtersFiltersStateInternal flat filter state
actionsDataTableFilterActionsSame actions as useDataTableFilters
strategyFilterStrategyActive strategy
queryRuleGroupTypeCurrently emitted RuleGroup
defaultsSideFilterDefaults | undefinedPass-through for the panel
sectionsReadonlyArray<SideFilterSectionGroup> | undefinedPass-through for the panel
reset() => voidClear all filters

DataTableSideFilters

PropTypeDefaultDescription
columnsArray<Column<TData>>—From useDataTableSideFilters
filtersFiltersState—From useDataTableSideFilters
actionsDataTableFilterActions—From useDataTableSideFilters
strategyFilterStrategy—From useDataTableSideFilters
defaultsSideFilterDefaults—Per-column UI hints
sectionsReadonlyArray<SideFilterSectionGroup>—Named groupings
localeLocale'en'i18n locale (shared with DataTableFilter)
titleReactNode'Filters'Panel header title
showActiveChipsbooleantrueShow the active-filter chip strip
showClearAllbooleantrueShow "Clear all" in the header
searchableboolean | stringfalseShow a search input above the sections; pass a column id to bind to a specific text column
variant'default' | 'bordered' | 'compact''default'Visual style
clearAllLabelstring'Clear all'Label for the global clear button
clearLabelstring'Clear'Label for per-section clear
classNamestring—Extra classes on the panel root

SideFilterColumnDefaults

FieldTypeDescription
mode'auto' | 'text-input' | 'inline-checkbox' | 'dropdown-chips' | 'date' | 'date-range' | 'numeric-range' | 'boolean'Force a specific render mode
showMoreThresholdnumberCollapse inline checkbox list above N items (default 8)
collapsedbooleanSection starts collapsed
stickybooleanPin section above all unpinned sections
titleReactNodeOverride the section title (defaults to column.displayName)
hiddenbooleanHide the column from the panel completely

SideFilterSectionGroup

FieldTypeDescription
idstringUnique group id
titleReactNodeGroup heading
columnIdsReadonlyArray<string>Column ids that belong to this group, in render order
withDividerbooleanRender a divider above the group title (default true)

Translator helpers

Standalone utilities exposed for advanced use (e.g. saved-views, hydrating from URL):

ExportSignatureDescription
filtersStateToRuleGroup(state: FiltersState, combinator?, operatorMap?) => RuleGroupTypeConvert internal filters to a QueryBuilder rule group
ruleGroupToFiltersState(group: RuleGroupType, columns, operatorMap?) => FiltersStateReverse: parse a rule group into internal filters (drops rules whose field doesn't match a known column)
DEFAULT_OPERATOR_MAPSideFilterOperatorMapThe 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:

ComponentDescription
SideFilterSectionCollapsible section wrapper used per column
SideFilterActiveChipsActive-filter chip strip with per-chip remove
SideFilterSearchDebounced text-search bound to a specific text column
SideFilterClear"Clear all" button (shows count)
SideFilterTextSingle-column text input controller
SideFilterCheckboxListInline checkbox list with "Show more"
SideFilterOptionsDropdownStatic-options dropdown with chips
SideFilterAsyncOptionsAsync-loaded dropdown with chips
SideFilterDateRangeDate-range picker with presets
SideFilterNumericRangeNumeric range slider + min/max inputs with validation
SideFilterBooleanTri-state radio (Any / True / False)

Type Exports

TypeDescription
DataTableSideFiltersPropsProps for <DataTableSideFilters>
DataTableSideFiltersOptionsOptions for useDataTableSideFilters()
UseDataTableSideFiltersReturnReturn type of useDataTableSideFilters()
SideFilterColumnDefaultsPer-column UI overrides
SideFilterDefaultsRecord<string, SideFilterColumnDefaults>
SideFilterSectionGroupSection grouping descriptor
SideFilterRenderModeAll render-mode strings
SideFilterCombinator'and' | 'or'
SideFilterOperatorMapDTF → QB operator override map

On this page