Skip to content

Data Table

A grid of rows and columns for scanning, comparing and acting on records — the component most B2B products are built around.

UI Components

A data table is a grid of rows and columns for scanning, comparing and acting on records. In most B2B products it is not a component so much as the product itself, with the rest of the interface arranged around it.

That makes it the place where density, performance and workflow decisions all land at once, and where a component that demos comfortably with six rows stops working at ninety thousand.

Scannability comes first

Right-align numbers, left-align text. Numbers compared down a column are read at the edge, which only works when the digits line up. Use a tabular figure set so 1 and 8 occupy the same width, and keep decimal places consistent.

A sticky header. Scrolling a hundred rows without column names means scrolling back up to remember which column is which.

Column widths that fit the content. A date column sized for a paragraph wastes the row; a name column sized for a date truncates every entry.

Row height as a deliberate choice. Comfortable for reading a few rows, compact for scanning many. In a tool people live in, offer both and remember the setting.

Zebra striping is usually unnecessary and often noise. A clear row separator and adequate line height do the same job more quietly.

The operations around it

A table on its own is a read-only list. What makes it a product is everything attached to it.

Sort on the columns that make sense, with the current sort visible and in the URL.

Filter, with the active filters shown outside the panel that set them. See filter and chip.

Select and act. Row selection, a running count, and a decision about whether select-all means the page or the whole set. See bulk actions.

Open. Whether a row opens a page, a drawer or expands in place is one of the more consequential decisions here, because it determines whether somebody keeps their place in the list.

Export, which is the pressure valve for everything the table cannot do.

Volume is what breaks it

Pagination, not infinite scroll. People need to return, to share a view, and to reach the footer. See pagination.

State in the URL. Page, sort, filters, column set. Without it nothing about the current view can be shared or restored, which in an internal tool generates a surprising amount of daily friction.

Virtualise long lists rather than rendering them. Ten thousand rows in the DOM is a slow page and a large memory cost.

Cursor paging past a few hundred pages. Offset paging degrades badly at depth, and switching removes the ability to jump to an arbitrary page — a real trade, made deliberately.

Column count has a cost. Twenty columns means horizontal scrolling, and horizontal scrolling hides the identifier column, which is the one thing that must stay visible. Freeze the first column or reduce the default set.

The states that get skipped

Filtered to empty is not empty. A new account with no records and an account whose filters exclude everything need different messages: one explains what goes here, the other offers to clear the filter. Showing the new-account empty state to somebody with nine thousand records is a small, confusing failure.

Loading should preserve the layout. Render the header and skeleton rows so nothing jumps when data lands.

Partial failure needs somewhere to go. When six of forty rows failed to update, "6 failed" with no route to them is worse than no message.

In practice

A table works well through the trial and starts generating support tickets once real customers are on it.

Three things had been invisible at demo scale. There are eleven columns, so the page scrolls horizontally and the customer name — the only column anyone uses to find a row — scrolls out of view. Sorting resets to page one silently, so somebody deep in a list loses their place. And the filter panel closes after use, leaving no indication that a filter is active, which produces a steady stream of "records are missing" reports.

None of it needed a redesign. A frozen first column, a column picker with a remembered default, filter state shown as chips above the table, and the sort and page in the URL. The table is the same table; it simply now survives being used.

Where teams get it wrong

  • Centring numbers. Comparison down the column becomes impossible.
  • All columns by default. Horizontal scroll hides the identifier.
  • No state in the URL. Nothing shareable, nothing restorable.
  • Infinite scroll on working data. Position lost, footer unreachable.
  • One empty state. A new account and a filtered result are different situations.
  • Testing on a demo account. Every volume problem is invisible until it is not.

Frequently Asked Questions