How should a B2B SaaS product design data tables, filters and saved views?
A B2B SaaS team can decide what filters, saved views, columns, paging and row actions should do by filling in one specification worksheet per table, then testing each decision with a short acceptance check.
Illustrative situation: a collections manager at a B2B billing platform opens the invoices table every Monday. She filters to overdue, over $5,000 and owned by her team, sorts by days late, hides four columns and pastes the link into chat for a colleague. The colleague sees every invoice, in a different column order, newest first. Later that week an admin edits the shared “Overdue” view to add a region filter, and the manager’s Monday list quietly loses rows. Nobody can say which view is the real one, so she keeps a screenshot. The scene is invented, but the pattern is common: filters, columns and views were built as separate features, and nobody decided what a “view” is.
Humbleteam, a digital product design agency, publishes this guide. It treats a table as a set of decisions a team can write down and test: what task the table serves, how filters behave, what a saved view stores, how rows load, and what a click on a row does. A worksheet near the end collects them.
What job should the table do?
Nielsen Norman Group (NN/g) names four core tasks a table should support: find records that fit criteria, compare data, view, edit or add a single row, and take action on records. Rank them for each table before drawing anything. A collections queue is mostly find and act. An audit log is find and compare.
The ranking also shows when a table is the wrong tool. Carbon’s data table guidance says not to use one as a replacement for a spreadsheet application. If people need 30 columns side by side and their own formulas, the right deliverable may be a good export, not a bigger grid.
Two NN/g defaults are cheap. Make the first column a human-readable identifier, such as an invoice number with a customer name, not an auto-generated ID. And order columns by importance, with related ones adjacent, so nobody’s eyes travel between column 1 and column 20.
How should filters work?
Start with the labels. NN/g’s guidance on filter categories and values asks for filters that are appropriate, predictable, free of jargon and prioritized. In one NN/g test, a vague “Item type” category beside a more specific “Style” category sent a participant to the wrong place when she looked for sandals. In B2B products the usual culprit is internal status codes. Use the words your customer’s finance or ops team uses.
Then decide when a filter applies. Carbon’s filtering pattern separates instant updates, good when users make one selection, from batch updates behind an “Apply filters” button, better for several selections across categories and for slow data return.
Show the state. NN/g says users should be able to tell at a glance that the data is filtered. Carbon adds that when filters hide in a drawer, the closed control needs a count of applied filters and a way to clear them without reopening it. Polaris suggests no more than 2 or 3 promoted filters, and the rest behind a menu.
Logic needs a stated rule. GitHub Projects treats multiple filters as AND, and multiple values for one field as OR, and does not support OR across different fields. Linear’s filter docs describe advanced filters with AND/OR logic and nested groups. Our suggestion: use AND across fields and OR within a field by default, write the result as a sentence (“Status is Overdue or Disputed, and Owner is my team”), and put nested groups behind an Advanced control.
Design the empty result. NN/g warns that a blank panel after filtering leaves users asking whether it is still loading, failed, or just empty, and recommends a short status message. Its guidance on no-results pages adds starting points. Our suggestion for a table: name the active filters, offer “Remove Region: West” on the most recent chip, and offer “Clear all.”
Which columns, sorting and density should people control?
NN/g says hiding and reordering columns must be low-effort and accessible to people who don’t use drag and drop, with a clear indicator when columns are hidden. HubSpot’s edit-columns dialog pairs checkboxes with drag-to-reorder. Stripe notes that column choices in a report apply to the on-screen view, while exports have their own column selection. Whichever you build, tell people which of the two a setting changes.
The WAI-ARIA Authoring Practices sortable table example wraps header text in a button and sets the aria-sort attribute on the sorted column. Add a stable tiebreaker, such as the record ID, so equal rows don’t reshuffle between loads. The tiebreaker is our suggestion.
Density has an accessibility limit. Carbon offers five row heights, from 24 to 64 px, and recommends the tallest only for two-line content. WCAG 2.2 sets a minimum pointer target of 24 by 24 CSS pixels, with a spacing exception. Tiny row buttons in a compact table can fail it. Whether density belongs to the person or to the view is a call to make explicitly; we lean toward the person.
Freezing needs a test. NN/g recommends freezing header rows and header columns when the table is larger than the screen. WCAG notes that sticky headers and footers can cover a focused element. Tab through the table with a frozen column and check that the focused cell stays visible.
What is a saved view, and who owns it?
Three products answer the ownership question differently.
- Airtable has collaborative, personal and locked views. Anyone who can edit may change a collaborative view’s configuration. A personal view is configurable only by its creator. A locked view stays fixed until someone with Creator or Owner permission unlocks it. Personal and locked views are paid-plan features.
- HubSpot makes saved views private by default and shares them as Private, Team, Everyone or Custom. Only the owner changes a view’s sharing settings, and Super Admins cannot change sharing on views they don’t own. The leftmost tab becomes the default view.
- GitHub Projects separates working state from the saved definition. A dot beside the view name shows unsaved changes that nobody else sees until you choose Save changes.
Linear gives every view an owner and reflects temporary filters in the URL: a copied link applies them, but sharing a link doesn’t grant access to a view.
From these, our suggestion is a model with three layers:
- The saved definition. It has a name, an owner and a visibility level. It stores filters, sort and columns.
- Personal changes on top. The view shows a “modified” marker with three actions: Save, Save as new view and Reset. Nothing reaches other people until the owner saves.
- The URL. It carries filters, sort, search and view ID, so a pasted link reproduces what you see, if the recipient has access.
Under that model the admin’s region filter is a deliberate save by the view’s owner, and the manager keeps a personal view that nobody else can edit.
Name views by purpose (“Overdue over $5,000, my team”), not by person, and choose the default view on purpose.
How should rows load: pages, load more or infinite scroll?
NN/g says infinite scrolling suits long lists of similar items but is less suited to finding a particular item. A Load More button restores access to the page footer and signals that more content exists, at the price of an extra click. Polaris says to paginate when a list holds more than 50 items.
For a work table that people leave and return to, pages are the safer default (our suggestion): “Showing 1-50 of 1,284,” a page-size control and the page in the URL, so Back returns to the same place.
Two limits matter at scale. Airtable loads views with more than 1,000 records as a preview with a “Load all records” action. And a table’s page is not its dataset: Stripe’s export includes all transactions matching your current filters, not just the visible page. Label export buttons with the count they will export.
What should a row click, a row action and an inline edit do?
Polaris says index table items should perform an action when clicked, usually opening the details page. NN/g notes that inline single-record actions work for one or two actions, and beyond that end up crowded or hidden behind hover or a generic menu. A default that follows: row click opens the record, one or two visible actions at most, the rest in a labeled menu.
Hover-only controls cost keyboard users. In a plain table, each widget is a separate tab stop, so 50 rows with three buttons each means 150 stops. The APG grid pattern shortens that with arrow-key navigation, but it requires focus-management code, and arrow keys then move between cells. Use it when the table is truly interactive or editable, and otherwise prefer a native HTML table.
For editing, NN/g says edit in place works only if the table is narrow, and the row should look different in edit mode. A modal hides neighboring records that people use as reference. A nonmodal side panel keeps both. Bulk actions, with their selection scope across pages and filters, are a separate problem; our guide to enterprise bulk action UX covers it.
What should loading, partial and stale states show?
Nielsen’s response-time limits give a rough scale: under 0.1 seconds needs no feedback, past 1 second show that the system is busy, and past about 10 seconds use a percent-done indicator with a way to cancel. Carbon recommends skeleton states instead of spinners when extra load time is expected. Our suggestions: keep the filter bar and headers usable while rows load, say plainly when the table shows a part of the data (“First 1,000 of 4,212 rows”), and show when data was last refreshed. Nielsen’s first heuristic is to keep users informed about what is going on, so a table that looks current but isn’t should say so.
The table specification worksheet
Fill one per table. The defaults are our suggestions, not standards, and the last column is a check a team or an agency can run.
| Decision | Options | Reasonable default | Acceptance check |
|---|---|---|---|
| Lead task | Find, compare, edit one, act | One lead task per table, written in one sentence | Three team members state the same task |
| First column | Generated ID, readable name | Readable identifier | A new user finds a known record by scanning |
| Filter apply | Instant, batch | Instant when fast; batch for slow queries or many categories | Time from change to result, measured |
| Filter logic | AND, OR, nested | AND across fields, OR within a field, nested behind Advanced | Reading the chips aloud gives the right query |
| Zero results | Blank, message | Names active filters; remove-last and clear-all | Apply filters that match nothing |
| View visibility | Private, team, everyone, locked | Private by default; owner controls sharing | A teammate cannot silently change your view |
| What a view stores | Filters, sort, columns, density, page | Filters, sort, columns; density and page stay personal | Open one view in two accounts |
| Unsaved changes | Auto-save, marker plus actions | Marker with Save, Save as new, Reset | Change a filter, reload, check |
| URL state | None, filters, filters plus view | Filters, sort, search, view ID; link is not access | Open a link with and without access |
| Paging | Pages, load more, infinite | Pages with count and page size | Open a record, press Back, same place |
| Row click | Open, select, nothing | Open record; 1-2 visible actions; rest in menu | Keyboard-only run through a full page |
| Editing | In place, side panel, modal | Side panel; in place only for narrow tables | Compare against a neighboring row while editing |
| Export | Visible, matching, all | Matching rows; button shows the count | File row count equals header count |
| Load, partial, stale | Spinner, skeleton, notice | Skeleton, partial notice, last-updated time | Throttle the network and read each state |
Illustrative worked example: back to the invoices table
This continuation is invented. The billing team writes the worksheet for the invoices table and changes five things.
| Area | Before | After |
|---|---|---|
| Shared “Overdue” view | Anyone can edit it; changes appear for everyone | Owned by the collections lead; others get a modified marker and Save as new |
| Manager’s Monday list | Rebuilt by hand each week | Personal view “Overdue over $5,000, my team,” opened by default |
| Filters | Eight dropdowns, state lost on reload | Three promoted filters, chips with counts, state in the URL |
| No matches | Blank table | “No invoices match Region: West and Over $5,000.” with remove and clear actions |
| Export | The 50 visible rows | All matching rows, button reads “Export 212 invoices” (invented count) |
Open questions remain: whether a locked view is worth building yet, and what density people choose. A short session with a few collections staff, using their own Monday view, would answer both.
Where do these defaults stop working?
The sources are design-system documentation, accessibility standards and product help pages. They show how well-known products behave, not how your users behave, and the products differ: Airtable’s personal and locked views need a paid plan, and HubSpot’s sharing is account-based. This guide does not cover mobile layouts for wide tables, which NN/g treats separately, real-time collaborative editing, or the engineering of very large datasets.
What can you produce now?
Four things, before anyone draws a screen:
- A worksheet per table with the lead task and a default for every row.
- A view model: the saved definition, personal changes and the URL, with ownership and sharing rules.
- A state list: default, filtered, zero results, loading, partial, stale and error, each with its copy.
- Acceptance checks you can run in a working session, including keyboard-only and network-throttled runs.
If you plan to bring in outside designers for the table, ask each team to fill the worksheet for one of your real tables and to explain the view model. Our comparison of UX/UI agencies that design complex B2B SaaS platforms shows which studios publish work on screens like these.
