import { DataTable } from '@uhg-abyss/web/ui/DataTable';import { useDataTable } from '@uhg-abyss/web/hooks/useDataTable';Abyss supports two types of column filtering:
- Basic filtering allows for one filter per column.
- Advanced filtering allows for multiple filters per column.
Disclaimer: The Abyss Design System supports only advanced filtering. Developers can still use basic filtering, but it is not supported by the design system.
The setup is very similar between the two.
Filtering is disabled by default. To enable filtering for all columns, set tableConfig.enableColumnFilters to true.
const dataTableProps = useDataTable({ // ... tableConfig: { enableColumnFilters: true, }, // ...});The enableColumnFilter property can also be provided to individual columns for more granular adjustment.
{ header: 'Column 1', accessorKey: 'col1', enableColumnFilter: true}To manage the filter state, provide a function to the tableConfig.onColumnFiltersChange property and set the state.columnFilters property; we recommend using useState for this, as shown below.
const [columnFilters, setColumnFilters] = useState([]);
const dataTableProps = useDataTable({ // ... tableConfig: { enableColumnFilters: true, onColumnFiltersChange: setColumnFilters, state: { columnFilters, }, }, columnFilterConfig: {}, // ...});Column filtering configuration
All column filtering configuration is done through the columnFilterConfig property. Columns can be configured independently using the columnFilterConfig.individualSettings property, which accepts an object where the keys are the column accessorKey values.
Default filters
To set default filters, provide an initial value for the state.columnFilters property. This is an array of objects, where each object contains the id (i.e., the accessorKey) of the column and an array of filter objects. Each filter object contains a value and a condition. See the Condition options section for a list of available conditions.
When using basic filtering, the value object contains a single filter in the filters array.
const [columnFilters, setColumnFilters] = useState([ { id: 'col2', value: { filters: [ // `value` can be a singular string or an array of two strings, depending on the `condition` { value: ['20', '40'], condition: 'between' }, ], }, },]);When using advanced filtering, the value object can contain multiple filter objects in the filters array. The matchType property accepts either 'all' or 'any' and determines how the filters are applied—whether all conditions must be met or only one must be met. The default is 'all'.
const [columnFilters, setColumnFilters] = useState([ { id: 'col1', value: { filters: [ { value: '10', condition: 'contains' }, { value: 'Col 1/Row 10', condition: 'notEqual' }, ], }, },]);Filtering mode
By default, column filters are set to advanced. To apply basic filtering for all columns, set defaultSettings.filterMode to basic.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { defaultSettings: { filterMode: 'basic', }, }, // ...});The filterMode property can also be provided to individual columns for more granular adjustment.
Note: It is recommended to pick one filter mode per table for consistency and simpler configuration.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { defaultSettings: { filterMode: 'basic', }, individualSettings: { col1: { filterMode: 'advanced', }, }, }, // ...});Case sensitivity
By default, column filters are not case sensitive. To enable case-sensitive filtering for all columns, set defaultSettings.caseSensitive to true.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { defaultSettings: { caseSensitive: true, }, }, // ...});The caseSensitive property can also be provided to individual columns for more granular adjustment.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { defaultSettings: { caseSensitive: true, }, individualSettings: { col1: { caseSensitive: false, }, }, }, // ...});Input type
Column filters can be configured to use different input types with the inputConfig property. The available input types are:
text: Utilizes TextInputdate: Utilizes DateInput and DateInputRangeselect: Utilizes SelectInput
Text Input
{ type: 'text', // This is the default and could be omitted},Date Input
{ type: 'date'},Select Input
To use a SelectInput with predefined options, provide an array of objects with value and label properties to the options property.
{ type: 'select', options: [ { value: 'Not Completed', label: 'Not Completed' }, { value: 'In Progress', label: 'In Progress' }, { value: 'Completed', label: 'Completed' }, ],},For API filtering with SelectInput, you'll need to provide additional properties:
{ type: 'select', options: searchResults, // Array of options from API response onInputChange: handleSearch, // Function to handle search input changes isLoading: isLoading, // Boolean to indicate loading state},For more details on API filtering implementation, see SelectInput.
Condition options
Each input type supports different filtering conditions:
| Condition | Text | Date | Select |
|---|---|---|---|
contains | ✅ Default | ||
startsWith | ✅ | ||
equals | ✅ | ✅ Default | ✅ Default |
notEqual | ✅ | ✅ | ✅ |
greaterThan | ✅ | ✅ | |
greaterOrEqual | ✅ | ✅ | |
lessThan | ✅ | ✅ | |
lessOrEqual | ✅ | ✅ | |
between | ✅ | ✅ | |
betweenInclusive | ✅ | ✅ | |
empty | ✅ | ✅ | ✅ |
notEmpty | ✅ | ✅ | ✅ |
To override the default condition for each input type, use the defaultSettings.textDefaultCondition, defaultSettings.dateDefaultCondition, and defaultSettings.defaultSelectCondition properties.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { defaultSettings: { textDefaultCondition: 'contains', dateDefaultCondition: 'equals', selectDefaultCondition: 'equals', }, }, // ...});The defaultCondition property can also be provided to individual columns for more granular adjustment.
Note: If a condition is not available for the chosen input type, it will be ignored.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { individualSettings: { col1: { defaultCondition: 'startsWith', }, }, }, // ...});Use the defaultSettings.textConditionMap, defaultSettings.dateConditionMap and defaultSettings.selectConditionMap properties to specify the options that should appear in the dropdown menu for each input type as well as to specify their order.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { defaultSettings: { textConditionMap: [ { condition: 'contains' }, { condition: 'equals' }, { condition: 'empty' }, ], dateConditionMap: [{ condition: 'equals' }, { condition: 'empty' }], selectConditionMap: [{ condition: 'equals' }, { condition: 'empty' }], }, }, // ...});The conditionMap property can also be provided to individual columns for more granular adjustment.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { individualSettings: { col1: { conditionMap: [ { condition: 'contains' }, { condition: 'equals' }, { condition: 'startsWith' }, ], }, }, }, // ...});When using basic filtering, dividers can be added to the condition dropdown by using the isSeparated property. If true for a given item, a divider will be added after that item. This applies to both the default settings as well as the individual column settings.
const dataTableProps = useDataTable({ // ... defaultSettings: { textConditionMap: [ { condition: 'equals', isSeparated: true }, { condition: 'empty' }, { condition: 'notEmpty', isSeparated: true }, { condition: 'contains' }, ], }, individualSettings: { col1: { conditionMap: [ { condition: 'equals', isSeparated: true }, { condition: 'empty' }, { condition: 'notEmpty', isSeparated: true }, { condition: 'contains' }, ], defaultCondition: 'contains', inputConfig: { type: 'text', }, }, }, // ...});Note: The default separators will be removed if you provide your own condition maps, whether in the default settings or individual column settings.
Important filtering callouts
This section contains some important callouts regarding column filtering. We strongly recommend reading through this section and viewing the examples before implementing column filtering.
Note: These callouts apply to both basic and advanced filtering.
Initial render
On initial render, only the filters present in state.columnFilters will be applied.
In the example below, even though the defaultCondition is set to 'empty' for col3, that filter will not be applied on initial render, as only the filter for col1 is present in the columnFilters state value at that time.
const [columnFilters, setColumnFilters] = useState([ { id: 'col1', value: { matchType: 'all', filters: [{ value: '4', condition: 'contains' }], }, },]);
const dataTableProps = useDataTable({ // ... tableConfig: { enableColumnFilters: true, onColumnFiltersChange: setColumnFilters, state: { columnFilters, }, }, columnFilterConfig: { individualSettings: { col3: { defaultCondition: 'empty', }, }, }, // ...});Updating applied filters
Knowing how the columnFilters state is managed internally is important for teams using server-side pagination and/or creating custom filters.
When a user selects a condition, that column filter will be added to the columnFilters state. If that column has an existing filter, the existing filter will be replaced with the new one.
Say we have an initial state like this:
columnFilters: [ { id: 'col1', value: { filters: [{ value: '4', condition: 'contains' }], }, },];After the user selects the 'contains' condition for col3, our state is this:
columnFilters: [ { id: 'col1', value: { filters: [{ value: '4', condition: 'contains' }], }, }, { id: 'col3', value: { filters: [{ value: '""', condition: 'contains' }], }, },];After the user selects the 'equals' condition for col1, our state is this:
columnFilters: [ { id: 'col1', value: { filters: [{ value: '""', condition: 'equals' }], }, }, { id: 'col3', value: { filters: [{ value: '""', condition: 'contains' }], }, },];Advanced filtering
For advanced filtering, no columnFilterConfig is necessary. The default filter mode is 'advanced', so as long as tableConfig.enableColumnFilters is set to true, the filters will be available.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { defaultSettings: { filterMode: 'advanced', }, }, // ...});Default filters
This example contains default filter values for some columns. Note that col1 has two default conditions.
Programmatically setting filters
This example shows how to programmatically add or update advanced filters. This is useful when using server-side pagination.
Basic filtering
To use advanced filtering, set columnFilterConfig.defaultSettings.filterMode to 'basic'. This will allow for multiple filters per column.
Default filters
This example contains default filter values for some columns as well as a different input type for each.
Programmatically setting filters
This example shows how to programmatically add or update basic filters. This is useful when using server-side pagination.
Basic & advanced filtering
This example demonstrates one column using the basic filter mode and another column using the advanced filter mode.
Note: Using multiple filter modes in a single table will require additional setup and configuration for teams. For most teams, one should show basic or advanced for the entire table.
Custom column filters
There may be times when the built-in column filtering functions do not meet your needs. In these cases, you can create your own custom filtering functions.
The example below shows a simple fuzzy filter function. See the TanStack Table docs for an example of another, more advanced custom fuzzy filter function.
First, define the custom filter function. This function should take the following parameters:
row: The row object being filtered.columnId: The ID of the column being filtered.filterValue: The value being used to filter the column.addMeta: A function to add metadata to the filter.isCaseSensitive: A boolean indicating whether the filter should be case-sensitive.
const fuzzyFilter = (row, columnId, filterValue, addMeta, isCaseSensitive) => { let rowValue = ensureString(row.getValue(columnId)); let filterValueCopy = filterValue; // Create a copy of filterValue
// Convert both rowValue and filterValue to lowercase if not case-sensitive if (!isCaseSensitive) { rowValue = rowValue.toLowerCase(); filterValueCopy = filterValueCopy.toLowerCase(); }
// Split the filter value into individual search terms const searchTerms = filterValueCopy.split(' ');
// Check if each search term appears in the row value return searchTerms.every((term) => { let termIndex = 0; for (let i = 0; i < rowValue.length; i++) { if (rowValue[i] === term[termIndex]) { termIndex++; } if (termIndex === term.length) { return true; } } return false; });};Next, add the filter to columnFilterConfig.additionalFilters. The key of this object is the name of the filter, which will be used as the condition. The value is an object containing the following properties:
filter: The custom filter function.label: The label to display in the filter dropdown.inputCount: The number of inputs to display for this filter; either 0 or 1.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { additionalFilters: { fuzzy: { filter: fuzzyFilter, label: 'Fuzzy', inputCount: 1, }, }, }, // ...});Finally, to enable the filter in a column, use the conditionMap property to place the custom filter in the dropdown.
const dataTableProps = useDataTable({ // ... columnFilterConfig: { individualSettings: { col1: { conditionMap: [ { condition: 'fuzzy' }, { condition: 'equals' }, { condition: 'startsWith' }, ], }, }, }, // ...});Note: The built-in filter function names are reserved. Any custom filter function names must be unique and must not conflict with the built-in filter function names.
The built-in filter function names are:
'between''betweenInclusive''contains''empty''equals''greaterOrEqual''greaterThan''lessOrEqual''lessThan''notEmpty''notEqual''startsWith'
Labels, however, can match the built-in labels. The example below shows how to replace the built-in "Contains" filter with a custom fuzzy filter.
const invalidDataTableProps = useDataTable({ // ... columnFilterConfig: { additionalFilters: { // Invalid; the `contains` key is reserved for the built-in filter contains: { filter: fuzzyFilter, label: 'Contains', inputCount: 1, }, }, }, // ...});
const validDataTableProps = useDataTable({ // ... columnFilterConfig: { additionalFilters: { // Valid; the `containsCustom` key is not reserved, even though the label is the same as the default containsCustom: { filter: fuzzyFilter, label: 'Contains', inputCount: 1, }, }, }, // ...});Global filtering
The DataTable.GlobalFilter sub-component provides an input field for filtering the entire table, allowing users to search across all columns at once.
<DataTable.GlobalFilter />To manage the global filter state, provide a function to the tableConfig.onGlobalFilterChange property and set the state.globalFilter property; we recommend using useState for this, as shown below.
const [globalFilter, setGlobalFilter] = React.useState('');
const dataTableProps = useDataTable({ // ... tableConfig: { // ... state: { globalFilter, }, // ... onGlobalFilterChange: setGlobalFilter, }, // ...});The enableGlobalFilter property can also be provided to individual columns for more granular adjustment. If false, the column will not be checked when executing the global filter.
{ header: 'Column 1', accessorKey: 'col1', enableGlobalFilter: false}Built-in global filtering
There are ten built-in global filtering functions to choose from:
'includesString': Matches all cells that contain the given string (case-insensitive)'includesStringSensitive': Matches all cells that contain the given string (case-sensitive)'equalsString': Matches all cells that exactly match the given string (case-insensitive)'equalsStringSensitive': Matches all cells that exactly match the given string (case-sensitive)'arrIncludes': Matches all cells where the array includes the given item'arrIncludesAll': Matches all cells where the array includes all of the given items'arrIncludesSome': Matches all cells where the array includes at least one of the given items'equals': Matches all cells that are strictly equal to the given value (e.g.1is not equal to'1')'weakEquals': Matches all cells that are loosely equal to the given value (e.g.1is equal to'1')'inNumberRange': Matches all cells where the number falls within the given range
onChange vs onSearch
By default, DataTable.GlobalFilter uses onSearch mode, applying the filter only after the user presses enter or clicks the search button. This is ideal for API-based filtering to avoid unnecessary requests.
Switching to onChange updates the filter instantly as the user types, providing immediate feedback but potentially triggering more frequent updates.
<DataTable.GlobalFilter searchMode={'onSearch' || 'onChange'} />Custom global filtering
There may be times when the built-in column filtering functions do not meet your needs. In these cases, you can create your own custom filtering functions.
Note: The setup for custom global filtering is very similar to that of custom column filtering.
The example below shows a simple fuzzy filter function. See the TanStack Table docs for an example of another, more advanced custom fuzzy filter function.
First, define the custom filter function. This function should take the following parameters:
row: The row object being filtered.columnId: The ID of the column being filtered.filterValue: The value being used to filter the column.addMeta: A function to add metadata to the filter.isCaseSensitive: A boolean indicating whether the filter should be case-sensitive.
const fuzzyFilter = (row, columnId, filterValue, addMeta, isCaseSensitive) => { let rowValue = ensureString(row.getValue(columnId)); let filterValueCopy = filterValue; // Create a copy of filterValue
// Convert both rowValue and filterValue to lowercase if not case-sensitive if (!isCaseSensitive) { rowValue = rowValue.toLowerCase(); filterValueCopy = filterValueCopy.toLowerCase(); }
// Split the filter value into individual search terms const searchTerms = filterValueCopy.split(' ');
// Check if each search term appears in the row value return searchTerms.every((term) => { let termIndex = 0; for (let i = 0; i < rowValue.length; i++) { if (rowValue[i] === term[termIndex]) { termIndex++; } if (termIndex === term.length) { return true; } } return false; });};Next, to enable the global filter in the column, pass the fuzzyFilter to the tableConfig.globalFilterFn property. This will override the default global filter function.
const dataTableProps = useDataTable({ // ... tableConfig: { globalFilterFn: fuzzyFilter, // Set the custom filter function }, // ...});Component Tokens
Note: Click on the token row to copy the token to your clipboard.
DataTable Tokens
| Token Name | Value | |
|---|---|---|
| data-table.color.border.column-header.drag | #002677 | |
| data-table.color.border.root | #CBCCCD | |
| data-table.color.border.row.drag | #002677 | |
| data-table.color.border.table | #CBCCCD | |
| data-table.color.icon.column-header-menus.grouping.active | #002677 | |
| data-table.color.icon.column-header-menus.grouping.hover | #004BA0 | |
| data-table.color.icon.column-header-menus.grouping.rest | #196ECF | |
| data-table.color.icon.column-header-menus.sorting.active | #002677 | |
| data-table.color.icon.column-header-menus.sorting.hover | #004BA0 | |
| data-table.color.icon.column-header-menus.sorting.rest | #196ECF | |
| data-table.color.icon.drag-handle.active | #002677 | |
| data-table.color.icon.drag-handle.hover | #004BA0 | |
| data-table.color.icon.drag-handle.rest | #196ECF | |
| data-table.color.icon.expander.active | #002677 | |
| data-table.color.icon.expander.disabled | #7D7F81 | |
| data-table.color.icon.expander.hover | #004BA0 | |
| data-table.color.icon.expander.rest | #196ECF | |
| data-table.color.icon.utility.drag-alternative.active | #000000 | |
| data-table.color.icon.utility.drag-alternative.disabled | #7D7F81 | |
| data-table.color.icon.utility.drag-alternative.hover | #323334 | |
| data-table.color.icon.utility.drag-alternative.rest | #4B4D4F | |
| data-table.color.icon.utility.filter.active | #002677 | |
| data-table.color.icon.utility.filter.hover | #004BA0 | |
| data-table.color.icon.utility.filter.rest | #196ECF | |
| data-table.color.surface.column-header.active | #E5F8FB | |
| data-table.color.surface.column-header.default | #F3F3F3 | |
| data-table.color.surface.column-header.drag | #E5F8FB | |
| data-table.color.surface.footer | #F3F3F3 | |
| data-table.color.surface.header | #FFFFFF | |
| data-table.color.surface.root | #FFFFFF | |
| data-table.color.surface.row.drag | #E5F8FB | |
| data-table.color.surface.row.even | #FAFCFF | |
| data-table.color.surface.row.highlighted | #E5F8FB | |
| data-table.color.surface.row.hover | #F3F3F3 | |
| data-table.color.surface.row.odd | #FFFFFF | |
| data-table.color.surface.table | #FFFFFF | |
| data-table.color.text.cell | #4B4D4F | |
| data-table.color.text.column-header | #4B4D4F | |
| data-table.color.text.header.heading | #002677 | |
| data-table.color.text.header.paragraph | #4B4D4F | |
| data-table.border-radius.all.container | 8px | |
| data-table.border-width.all.column-header.drag | 2px | |
| data-table.border-width.all.root | 1px | |
| data-table.border-width.all.row.drag | 2px | |
| data-table.border-width.all.table | 1px | |
| data-table.sizing.all.icon.column-header-menus | 20px | |
| data-table.sizing.all.icon.drag-handle-row | 24px | |
| data-table.sizing.all.icon.expander-column | 24px | |
| data-table.sizing.all.icon.utility.drag-alternative | 20px | |
| data-table.sizing.all.icon.utility.filter | 20px | |
| data-table.sizing.height.cell.comfortable | 48px | |
| data-table.sizing.height.cell.compact | 32px | |
| data-table.sizing.height.cell.cozy | 40px | |
| data-table.spacing.gap.horizontal.button-group | 8px | |
| data-table.spacing.gap.horizontal.cell | 4px | |
| data-table.spacing.gap.horizontal.drag-alternative | 8px | |
| data-table.spacing.gap.horizontal.input-container | 8px | |
| data-table.spacing.gap.horizontal.slot-wrapper | 24px | |
| data-table.spacing.gap.vertical.column-header | 2px | |
| data-table.spacing.gap.vertical.header | 4px | |
| data-table.spacing.gap.filter-two-inputs | 16px | |
| data-table.spacing.padding.all.column-header | 8px | |
| data-table.spacing.padding.all.column-header-menus | 2px | |
| data-table.spacing.padding.all.header | 16px | |
| data-table.spacing.padding.all.result-text | 16px | |
| data-table.spacing.padding.all.slot-wrapper | 16px | |
| data-table.spacing.padding.horizontal.cell | 8px | |
| data-table.spacing.padding.vertical.button-group | 8px | |
| data-table.spacing.padding.vertical.cell | 4px | |
| data-table.elevation.column.pinned.left | 6px 0px 8px -2px rgba(0,0,0,0.16) | |
| data-table.elevation.column.pinned.right | -6px 0px 8px -2px rgba(0,0,0,0.16) | |
| data-table.elevation.column-header | 0px 6px 8px -2px rgba(0,0,0,0.16) | |
| data-table.elevation.table-settings-dropdown.section-header | 0px 2px 4px -2px rgba(0,0,0,0.16) |