Data tables are the most commonly misbuilt UI component in web applications. They look simple: rows, columns, some sort and filter controls. But they are used under conditions that expose every UX shortcut taken during development - large datasets, complex filter combinations, bulk operations, time pressure. The failure modes are consistent enough that you can predict where a data table will break before you even open it.
This article covers the five most common failure modes with practical fixes. Each one is a decision made during implementation that seemed fine at the time and becomes a usability problem in production.
Failure 1: Sort State That Disappears on Hover End
What happens: the sort indicator (the arrow icon in the column header) appears on hover and disappears when the user moves the mouse. Or: the sort indicator is visible when the table first loads but disappears after the user has interacted with other columns.
Why it breaks UX: users scan large tables while their mouse moves across the data. If the sort indicator only appears on hover, they have no persistent reference for which column is sorted and in what direction. They re-sort accidentally, or they are uncertain whether the data is currently sorted at all.
The fix: sort indicators should be permanently visible on the active sort column. The indicator should clearly distinguish ascending from descending (not just change color - use a directional icon that changes orientation). Non-sorted sortable columns can show a neutral icon on hover to communicate that sorting is available.
Using TanStack Table, sort state is tracked in component state and can be applied as a class or data attribute to the header cell, making persistent CSS styling straightforward:
<th
onClick={() => header.column.toggleSorting()}
aria-sort={
header.column.getIsSorted() === "asc"
? "ascending"
: header.column.getIsSorted() === "desc"
? "descending"
: "none"
}
>
{header.column.columnDef.header}
{header.column.getIsSorted() === "asc" && " (A-Z)"}
{header.column.getIsSorted() === "desc" && " (Z-A)"}
</th>
The aria-sort attribute also makes the sort state accessible to screen readers, addressing two problems with one implementation.
Failure 2: Filters With No Clear-All Path
What happens: users can apply multiple column filters, but clearing them requires clicking an X on each filter chip individually. There is no "Clear all filters" button. The fastest way to return to an unfiltered view is to refresh the page.
Why it breaks UX: users who apply filters frequently develop the refresh habit instead of looking for a clear-all path that does not exist. They assume clearing individual filters one by one is intentional behavior. Power users export filtered results to work around filter state management they find cumbersome.
The fix: show a "Clear filters" control whenever any filter is active. Display the count of active filters as a badge. When filter panels are collapsed, show an indicator that filters are applied so users do not assume the current view is the full dataset.
Also: persist filter state within a session. A user who filters to "Status: Open, Assignee: Maria" and navigates away should return to the same filter state, not a reset table.
Failure 3: Bulk Selection That Silently Caps at the Current Page
What happens: the table has a header checkbox that "selects all." With 500 rows across 10 pages (50 rows per page), clicking "select all" selects the 50 visible rows. The user runs a bulk action believing it applies to 500 rows. The action applies to 50.
Why it breaks UX: this is the data table failure mode with the most severe consequences. Depending on the action, users may not discover the error until they are looking at unexplained inconsistencies in their data. It also destroys trust in the bulk action feature permanently once users encounter it.
The fix: after the user selects the current page, show an explicit prompt with the full matching row count:
{selectedRows.length === currentPageRows.length && totalRows > currentPageRows.length && (
<div className="selection-prompt">
All {currentPageRows.length} rows on this page are selected.{" "}
<button onClick={selectAllRows}>
Select all {totalRows} rows matching your filter
</button>
</div>
)}
The prompt makes the selection scope explicit and gives the user an intentional path to selecting the full dataset.

Photo by Andrew Neel on Pexels
Failure 4: Row Actions That Require Navigation for Simple Edits
What happens: to change a single field on a record (updating a status, reassigning an owner, editing a short text value), users must click into a detail page, find the field, update it, save, and navigate back. For users who perform this operation many times per session, it is a significant time cost.
Why it breaks UX: the detail page navigation is designed for complex edits, not for the quick field updates that account for the majority of edit operations in most applications. The cost of the navigation is higher than the value of keeping the edit UI simple.
The fix: identify which fields are edited frequently and in isolation (status changes, owner reassignment, due date updates, simple text fields). Add inline editing for these fields with a clear editable state on hover and save/cancel controls on the row. Complex edits that require multiple fields or validation dependencies still go to the detail page.
For status fields in particular, a dropdown that opens directly in the cell without a dedicated edit mode is often the right pattern - the user clicks the status badge, the dropdown opens, they select a new value, and the update commits automatically.
Failure 5: No Visual Affordance for Keyboard Interaction
What happens: the table is fully interactive for mouse users but requires a mouse for sorting, filtering, row selection, and row actions. Keyboard users cannot use the table's core features.
Why it breaks UX: keyboard-only users exist in most enterprise applications. Accessibility compliance requirements aside, keyboard navigation is also used by power users who find it faster than mouse interactions for data management workflows.
The fix: sort toggles must be <button> elements or <th> elements with role="button" and tabindex="0". Filter triggers must be keyboard-reachable. Row selection checkboxes must have visible focus states and be keyboard-operable. Row action menus must open on keyboard trigger.
MDN documents keyboard interaction patterns for complex UI components. W3C's ARIA Authoring Practices Guide covers grid patterns specifically. Accessibility Insights is a free tool for checking whether your table passes keyboard accessibility tests.
The Pattern Behind All Five Failures
Each of these failures shares a root cause: the feature was built for the happy path and the edge cases were deferred or ignored. Sort was implemented without considering persistent state. Filters were added without designing a clear-all path. Bulk selection was built for the current page without auditing pagination behavior. Inline editing was scoped as "out of scope" for the initial release and never revisited.
The comprehensive guide to designing data table UX across all of these dimensions is in How to Design Data Tables for Complex Web Applications. 137Foundry builds and improves data-intensive web applications for teams where these table patterns are central to their users' daily workflow.

Photo by markusspiske on Pixabay
Quick Audit Checklist
Before shipping any data table update, verify:
- Sort indicator is always visible on the active column, not just on hover
- A "Clear all filters" control exists and is visible when filters are applied
- "Select all" scope (current page vs. all matching rows) is explicitly communicated
- Frequently edited fields support inline editing without full-page navigation
- All interactive controls (sort, filter, selection, row actions) are keyboard-accessible
These five checks cover the issues that generate the most user complaints and support requests for data tables in production. Working through them before release is faster than tracking down and fixing them after users have filed tickets.
Top comments (0)