Filtering
Built-in per-column filter popups for text, number, date, datetime, and time-of-day columns, with operator selection and two-condition AND/OR logic. Categorical columns can use a set filter instead, picking values from a checklist rather than typing an operator.
Column filters: text, number, and date
Click the filter icon in any column header. Pick an operator, enter a value, then click Apply. Try salary ≥ 130000, or filter hired Before 2021-01-01.
import DataTable, { type TableColumn } from 'react-data-table-component';
interface Employee {
id: number;
name: string;
department: string;
salary: number;
hired: string;
}
const data: Employee[] = [
{ id: 1, name: 'Aria Chen', department: 'Engineering', salary: 155000, hired: '2019-03-12' },
{ id: 2, name: 'Marcus Webb', department: 'Product', salary: 132000, hired: '2020-07-01' },
{ id: 3, name: 'Priya Kapoor', department: 'Design', salary: 118000, hired: '2021-01-15' },
{ id: 4, name: 'Jordan Ellis', department: 'Analytics', salary: 143000, hired: '2018-11-30' },
{ id: 5, name: 'Sam Rivera', department: 'Engineering', salary: 128000, hired: '2022-04-22' },
{ id: 6, name: 'Taylor Brooks', department: 'Sales', salary: 97000, hired: '2023-02-08' },
{ id: 7, name: 'Morgan Lee', department: 'Engineering', salary: 162000, hired: '2017-09-05' },
{ id: 8, name: 'Casey Park', department: 'Design', salary: 109000, hired: '2022-11-19' },
{ id: 9, name: 'Drew Santos', department: 'Product', salary: 138000, hired: '2020-03-30' },
{ id: 10, name: 'Avery Johnson', department: 'Sales', salary: 104000, hired: '2021-08-14' },
];
const columns: TableColumn<Employee>[] = [
{ id: 'name', name: 'Name', selector: r => r.name, sortable: true, filterable: true },
{ id: 'dept', name: 'Department', selector: r => r.department, filterable: true },
{
id: 'salary',
name: 'Salary',
selector: r => r.salary,
format: r => `$${r.salary.toLocaleString()}`,
right: true,
sortable: true,
filterable: true,
filterType: 'number',
},
{ id: 'hired', name: 'Hired', selector: r => r.hired, sortable: true, filterable: true, filterType: 'date' },
];
export default function App() {
return <DataTable columns={columns} data={data} highlightOnHover pagination paginationPerPage={10} />;
}Filtering runs on the full dataset before pagination, so a filter matches rows on every page, not just the one you are viewing. The result count and page navigation update to the filtered set. This applies to client-side pagination; see theserver-side recipe for the server case.
Quick start
Add filterable: true and a stable id to any column. A filter icon appears in the column header. Filters across columns combine with AND. A row must pass every active filter to appear.
const columns: TableColumn<Row>[] = [{ id: 'name', name: 'Name', selector: r => r.name, filterable: true }];
<DataTable columns={columns} data={data} />;Filter types
Set filterType to get the right operator set and input widget. It defaults to "text". Five of the six types work the same way, an operator plus a value you type;"set" is the exception and shows a checklist instead.
const columns: TableColumn<Row>[] = [
{ id: 'name', name: 'Name', selector: r => r.name, filterable: true },
{ id: 'score', name: 'Score', selector: r => r.score, filterable: true, filterType: 'number' },
{ id: 'dob', name: 'Birth date', selector: r => r.dob, filterable: true, filterType: 'date' },
{ id: 'seen', name: 'Last seen', selector: r => r.seen, filterable: true, filterType: 'datetime' },
{ id: 'ranAt', name: 'Ran at', selector: r => r.ranAt, filterable: true, filterType: 'time' },
];filterType | Default operator | Input | Operators |
|---|---|---|---|
"text" (default) | Contains | Text | Contains, Does not contain, Equals, Does not equal, Begins with, Ends with, Blank, Not blank |
"number" | Equals | Number | Equals, Does not equal, Greater than, ≥, Less than, ≤, Between, Blank, Not blank |
"date" | Equals | Date | Equals, Before, After, Between, Blank, Not blank |
"datetime" | Equals | Date & time | Equals, Before, After, Between, Blank, Not blank |
"time" | Equals | Time | Equals, Before, After, Between, Blank, Not blank |
"set" | n/a | Checklist | None, pick values from a list |
A few types have behavior worth knowing before you pick one:
"date"matches a whole calendar day, so Equals finds a row recorded at any time that day."datetime"matches an exact instant, down to the minute."time"ignores the date and compares only the time of day, so it filters across every date at once. Handy for logs: “anything after 17:00”, or “errors between 02:00 and 04:00”. A Between that starts later than it ends wraps past midnight, so22:00–06:00gives you an overnight window.- Between gives you two inputs and includes both bounds. Fill in only one to leave that side open, so a number filter with just a lower bound behaves like ≥.
- Date and datetime columns expect the
selectorto return an ISO string, like"2024-03-15"or"2024-03-15T14:30".
datetimefiltering assumes your cell values are local time. If they carry aZor a UTC offset, the browser’sdatetime-localinput has no timezone to compare against and matches will be off. Supply afilterFunctionfor those columns.
Time-of-day filter
Log rows across several days. Open the Time filter, choose Between, and enter 02:00 and 04:00 to surface the nightly cron failures regardless of date. Try 22:00 to 06:00 for an overnight window that wraps past midnight.
const columns: TableColumn<LogEntry>[] = [
{
id: 'at',
name: 'Time',
selector: r => r.at, // full ISO timestamp
format: r => r.at.slice(11), // show just the time
filterable: true,
filterType: 'time', // filters by time of day, ignoring the date
},
// ...
];Set filters: pick from a checklist
filterType: 'set' replaces the operator dropdown with a checklist of the column’s distinct values. Instead of typing an operator and a value, you check the values you want to keep. This is usually what people want for categorical columns (department, status, region, log level), where “Engineering or Design or Product” would otherwise need several OR conditions.
const columns: TableColumn<Row>[] = [
{ id: 'department', name: 'Department', selector: r => r.department, filterable: true, filterType: 'set' },
];The checklist is built from your data, so there is nothing else to configure.
Set filter
Open the Service, Severity, or Owner filter and check the values to keep. Two incidents have no owner, so the Owner filter includes (Blanks). Use the search box to narrow a long list, then (Select all) to act on just the matches.
const columns: TableColumn<Incident>[] = [
{ id: 'service', name: 'Service', selector: r => r.service, filterable: true, filterType: 'set' },
{ id: 'severity', name: 'Severity', selector: r => r.severity, filterable: true, filterType: 'set' },
{ id: 'owner', name: 'Owner', selector: r => r.owner, filterable: true, filterType: 'set' },
// ...
];- Values are derived from the rows you pass in, sorted naturally. The list is the column’s full distinct set and does not change as you filter other columns. Set
filterOptions.valueson the column to supply the list instead. - One cell can hold several values. A column of tags or a stack, formatted as
React, TypeScript, lists each part separately once you setfilterOptions.separator. - A search box narrows long lists, and (Select all) acts on what the search has narrowed to. Searching, unchecking (Select all), then checking one value is the quickest way to filter down to a single value.
- Empty cells are collected under (Blanks), and are represented by the empty string in filter state.
- Values are read through the column’s
selectorand compared as strings.formataffects only what the cell displays, not what the checklist shows.
Supplying the checklist values
Deriving values from the rows only works when the rows already contain every value worth filtering on. A column whose domain is fixed and known, a status or a priority, is usually the opposite case: the list is something you know up front, and the loaded rows are just a sample of it. SetfilterOptions.values on the column to supply the list yourself.
This matters most with server-side data. The table only holds the current page, so a status the server knows about but the page does not would never appear in the checklist, and the user could not ask for it. That is circular: the value only becomes selectable once rows carrying it are loaded, but loading them is the whole point of selecting it. Supplying the list breaks the loop.
The demo below holds only Active and Pending tickets, as though the first page had just come back from a server. All four statuses are still listed, and pickingClosed or Archived refetches.
Set filter with supplied values
Open the Status filter. All four statuses are offered even though only Active and Pending are loaded, so picking one the page does not hold can go and fetch it.
const STATUSES = ['Active', 'Pending', 'Closed', 'Archived'];
const columns: TableColumn<Ticket>[] = [
{ id: 'subject', name: 'Subject', selector: r => r.subject, grow: 2 },
{
id: 'status',
name: 'Status',
selector: r => r.status,
filterable: true,
filterType: 'set',
filterOptions: { values: STATUSES },
},
];
<DataTable
columns={columns}
data={rows}
filterServer
filterValues={filters}
onFilterChange={(columnId, next) => {
setFilters(prev => ({ ...prev, [columnId]: next }));
fetchFromServer(next.values); // undefined means nothing is selected yet
}}
/>;Pair it with filterServer, which is implied by paginationServer, so the built-in matcher does not run a second time against rows the server already filtered. SeeServer-side filtering below.
- Order is kept as given. Derived values are sorted naturally, but a supplied list is authored, so
['Low', 'Medium', 'High']stays in that order rather than being alphabetized. - Blanks are only offered if you ask for one. Include an empty string in the list to get a(Blanks) entry. Derived lists add one whenever a row has an empty cell.
valuesalso takes a function, receiving the rows the table is holding, so a fixed domain can be merged with whatever else turned up:{ values: rows => [...new Set([...STATUSES, ...rows.map(r => r.status)])] }.- The column still needs a
selector.filterOptions.valuessupplies the checklist, but client-side matching reads cell values through the selector.
Cells holding several values
A column sometimes holds more than one thing per cell: a list of tags, a stack, the labels on an issue. Left alone, a set filter treats the whole cell as one value, so a row reading React, TypeScript offers exactly that string in the checklist and nothing for React on its own. SetfilterOptions.separator to split the cell into its parts.
Each part becomes its own checklist entry, and a row matches when any of its parts is selected. Checking React and Postgres shows every project using either, which is how a tag filter is normally read.
Set filter on a multi-value column
Open the Stack filter. Each technology is listed separately even though the cells hold comma separated strings, and picking one shows every project using it.
const columns: TableColumn<Project>[] = [
{ id: 'name', name: 'Project', selector: r => r.name, grow: 2 },
{
id: 'stack',
name: 'Stack',
selector: r => r.stack,
filterable: true,
filterType: 'set',
filterOptions: { separator: ',' },
},
];- Parts are trimmed, and empty ones dropped.
'React, , TypeScript,'yields two values, so ragged data does not litter the checklist with blanks. - A cell left with nothing counts as blank. An empty cell, or one holding only separators, still reaches the (Blanks) entry.
- A RegExp works too, for data that is not consistently delimited:
{ separator: /s*[,|]s*/ }. - Pair it with
filterOptions.valuesto supply the parts yourself rather than deriving them from the loaded rows. The two are independent:valuessets the checklist,separatorgoverns how cells are matched against it.
Two conditions per column
Every filter popup has a + Add condition link. Adding a second condition reveals an AND / OR toggle: AND means a row has to match both, OR means either will do. That covers things like “starts with J but does not end with son” without writing a custom filter function. Set filters have no operators, so they show the checklist instead of this link.
Apply / Clear behavior
Filters apply only when the user clicks Apply. Typing does not immediately re-filter. This avoids jarring mid-keystroke changes on large datasets. Clicking Clear resets the column's filter and applies immediately.
Keyboard and accessibility
- The filter button carries
aria-haspopup="dialog"andaria-expanded, and the panel is arole="dialog"labelled for screen readers. - Opening the panel moves focus into it. Tab and Shift+Tab cycle within the panel rather than escaping behind it, and Escape closes it and returns focus to the filter button.
- In a set filter, Tab moves through the search box, (Select all), each value, then Clear and Apply; Space toggles the focused checkbox. Checkboxes show a focus ring, and the focused row is highlighted.
- While a search narrows the checklist, (Select all) acts only on the matches, and announces that to screen readers.
- The panel flips above its button near the bottom edge, clamps within the viewport, and scrolls internally when it cannot fit. It fades in on open, and holds still under
prefers-reduced-motion: reduce.
Filter state
You can ignore this section until you need to read or write filters yourself, which comes up when you persist them in a URL, restore them on load, or filter on the server. One FilterState describes one column’s filter. Operator-based types fill in condition1, plus condition2 andlogic when there is a second condition. Set filters use values instead and ignore the conditions entirely.
import type { FilterState } from 'react-data-table-component';
// Operator-based: "starts with J AND ends with son"
const nameFilter: FilterState = {
condition1: { operator: 'startsWith', value: 'J' },
condition2: { operator: 'endsWith', value: 'son' },
logic: 'AND', // 'AND' | 'OR' — defaults to 'AND'
};
// Set filter: "keep Engineering and Design rows, plus rows with no department"
const deptFilter: FilterState = {
condition1: { operator: 'equals' }, // ignored by set filters
values: ['Engineering', 'Design', ''],
};For a set filter, values: undefined means no selection has been made yet and matches every row; an empty array means nothing is selected and matches none. Cells that are empty or contain only whitespace are all treated as blanks and selected with the empty string, shown in the checklist as (Blanks).
Values that appear after a set filter is applied
When the built-in panel applies a set filter it also records knownValues, the values in the checklist at that moment. If new data later brings a value that was not on that list, it was never unchecked, so those rows stay visible. Checking or unchecking anything in the panel makes the selection explicit again and records a fresh snapshot.
Omit knownValues when building filter state yourself and values is treated as an exhaustive allow-list, so anything not listed is filtered out, including values added later.
const filter: FilterState = {
condition1: { operator: 'equals' },
values: ['Engineering'],
// Design and Support were in the checklist and left unchecked; anything else is new
knownValues: ['Engineering', 'Design', 'Support'],
};Two helpers are exported for working with filter state: emptyFilterState builds a blank one for a given filter type, and isFilterActive tells you whether a filter is actually narrowing anything.
import { emptyFilterState, isFilterActive } from 'react-data-table-component';
// Create a default-empty FilterState for a given type
emptyFilterState('number'); // { condition1: { operator: 'equals' } }
emptyFilterState('text'); // { condition1: { operator: 'contains' } }
// Check whether a FilterState is actually filtering anything
isFilterActive({ condition1: { operator: 'contains' } }); // false — no value
isFilterActive({ condition1: { operator: 'contains', value: 'a' } }); // true
isFilterActive({ condition1: { operator: 'blank' } }); // true — no value neededCustom filter function
Override built-in operator logic per column with filterFunction. It receives the full FilterState so both conditions are available:
import type { TableColumn, FilterState } from 'react-data-table-component';
const columns: TableColumn<Row>[] = [
{
id: 'tags',
name: 'Tags',
selector: r => r.tags.join(', '),
filterable: true,
filterFunction: (row, filter) => {
const term = (filter.condition1.value ?? '').toLowerCase();
return row.tags.some(tag => tag.toLowerCase().includes(term));
},
},
];Controlled mode
Pass filterValues and onFilterChange to own the filter state yourself. Useful for persisting it in a URL or resetting it programmatically.onFilterChange fires on every Apply or Clear click.
import { useState } from 'react';
import DataTable, { type FilterState } from 'react-data-table-component';
function App() {
const [filterValues, setFilterValues] = useState<Record<string | number, FilterState>>({});
const [resetPage, setResetPage] = useState(false);
function handleFilterChange(columnId: string | number, filter: FilterState) {
setFilterValues(prev => ({ ...prev, [columnId]: filter }));
setResetPage(v => !v); // jump back to page 1 after each filter
}
return (
<DataTable
columns={columns}
data={data}
filterValues={filterValues}
onFilterChange={handleFilterChange}
pagination
paginationResetDefaultPage={resetPage}
/>
);
}Server-side filtering
If your backend does the filtering, add filterServer. The popups still render and still callonFilterChange, but the built-in matcher is skipped, so the rows your server returned are shown as-is. paginationServer turns this on for you, since a page cannot be filtered against rows it does not hold.
const [filterValues, setFilterValues] = useState({});
const handleFilterChange = (columnId, filter) => {
const next = { ...filterValues, [columnId]: filter };
setFilterValues(next);
fetchFromApi({ filters: next, page: 1 }).then(setRows);
};
<DataTable
columns={columns}
data={rows}
filterServer
filterValues={filterValues}
onFilterChange={handleFilterChange}
/>Localization
Every string in the filter panel is overridable through the table-levellocalization prop, under its filter key. SeeLocalization for the full list of keys and their defaults.
Headless usage
Use useColumnFilter directly when building a custom table with the headless hooks. See Headless hooks for the full API.
import { useColumnFilter, type FilterState } from 'react-data-table-component';
const { filterValues, handleFilterChange, filteredData } = useColumnFilter(columns);
// Call handleFilterChange when the user applies a filter in your custom UI
function onApply(columnId: string | number, filter: FilterState) {
handleFilterChange(columnId, filter);
}
// Apply all active filters before rendering rows
const rows = filteredData(tableRows);See it combined with other features in the Server-side sort, page & filter recipe and URL-synced table state.
Prop reference
| Prop | Type | Default | Description |
|---|---|---|---|
filterValues | Record<string | number, FilterState> | - | Controlled filter state. Omit to use internal state. See Filtering. |
onFilterChange | (columnId, filter: FilterState) => void | - | Called when the user clicks Apply or Clear in a filter popup. |
filterServer | boolean | false | Disable client-side filtering. Use with onFilterChange to filter remotely. |
column.filterOptions | { values?: string[] | ((rows: T[]) => string[]); separator?: string | RegExp } | - | Per-column set filter settings. values supplies the checklist, separator splits multi-value cells. |
Per-column filtering is configured on each TableColumnvia filterable, filterType, and filterFunction.