Skip to main content

DataTable - Filtering

Displays a matrix of information with columns, rows, and information that can operate dynamically.

Submit feedback
github
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 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:

ConditionTextDateSelect
containsDefault
startsWith
equalsDefaultDefault
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. 1 is not equal to '1')
  • 'weakEquals': Matches all cells that are loosely equal to the given value (e.g. 1 is 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 NameValue
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)

Table of Contents