Home/ Cookbook/ Tables/ Heavy with everything

Build the kitchen-sink table — filters, bulk actions, saved views, the lot

By the end of this recipe you'll have the end-state data table: server-paged, sort + filter, editable cells, bulk actions, column show/hide, saved views, CSV export, sticky first column, frozen header. Most apps don't need this. When they do, here's how it composes.

Tables 6 primitives · 2 blocks · 2 patterns ~60 min React · @corelithzw/react

Overview

A toolbar of filter chips, a bulk-action bar that appears on row select, a column-visibility drawer, a saved-views menu, paged server data, inline editing on safe cells, and CSV export of the current view. All the layers, sitting on top of each other, in one composition.

This recipe is the synthesis of the other table recipes. The trick is that each layer is independent — toolbar doesn't know about inline edit; bulk action doesn't know about sort. Compose them in a fixed order and the result is predictable. Build them as a tangle and the table will become unmaintainable in 6 months.

This is the END state — most tables don't need it. A POS line items table needs editable cells, not column visibility. An audit log needs filters, not bulk actions. Reach for the simpler recipes first. The "everything" table is for when a power user lives inside it 8 hours a day.

Skip this recipe if you're not sure you need it. Skip if you'd ever describe the user as "casual" — this UI is for the operator who knows what they want and needs the shortest path to it.

What you'll build

Default with toolbar, two rows selected (bulk bar), and saved-views open.

Huchu

All transactions

QStatusDate···
RefAmtSt
TXN-001$12P
TXN-002$45O
TXN-003$220P
TXN-004$85V
01 Default — toolbar with filter chips
Huchu

All transactions

2 selectedExport · Void · Delete
RefAmtSt
TXN-001$12P
TXN-002$45O
TXN-003$220P
TXN-004$85V
02 2 selected — bulk bar replaces toolbar
Huchu

All transactions · Views

Unpaid this month
Voided last 30 days
High value (> $1k)
+ Save current as view
RefAmtSt
TXN-006$520O
03 Saved views — switch between presets

Required pieces

Most of the kit, honestly. This is the composition test.

@corelithzw/react exports used here: DataToolbar, FilterChips, Checkbox, DropdownMenu, Popover, Button, Pagination, Drawer, Toast, useUrlState.

Step-by-step build

01

Layered state — selection, filter, view, columns, edit

Five independent stores. URL holds filter + sort + page (shareable). LocalStorage holds saved views + column visibility (user preference). Component state holds selection + edit (ephemeral). Don't merge them.

Code-only step

Three storage tiers — URL, LocalStorage, memory — each with a clear "who owns this" rule. The moment you blur them, two-way sync hell is unavoidable.

Step 1 · State layers@corelithzw/react
02

Toolbar swaps with bulk bar — same slot, different content

Reserve one horizontal strip above the table. When selection.size === 0, render the filter toolbar. Otherwise render the bulk-action bar. Don't stack them — that's two rows fighting for attention. Same slot, different content, one mental load.

Status: Open Date: 30d Export
3 selected Export · Void · Delete · Cancel
Step 2 · Toolbar swap
03

Saved views — a serialized TableQuery, named

A saved view is just { name, q: TableQuery }. Store an array of them in localStorage. Switching a view = setQ(view.q). Saving the current view = { name: prompt('Name?'), q }. No backend required for v1.

Unpaid this month
Voided last 30 days
High value (> $1k)
+ Save current as view
Step 3 · Saved views
04

Column visibility + sticky first column

A checkbox list in a Popover toggles a Set of visible columns. The first column is sticky to the left so the row label stays visible while scrolling horizontally. position: sticky; left: 0 on the <th> and <td> of column 0.

Columns
Ref
Amount
Created by
Status
Step 4 · Columns + sticky
05

Bulk actions — confirm destructives, optimistic for safe ones

Delete/Void: alert-dialog ("Delete 12 transactions? This cannot be undone."). Export: just do it. Status changes: optimistic + Toast undo. The pattern: irreversible needs confirmation, reversible doesn't.

Delete 12 transactions?
This cannot be undone.
Cancel Delete
Step 5 · Bulk actions

Final composition

~350 lines once you wire it all. The point is the composition tree — each layer is a small file underneath.

Status: Open Views ▾ Columns Export
RefAmountStatus
TXN-0001$12.40Open
TXN-0002$45.00Open
HeavyTransactionsTable.tsx@corelithzw/react

Variations

Three subtractions — what to remove first when this is overkill.

Drop saved views

If users use 1–2 filter combos, hard-code them as tabs above the table. Saved views are for users with 5+ saved combos who switch hourly.

// remove ViewMenu + storage
<Tabs value={preset}
  onChange={setPreset}>
  <Tab value="all">All</Tab>
  <Tab value="open">Open</Tab>
</Tabs>

Drop column visibility

If your row has < 6 columns, every column is essential. Hiding earns nothing. Keep the table fixed-column.

// remove ColumnMenu + storage
// inline ALL_COLS at render:
{ALL_COLS.map(c => (
  <th key={c.key}>{c.label}</th>
))}

Drop inline editing

If edits trigger workflows (PO approval, audit trail), edits belong in a drawer with explicit Save, not inline. See the autosave-drawer recipe.

<td onClick={() =>
  openDrawer(row.id)
}>
  {row[c]}
</td>
// no EditableCell at all

Accessibility

More layers = more places to fail. These are the load-bearing ones.

  • The toolbar swap is announced. Wrap the toolbar slot in role="region" aria-live="polite" so screen readers say "3 selected" when the bulk bar appears.
  • Each Checkbox has an aria-label. Per-row: "Select TXN-0001". Header: "Select all on this page". Without labels they're 30 unnamed checkboxes.
  • Saved views menu uses role="menu". The DropdownMenu primitive does this for you — don't roll your own <ul>.
  • Sticky first column doesn't change tab order. Visual stickiness via CSS only; Tab still walks the cells in DOM order, left to right.
  • Destructive bulk actions go through AlertDialog. The dialog has role="alertdialog"; focus traps inside; Esc cancels; Enter on Confirm proceeds.
  • The "current view" badge has aria-current. In the views dropdown, the active item gets aria-current="true" so AT users know which preset is loaded.