Building a Virtualized Data Table: Sorting, Filtering, Selection

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:

  1. Data source (server-paginated, server-sorted, server-filtered) or in-memory
  2. Render layer with virtualization
  3. 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.

Scroll to Top