Search docs

Jump to any table example

Discord
ShadTable

A collection of composable table components built on shadcn/ui and TanStack Table.

Toolbar Filter Table

A simple filter row above the table — dropdown selects and a search input, filters applied immediately. The 80% use case for filtering, and the default place to start.

npx shadcn add https://shad-table.dev/r/toolbar-filter-table.json
TitleCategoryPriorityStatus
Redesign onboarding flowDesignHighIn Progress
Fix pagination bug on exportEngineeringHighOpen
Write Q3 newsletter draftMarketingLowDone
Audit component color tokensDesignMediumOpen
Migrate auth to new session storeEngineeringHighIn Progress
Plan launch landing page copyMarketingMediumOpen
Ship dark mode for settings pageDesignMediumDone
Add rate limiting to public APIEngineeringHighOpen
Set up A/B test for pricing pageMarketingLowIn Progress
Review accessibility on data tableDesignMediumOpen
Optimize bundle size for docs siteEngineeringMediumDone
Coordinate partner co-marketing postMarketingLowDone

How it works

1.One search input, one filter per dropdown, no submit step

The search box writes to globalFilter and each Select writes to its own column filter on every change event — there's no Apply button and no debounce. getFilteredRowModel() re-runs on each keystroke and re-render, which is the entry-point pattern most tables need before anything fancier is worth reaching for.

src/components/toolbar-filter/data-table.tsx
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
state: { columnFilters, globalFilter },
})

2."All" is undefined, not a sentinel string left in the filter state

Radix Select can't represent an empty string as a value, so ToolbarSelect uses the literal 'all' for its placeholder item — but it converts that back to undefined before calling column.setFilterValue. An actual column filter is never set to the string 'all'; clearing a dropdown removes it from columnFilters entirely.

src/components/toolbar-filter/data-table.tsx
const value = (column?.getFilterValue() as string | undefined) ?? 'all'
<Select
value={value}
onValueChange={(next) =>
column?.setFilterValue(next === 'all' ? undefined : next)
}
>

3.Global search and column filters compose for free

globalFilter (the search box) and columnFilters (the two Selects) are independent pieces of table state, but TanStack Table narrows the row model through both — typing in the search box while a Category filter is active searches only within that category, with no extra code to keep the two in sync.

src/components/toolbar-filter/data-table.tsx
<Input
placeholder="Search tasks..."
value={globalFilter}
onChange={(e) => table.setGlobalFilter(e.target.value)}
/>
<ToolbarSelect column={table.getColumn('category')} ... />
<ToolbarSelect column={table.getColumn('status')} ... />