Data Table Pattern
Build production-ready data tables for SaaS dashboards with loading, empty, error, filters, pagination, and row actions.
OverviewLink to section
Data tables are a core SaaS dashboard pattern.
They power admin views, billing screens, customer lists, logs, subscriptions, projects, invoices, and operational workflows. A production-ready table is not only rows and columns. It is a complete interface for understanding, filtering, acting on, and navigating structured data.
Use this pattern when users need to:
- scan repeatable records
- compare structured values
- filter or search a dataset
- act on individual rows
- move through paginated results
- recover from loading, empty, and error states
Why this matters
Tables are where product complexity becomes visible. If loading, filters, row actions, and pagination feel inconsistent, the entire dashboard feels less reliable.
What you’ll buildLink to section
Table anatomy
Controls
Production states
Mental modelLink to section
The table renders structure. The data layer owns truth. The toolbar controls intent. The footer handles navigation.
| Area | Responsibility |
|---|---|
| Header | Page context and primary actions |
| Toolbar | Search, filters, bulk actions, and view controls |
| Table | Rows, columns, selection, and row rendering |
| Row actions | Contextual actions for one record |
| Footer | Pagination, counts, and selection summary |
| Data layer | Fetching, sorting, filtering, permissions, and mutations |
System rule
A table should not own the data source. Keep fetching, filtering, sorting, permissions, and mutations in the feature layer.
StepsLink to section
Define the anatomyLink to section
A production table is composed from distinct areas. Each area should evolve independently.
export function ProjectsTable() {
return (
<section className="space-y-4">
<ProjectsTableHeader />
<ProjectsTableToolbar />
<ProjectsTableContent />
<ProjectsTableFooter />
</section>
);
}This keeps the table flexible when product requirements grow.
Keep data outside the tableLink to section
The table should receive data, status, and callbacks. It should not own fetching, permission checks, or mutation rules.
export function ProjectsTableContainer() {
const projects = useProjects();
const filters = useProjectFilters();
return (
<ProjectsTable
data={projects.data}
status={projects.status}
filters={filters.value}
onFiltersChange={filters.setValue}
onRetry={projects.refetch}
/>
);
}The feature layer owns truth. The table renders the current state.
Design states before rowsLink to section
Every table needs explicit loading, empty, and error states.
No resultsTry adjusting your filters. |
Failed to load dataPlease try again later. |
Do not add these states at the end. They define the production feel of the table.
Add controls deliberatelyLink to section
Filters and row actions should support the task without making the table feel crowded.
Use progressive disclosure:
- inline controls for frequent filters
Sheetfor advanced filtersDropdownMenufor secondary row actions- confirmation or recovery for destructive actions
| Name | Status | Actions |
|---|---|---|
| Acme | Active |
AnatomyLink to section
A scalable table has clear regions.
| Region | What belongs there | What to avoid |
|---|---|---|
| Header | title, description, primary action | data fetching logic |
| Toolbar | search, filters, bulk actions | every possible control at once |
| Table | rows, columns, selection, row actions | business rules and server logic |
| Footer | pagination, item count, selected count | hidden navigation state |
Why this matters
When responsibilities are separated, the table can grow without becoming a single fragile component.
StatesLink to section
A production table handles more than loading and success.
| State | Component | Goal |
|---|---|---|
| Loading | Skeleton | Preserve shape while data is expected |
| Empty | EmptyState | Explain valid empty results |
| Error | Alert | Explain failure and recovery |
| Filtering | Toolbar state | Show active constraints clearly |
| Mutating | Inline state or toast | Confirm action progress and recovery |
| Paginating | Footer state | Keep navigation predictable |
Never use the same feedback pattern for every state. Loading data, deleting a row, saving filters, and changing pages are different user experiences.
VariationsLink to section
Simple table
Use when the dataset is small and users mainly need to scan records.
Decision guideLink to section
Use this pattern if:
- the page displays structured repeatable data
- users need to scan, filter, compare, or act on rows
- the table appears in a dashboard or admin surface
- loading, empty, and error states matter
- pagination, row actions, or bulk actions are required
Avoid this pattern if:
- content is editorial or marketing-focused
- data is better represented as cards, a timeline, or a feed
- the user only needs a single summary value
- mobile-first scanning is more important than tabular comparison
- the table would hide the primary product action
Prefer
- explicit loading, empty, and error states
- controlled pagination and filtering
- row actions in contextual menus
- progressive filters for complex datasets
- clear ownership between data layer and UI
Avoid
- fetching data inside a visual table primitive
- using a table for non-tabular content
- hiding blocking failures behind only a toast
- crowding the toolbar with every possible filter
- destructive row actions without confirmation or recovery
Production checklistLink to section
Before shipping a data table, confirm that:
- loading preserves the expected layout
- empty state explains the result and next action
- error state includes recovery or retry
- pagination is controlled and predictable
- filters can be cleared
- active filters are visible
- row actions are keyboard accessible
- destructive actions require confirmation or recovery
- selection state is clear when bulk actions exist
- mobile behavior is defined
Ready to turn this pattern into a real SaaS surface?
Starter Pro gives you a stronger foundation for production dashboards, protected routes, auth, billing, and backend-backed product flows. → /docs/starter-pro