Skip to content

Interface

Tables people actually use, and the four rules behind them

A table is the most used component in any operational product and the one most likely to exist three different ways in the same codebase, each with its own search box behaving differently.

The short answer

One search per table. Search on the server when the data is larger than a page and on the client when it is not, and never both at once. Keep the previous page rendered while the next loads. Let the table own its empty state, because only the table knows why it is empty.

One search per table

The rule sounds trivial and it is the one broken most often. A table acquires a global search, then a per column filter, then a row of quick filter chips, and now three controls narrow the same set with different semantics and no shared state.

Nobody can form a mental model of that. They type in one box, get a result, type in another, and the result changes in a way that does not compose with the first.

One search. If a column genuinely needs its own filter, it is a filter, it is visibly labelled as one, and the way it combines with the search is stated on the screen rather than guessed at.

Search where the data is

This decision is about size, not preference.

If the entire set fits comfortably in one payload, ship it and filter on the client. It is instant, it survives a flaky connection, and it removes a round trip per keystroke. Right for a list of classes, categories, or staff.

If the set does not fit, search on the server, with the query held in the URL. Slower per keystroke and correct at any size.

The failure mode is the one in between: paginating from the server while filtering on the client. The box then searches the current page only, while looking exactly like a search over everything. Users reasonably conclude that records are missing. That is a data integrity complaint caused entirely by a user interface decision, and it costs far more to answer than it would have cost to avoid.

Keep the previous page while the next loads

Replacing rows with a spinner on every page change is jarring in a way people cannot quite articulate but consistently dislike. They lose their place, the layout collapses and then reflows, and the wait feels considerably longer than it is.

Keep the previous rows on screen, dim them slightly or run a thin progress line, and swap when the data arrives. The same milliseconds feel like a fraction of the wait.

Pair that with pagination state in the URL, so a refresh, the back button and a shared link all land in the same place. It is a five minute change that removes an entire category of complaint.

The table owns its empty state

Three different empty states get flattened into one message constantly, and each needs a completely different response.

  • Nothing exists yet. The answer is how to create the first one.
  • Nothing matches the current filter. The answer is which filter is responsible, and a control to clear it.
  • Something failed. The answer is what failed and how to retry. This must never look like an empty set, because a failed request rendered as “no results” is a lie the user will act on.

Only the table knows which of the three it is in. Pushing the empty state up to the page level makes it generic, and generic means wrong two thirds of the time.

What a good table does when nothing is wrong

It shows how many rows exist in total, not just how many are on screen. It keeps column widths stable between pages so the eye does not have to reacquire them. It puts the most scanned column on the left. It right aligns every number. And it never moves a row underneath a cursor that was about to click it.

Questions people actually ask

Client side or server side search?
If the whole set fits in one reasonable payload, search on the client, because instant beats correct but slow at that size. If it does not fit, search on the server. The real failure is doing both, where a client filter over one page pretends to be a search over everything.
Why keep the previous page visible while loading?
Because replacing a table with a spinner destroys the reader's position and their sense of place. Keeping the old rows with a subtle loading state makes pagination feel instant even when it is not.
Should tables use infinite scroll?
Almost never in operational software. People need to know how much there is, to be able to reach the end, and to return to the same place tomorrow. Pagination held in the URL gives all three.

Published June 2026, updated July 2026.

Hiring someone to own work like this?

I am open to senior backend and full stack roles, remote and permanent.

Get in touch / Read the CV