# Building a Virtualized Data Table: Sorting, Filtering, Selection

Source: https://www.techinterview.org/post/3233475104/building-virtualized-data-table-frontend/
Updated: 2026-06-30 · techinterview.org

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.
