FilterPanel
The FilterPanel component provides a standalone, reusable filter UI that can be placed next to any data view. It renders as a positioned dropdown anchored below a trigger button and supports single-select, multi-select, numeric range (with histogram), and fully custom filter editors declared as children.
Key Features
Section titled “Key Features”- Automatic Clear Buttons: When a filter has active selections, a round clear button (×) appears in the filter header next to the count badge, making it easy to reset individual filters.
- Flexible Filter Types: Supports string/option filters (single or multi-select), numeric range filters with histograms, and fully custom editors.
- Integrated State Management: Use the
useFilterStatehook to manage all filter state in one place.
Components and Exports
Section titled “Components and Exports”| Export | Description |
|---|---|
FilterPanel | Main dropdown panel component |
FilterEditor | Slot component — declares a custom editor for a specific filter group |
RangeHistogramFilter | Standalone numeric range slider with histogram bars |
useFilterState | State management hook — tracks selections, ranges, and custom values |
FilterDefinition | Type describing a single filter group |
FilterEditorProps | Props passed to a FilterEditor render-prop child ({ value, onChange }) |
FilterEditorSlotProps | Props for the FilterEditor component itself |
FilterValues | Record<string, Set<string>> — selected option keys per filter |
RangeValues | Record<string, [number, number] | null> — selected ranges per filter |
CustomFilterValues | Record<string, unknown> — values for custom editor filters |
Quick Start
Section titled “Quick Start”import { FilterPanel, useFilterState } from '@cratis/components/Filter';import type { FilterDefinition } from '@cratis/components/Filter';
const filters: FilterDefinition[] = [ { key: 'status', label: 'Status', type: 'string', options: [ { key: 'active', label: 'Active', value: 'active', count: 42 }, { key: 'inactive', label: 'Inactive', value: 'inactive', count: 18 }, ], },];
function MyView() { const buttonRef = useRef<HTMLButtonElement>(null); const [isOpen, setIsOpen] = useState(false);
const { filterValues, rangeValues, expandedFilterKey, setExpandedFilterKey, handleToggleFilter, handleClearFilter, handleRangeChange, } = useFilterState(filters);
return ( <> <button ref={buttonRef} onClick={() => setIsOpen(v => !v)}> Filters </button> <FilterPanel isOpen={isOpen} filters={filters} filterValues={filterValues} rangeValues={rangeValues} expandedFilterKey={expandedFilterKey} anchorRef={buttonRef} onClose={() => setIsOpen(false)} onFilterToggle={handleToggleFilter} onFilterClear={handleClearFilter} onRangeChange={handleRangeChange} onExpandedFilterChange={setExpandedFilterKey} /> </> );}Filter Types
Section titled “Filter Types”Single-select (type: 'string', multi: false)
Section titled “Single-select (type: 'string', multi: false)”Renders a radio-button list. Clicking an already-selected option deselects it.
{ key: 'status', label: 'Status', type: 'string', options: [ { key: 'active', label: 'Active', value: 'active', count: 42 }, ],}Multi-select (type: 'string', multi: true)
Section titled “Multi-select (type: 'string', multi: true)”Renders a checkbox list — multiple values may be selected simultaneously.
{ key: 'department', label: 'Department', type: 'string', multi: true, options: [ { key: 'engineering', label: 'Engineering', value: 'engineering', count: 120 }, { key: 'design', label: 'Design', value: 'design', count: 32 }, ],}Numeric range with histogram (type: 'number')
Section titled “Numeric range with histogram (type: 'number')”Renders a RangeHistogramFilter — a draggable range slider overlaid on a histogram of the actual data distribution.
{ key: 'salary', label: 'Salary', type: 'number', buckets: 15, numericRange: { min: 40_000, max: 200_000, values: salaryDataPoints, // FilterValue[] used to draw the histogram },}Custom editor (type: 'custom')
Section titled “Custom editor (type: 'custom')”Declare type: 'custom' in the FilterDefinition, then place a matching <FilterEditor> child inside <FilterPanel>. The value is stored in customValues keyed by the filter’s key.
// 1. Declare the filter group (no editor function here)const filters: FilterDefinition[] = [ { key: 'rating', label: 'Rating', type: 'custom' },];
// 2. Attach the editor declaratively as a child of FilterPanel<FilterPanel filters={filters} customValues={customValues} onCustomValueChange={handleCustomValueChange} {...otherProps}> <FilterEditor filterKey="rating"> {({ value, onChange }) => ( <MyStarRatingWidget value={value as number} onChange={onChange} /> )} </FilterEditor></FilterPanel>Pass customValues and onCustomValueChange to FilterPanel when using custom editors:
const { customValues, handleCustomValueChange, ...rest } = useFilterState(filters);
<FilterPanel {...rest} customValues={customValues} onCustomValueChange={handleCustomValueChange}> <FilterEditor filterKey="rating"> {({ value, onChange }) => <MyStarRatingWidget value={value as number} onChange={onChange} />} </FilterEditor></FilterPanel>Clearing Filters
Section titled “Clearing Filters”Each filter group automatically displays a round clear button (×) in its header when it has active selections:
- String/option filters: The clear button appears next to the selection count badge (e.g., “3 selected”)
- Numeric range filters: The clear button appears next to the “Range” indicator
- Custom filters: The clear button appears when the filter has a value (non-null, non-undefined)
The clear button includes a tooltip describing its action and can be clicked without expanding the filter. When clicked:
- For string/option filters, all selections are cleared via
onFilterClear(filterKey) - For range filters, the range is reset to
nullviaonRangeChange(filterKey, null) - For custom filters, the value is set to
undefinedviaonCustomValueChange(filterKey, undefined)
Custom filter editors should not implement their own clear buttons; the header clear button handles this automatically.
FilterPanel Props
Section titled “FilterPanel Props”| Prop | Type | Required | Description |
|---|---|---|---|
isOpen | boolean | ✓ | Whether the panel is visible |
filters | FilterDefinition[] | ✓ | Filter group definitions |
filterValues | FilterValues | ✓ | Current string/option selections |
rangeValues | RangeValues | ✓ | Current numeric range selections |
customValues | CustomFilterValues | — | Values for custom-editor filters |
search | string | — | Current search-box value |
searchPlaceholder | string | — | Placeholder for search input (default: 'Search…') |
expandedFilterKey | string | null | — | Which filter group is open |
anchorRef | RefObject<HTMLButtonElement> | ✓ | Button the panel anchors below |
onClose | () => void | ✓ | Called when panel should close |
onSearchChange | (value: string) => void | — | If provided, shows a search box |
onFilterToggle | (filterKey, optionKey, multi) => void | ✓ | Called when an option is toggled |
onFilterClear | (filterKey) => void | ✓ | Called when all selections for a filter are cleared |
onRangeChange | (filterKey, range) => void | ✓ | Called when a numeric range changes |
onExpandedFilterChange | (key | null) => void | ✓ | Called when the expanded group changes |
onCustomValueChange | (filterKey, value) => void | — | Called when a custom editor value changes |
children | ReactNode | — | <FilterEditor> slot elements for custom filter groups |
FilterEditor Props
Section titled “FilterEditor Props”FilterEditor is a declarative slot component. It renders nothing itself — FilterPanel discovers it from children and slots the editor into the correct filter group.
| Prop | Type | Required | Description |
|---|---|---|---|
filterKey | string | ✓ | Must match the key of the corresponding FilterDefinition |
children | (props: FilterEditorProps) => ReactNode | ✓ | Render prop receiving { value, onChange } |
useFilterState Hook
Section titled “useFilterState Hook”useFilterState(filters) initialises and manages all filter state in one call. Its return value can be spread directly into FilterPanel:
const state = useFilterState(filters);
<FilterPanel isOpen={open} filters={filters} anchorRef={buttonRef} filterValues={state.filterValues} rangeValues={state.rangeValues} customValues={state.customValues} expandedFilterKey={state.expandedFilterKey} onClose={() => setOpen(false)} onFilterToggle={state.handleToggleFilter} onFilterClear={state.handleClearFilter} onRangeChange={state.handleRangeChange} onExpandedFilterChange={state.setExpandedFilterKey} onCustomValueChange={state.handleCustomValueChange}> <FilterEditor filterKey="myCustomFilter"> {({ value, onChange }) => <MyEditor value={value} onChange={onChange} />} </FilterEditor></FilterPanel>The hook re-syncs state when the filters array reference changes — existing selections are preserved for filter keys that are still present.
RangeHistogramFilter Props
Section titled “RangeHistogramFilter Props”RangeHistogramFilter can also be used standalone, independently of FilterPanel.
| Prop | Type | Required | Description |
|---|---|---|---|
values | FilterValue[] | ✓ | Raw data values used to compute the histogram |
min | number | ✓ | Lower bound of the full range |
max | number | ✓ | Upper bound of the full range |
buckets | number | Number of histogram bars (default 20) | |
selectedRange | [number, number] | null | ✓ | Currently selected range, or null for none |
onChange | (range: [number, number] | null) => void | ✓ | Called when the range changes |
Importing
Section titled “Importing”The Filter module is available at its own subpath — you do not need to import from the root package:
import { FilterPanel, FilterEditor, useFilterState } from '@cratis/components/Filter';import type { FilterDefinition } from '@cratis/components/Filter';From the package root, the module is available under the Filter namespace:
import { Filter } from '@cratis/components';
// Use as Filter.FilterPanel, Filter.FilterEditor, Filter.useFilterState