Data tables are one of the most-built and most-broken UI components. The naive implementation breaks at 1,000 rows. The interview question “build a Linear-style data table” or “build the rows of an admin panel” probes whether you know the patterns: virtualization, controlled vs uncontrolled state, selection, keyboard navigation, and the brutal complexity of multi-column sort/filter.
Functional requirements
- Display thousands of rows smoothly
- Sort by column (click header)
- Filter by column
- Multi-row selection (checkbox + shift-click range)
- Column resize, reorder
- Keyboard navigation
- Sticky header, sometimes sticky columns
Architecture
Three core pieces:
- Data source (server-paginated, server-sorted, server-filtered) or in-memory
- Render layer with virtualization
- Interaction layer (selection, keyboard, resize)
Server-side vs client-side data
For more than ~10K rows, server-side is the only viable path:
- Sort/filter on the database
- Paginate (cursor-based, not offset, to avoid stale-page issues)
- Client requests visible window + buffer
Client-side is fine for <10K rows. Simpler architecture, but slower for huge datasets.
Virtualization
Render only visible rows. Libraries: TanStack Table (formerly React Table) + TanStack Virtual is the standard 2026 stack.
For tables with variable-height rows (long text, expandable rows), use dynamic measurement. For fixed-height rows, simpler offset arithmetic.
Selection
Three modes:
- Single: click row → highlight
- Multi: checkbox column
- Range: shift-click between two rows; Ctrl/Cmd-click toggles
“Select all visible” vs “Select all matching filter” is a critical UX distinction. Don’t select the whole 10M-row dataset by accident.
Sort
Click column header → toggle sort. Click again → reverse. Click again → clear.
Multi-column sort: shift-click adds secondary sort. UI shows the sort priority. Users who do not know about this feature do not need to encounter it; users who do, expect it.
Filter
Per-column filters work for simple cases. For complex queries (date ranges, multi-select, OR conditions), a separate filter panel is better UX.
Server-side filter logic should match what the client UI suggests is happening.
Column resize and reorder
Resize: drag column edge. Persist to local storage so user preferences survive across sessions.
Reorder: drag column header. Same — persist to local storage.
Keyboard navigation
- Tab to enter the table
- Arrow keys to move between cells
- Space to toggle selection
- Shift+arrow to extend selection
- Page Up/Down to scroll
- Cmd+A to select all visible
Accessibility
- Use semantic
<table>,<thead>,<tbody> - aria-rowindex/aria-colindex for virtualized rows
- aria-sort on column headers
- aria-selected on row checkboxes
- Live region announces “5 rows selected” when count changes
Performance budget
- Initial render: <100ms
- Scroll: 60fps even with 100K rows
- Filter/sort: <200ms perceptual response (with skeleton during pending server query)
Common mistakes
- Building tables without virtualization (breaks at 1000 rows)
- Re-rendering all rows on every selection change
- Server-side pagination but client-side sort (incoherent)
- Sticky header that breaks on virtualization
- No keyboard support
Frequently Asked Questions
TanStack Table or AG Grid?
TanStack Table for most React apps — flexible, headless, modern. AG Grid for enterprise admin panels with rich features (Excel-like cell editing, charts) where you do not want to build them.
How do I handle a row with 50+ columns?
Sticky first column, horizontal scroll, column visibility toggles. Dense data needs density-friendly UX.
What if my server returns rows out of order?
Use cursor-based pagination with stable IDs. Server returns rows in a deterministic order tied to the cursor.