Skip to content

Data table toolbar

The control strip that sits above a table or card grid: a search field, a filter menu, a grid/list view toggle, and the page’s primary action. It’s fully controlled — you own query and view state and this block just renders the controls and calls back. The search box flexes to fill space while the right-hand cluster stays fixed.

'use client';
import {
Button,
Flex,
Menu,
MenuItem,
MenuTrigger,
Segment,
SegmentedControl,
TextInput,
} from '@helix-ui/core';
import { Filter, LayoutGrid2, Plus, Rows, Search } from '@helix-ui/icons';
export type TableView = 'grid' | 'list';
export interface DataTableToolbarProps {
query: string;
onQueryChange: (value: string) => void;
view: TableView;
onViewChange: (view: TableView) => void;
filters?: { label: string; onSelect: () => void }[];
primaryLabel?: string;
onPrimary?: () => void;
}
export function DataTableToolbar({
query,
onQueryChange,
view,
onViewChange,
filters = [],
primaryLabel = 'New',
onPrimary,
}: DataTableToolbarProps) {
return (
<Flex align="center" justify="between" gap={3} wrap>
<Flex align="center" gap={2} style={{ flex: '1 1 260px' }}>
<div style={{ flex: '1 1 220px', maxWidth: 340 }}>
<TextInput
aria-label="Search"
size="sm"
placeholder="Search…"
prefix={<Search size={16} />}
value={query}
onChange={onQueryChange}
/>
</div>
{filters.length > 0 ? (
<MenuTrigger>
<Button variant="outline" tone="neutral" size="sm">
<Filter size={16} />
Filter
</Button>
<Menu>
{filters.map((filter) => (
<MenuItem key={filter.label} onAction={filter.onSelect}>
{filter.label}
</MenuItem>
))}
</Menu>
</MenuTrigger>
) : null}
</Flex>
<Flex align="center" gap={2}>
<SegmentedControl
size="sm"
aria-label="View"
selectedKeys={new Set([view])}
onSelectionChange={(keys) => onViewChange([...keys][0] as TableView)}
>
<Segment id="grid" aria-label="Grid view">
<LayoutGrid2 size={16} />
</Segment>
<Segment id="list" aria-label="List view">
<Rows size={16} />
</Segment>
</SegmentedControl>
<Button size="sm" onClick={onPrimary}>
<Plus size={16} />
{primaryLabel}
</Button>
</Flex>
</Flex>
);
}

Usage

const [query, setQuery] = useState('');
const [view, setView] = useState<TableView>('list');
<DataTableToolbar
query={query}
onQueryChange={setQuery}
view={view}
onViewChange={setView}
filters={[
{ label: 'Active only', onSelect: () => setStatus('active') },
{ label: 'Archived', onSelect: () => setStatus('archived') },
]}
primaryLabel="New project"
onPrimary={createProject}
/>;

Notes

  • onSelectionChange hands back a Set; the block reads the single selected key off it. disallowEmptySelection (on by default) keeps a view always active.
  • For more than a couple of filter dimensions, promote the Menu to a Sheet or a filter bar — a dropdown gets unwieldy past ~6 items.