DataTable
Composable data display system for table, grid, and map views with toolbar controls, selection, modifiers, and saved preferences.
Overview
DataTable is the most important data-display component in the platform.
It is not just a table widget. It is a composable system for:
- table views
- grid views
- map views
- selection and bulk actions
- search, filters, sorting, and saved views
- master-detail flows
- infinite scrolling datasets
The current platform pattern is the composable namespace API:
DataTable.RootDataTable.ToolbarDataTable.Content
Most brand-dashboard list pages are built from those three layers plus optional helpers.
Core Structure
import { DataTable } from '@fsai/shared-ui';
<DataTable.Root
itemName="Lead"
items={items}
isLoading={isLoading}
isFetching={isFetching}
totalResultsCount={totalCount}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
isFetchingNextPage={isFetchingNextPage}
localStorageKey="sales_list"
enableColumnHiding
className="flex-1"
>
<DataTable.Toolbar>
<DataTable.Toolbar.Left>
<DataTable.Search onChange={setSearch} placeholder="Search leads..." />
<DataTable.Filters
available={availableFilters}
applied={appliedFilters}
onApply={setAppliedFilters}
/>
<DataTable.Sorting
available={availableSorting}
applied={appliedSorting}
onApply={setAppliedSorting}
/>
<DataTable.ColumnsModifier columns={columns} />
</DataTable.Toolbar.Left>
<DataTable.Toolbar.Right>
<DataTable.ResultsCount total={totalCount} itemName="Lead" />
<DataTable.Menu actions={menuActions} />
</DataTable.Toolbar.Right>
</DataTable.Toolbar>
<DataTable.Content
view="table"
columns={columns}
onRowClick={handleRowClick}
error={error}
errorState={{
serverError: {
title: 'Failed to Load Leads',
description: 'There was a problem loading leads. Please try again.',
primaryAction: {
label: 'Retry',
role: 'button',
variant: 'primary',
onClick: () => void refetch(),
leftAdornment: 'ArrowRotateClockwise',
},
},
}}
searchValue={searchValue}
appliedFiltersCount={appliedFilters.length}
modifiers={modifiers}
emptyState={emptyState}
table={{ resizableColumns: true, reorderableColumns: true }}
/>
</DataTable.Root>That is the main pattern to copy for dashboard list pages.
Namespace API
Root And Context
| Part | Purpose |
|---|---|
DataTable.Root | Provider-backed root wrapper for items, loading state, selection state, view mode, persistence, and infinite scroll. |
DataTable.Provider | Lower-level provider when the root shell is not wanted directly. |
DataTable.useContext | Accesses shared data-table state such as viewMode, selection, and scroll helpers. |
Toolbar
| Part | Purpose |
|---|---|
DataTable.Toolbar | Toolbar row above the content view. |
DataTable.Toolbar.Left | Left-aligned controls such as search, filters, sorting, and saved views. |
DataTable.Toolbar.Right | Right-aligned controls such as result counts and primary actions. |
DataTable.Search | Search input for table-local or server-driven search. |
DataTable.Filters | Filter builder for structured available/applied filters. |
DataTable.Sorting | Sorting selector for available/applied sort options. |
DataTable.ColumnsModifier | Column visibility and column-order management UI. |
DataTable.CheckboxFilters | Chip-style filter controls for simpler filtered list pages. |
DataTable.ViewToggle | Switches between supported view modes such as table and grid. |
DataTable.GridDensityToggle | Grid column-density control for grid views. |
DataTable.ResultsCount | Displays result totals using the current item naming. |
DataTable.PrimaryAction | Primary create/add action. |
DataTable.SecondaryAction | Secondary toolbar action. |
DataTable.Menu | Overflow actions rendered from DataTableAction[]. |
DataTable.SavedViews | Saved filter/sort/column view presets. |
Toolbar Action Rule
Toolbar actions should follow a consistent rule:
- if there is only one main action, render it directly with
DataTable.PrimaryAction - if there is more than one action, use
DataTable.Menu - when using a menu, the first item should always be creation-oriented
- if there are multiple creation paths, the creation-oriented first item can expand into a submenu
This keeps table toolbars easier to scan and makes the primary path obvious.
Content And Views
| Part | Purpose |
|---|---|
DataTable.Content | Dispatches to the correct view implementation based on view. |
DataTable.TableView | Lower-level table renderer. |
DataTable.GridView | Lower-level grid renderer. |
DataTable.MapView | Lower-level map renderer. |
DataTable.Panel | Supporting panel component for content-level detail layouts. |
DataTable.Modifiers | Selection modifier bar for bulk actions. |
DataTable.Group | Grouped row/list rendering for advanced grouped content. |
Empty And Error States
DataTable.Content owns both collection emptiness and collection failure handling.
Use:
emptyStatewhen the dataset is genuinely emptyerrorwhen the data request failederrorStateto override the sharedErrorGuardcopy or actions, especially for retry
DataTable intentionally keeps toolbar and surrounding context visible while the content region shows the failure.
Grid Item Companion
DataTableGridItem is an important companion export for DataTable.Content view="grid".
It is not part of the DataTable namespace, but it is the standard grid-card shell used by brand-dashboard grid views.
Common parts:
| Part | Purpose |
|---|---|
DataTableGridItem | Root clickable grid card container. |
DataTableGridItem.Hero | Visual top section with icon or background image. |
DataTableGridItem.Hero.Badge | Badge slot inside the hero area. |
DataTableGridItem.Body | Main content region. |
DataTableGridItem.Body.Top | Top content stack inside the body. |
DataTableGridItem.Body.Bottom | Bottom metadata/detail stack inside the body. |
DataTableGridItem.Title | Primary grid item title. |
DataTableGridItem.SubTitle | Secondary title text. |
DataTableGridItem.Description | Longer supporting description text. |
DataTableGridItem.Detail | Label/value metadata row. |
DataTableGridItem.Highlight | Highlighted inline content block. |
DataTableGridItem.Menu | Overflow actions for the grid item. |
DataTableGridItem.Menu.Item | Menu action item. |
DataTableGridItem.Menu.Divider | Divider inside the item menu. |
DataTable.Root Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | — | Dataset for the current view. |
itemName | string | { singular: string; plural: string } | 'item' | Naming used by result counts and selection UI. |
isLoading | boolean | — | Initial loading state. |
isFetching | boolean | — | Background refetch state. |
isFetchingNextPage | boolean | — | Infinite-scroll pagination fetch state. |
hasNextPage | boolean | — | Whether infinite-scroll can continue. |
fetchNextPage | () => void | — | Infinite-scroll loader callback. |
totalResultsCount | number | — | Server-driven total count. |
selectionMode | 'single' | 'multiple' | 'multiple' | Selection behavior. |
checkedIds / setCheckedIds | controlled selection pair | — | Controlled selection state. |
isTopLevelChecked / setIsTopLevelChecked | controlled top-level selection pair | — | Controlled select-all state. |
disabledIdentifiers | Set<string> | — | Prevents certain items from being selected. |
viewModes | [DataTableViewMode, ...DataTableViewMode[]] | ['table'] | Supported views. |
initialViewMode | DataTableViewMode | 'table' | Default view mode. |
localStorageKey | string | — | Persists view mode and column preferences. |
enableColumnHiding | boolean | false | Enables column preference UI and persistence. |
columns | DataTableColumn<T>[] | — | Optional columns passed into context for column state helpers. |
className | string | — | Additional root layout classes. |
DataTable.Content Modes
Table View
<DataTable.Content
view="table"
columns={columns}
onRowClick={handleRowClick}
error={error}
errorState={{
serverError: {
title: 'Failed to Load Rows',
description: 'There was a problem loading these results. Please try again.',
primaryAction: {
label: 'Retry',
role: 'button',
variant: 'primary',
onClick: () => void refetch(),
leftAdornment: 'ArrowRotateClockwise',
},
},
}}
modifiers={modifiers}
emptyState={emptyState}
searchValue={searchValue}
appliedFiltersCount={appliedFilters.length}
table={{
resizableColumns: true,
reorderableColumns: true,
hideVerticalLines: true,
}}
/>Grid View
<DataTable.Content
view="grid"
gridItemRenderer={(props) => <CourseGridItem {...props} item={props.item} />}
grid={{ fixedGridColumns: 4 }}
onRowClick={handleOpen}
modifiers={modifiers}
emptyState={emptyState}
searchValue={searchValue}
appliedFiltersCount={appliedFilters.length}
/>When the grid items are card-like content, DataTableGridItem is usually the right renderer shell:
<DataTable.Content
view="grid"
gridItemRenderer={(props) => (
<DataTableGridItem
onClick={props.onClick}
isChecked={props.isChecked}
isActive={props.isActive}
isLoading={props.isLoading}
>
<DataTableGridItem.Hero icon="GraduateCap">
<DataTableGridItem.Hero.Badge label="Published" color="green" />
</DataTableGridItem.Hero>
<DataTableGridItem.Body>
<DataTableGridItem.Body.Top>
<DataTableGridItem.Title>{props.item.title}</DataTableGridItem.Title>
<DataTableGridItem.Description>
{props.item.description}
</DataTableGridItem.Description>
</DataTableGridItem.Body.Top>
<DataTableGridItem.Body.Bottom>
<DataTableGridItem.Detail label="Created">
{format(new Date(props.item.createdAt), 'MMM d, yyyy')}
</DataTableGridItem.Detail>
</DataTableGridItem.Body.Bottom>
</DataTableGridItem.Body>
</DataTableGridItem>
)}
/>Map View
Use view="map" when the dataset is spatial and the screen has a custom map renderer.
Error Recovery
DataTable does not expect product code to drop a raw ErrorState into the middle of the table layout.
Instead:
- pass the failure object through
error - customize recovery UI with
errorState - keep using
emptyStatefor no-results and zero-data cases
Internally, the content wrapper renders ErrorGuard with table-tuned spacing so the fallback reads as part of the collection surface instead of replacing the whole page.
See the error-handling patterns page for the broader platform rule.
Factory Helpers
The column factory helpers are one of the most important parts of the current API. They keep the dashboard's table cells more consistent and reduce repeated boilerplate.
| Helper | Purpose |
|---|---|
createEntityColumn | Avatar/icon + strong primary entity label column. |
createEntityReferenceColumn | Single related-entity badge column, optionally clickable. |
createEntityMultiReferenceColumn | Multiple related-entity badges with overflow summary. |
createTextColumn | Standard text/truncation column. |
createDateColumn | Standard formatted date column. |
createMoneyColumn | Standard money-formatting column. |
createBadgeColumn | Fixed-color badge column. |
createDynamicBadgeColumn | Badge column with per-row icon/color mapping. |
formatGenericTableValue | Shared fallback formatter for generic values. |
Example:
const columns = [
createEntityColumn({
id: 'name',
label: 'Project',
entityType: 'workflow',
getValue: (item) => item.name,
}),
createDateColumn({
id: 'createdAt',
label: 'Created',
getValue: (item) => item.createdAt,
}),
];Common Usage Patterns
Power-User List Page
Common in leads, marketing, vendors, franchisees, and other operational list pages:
DataTable.RootSearchFiltersSortingColumnsModifierSavedViewsResultsCountMenuorPrimaryActionContent view="table"
Toolbar Action Patterns
Single Action -> Direct Button
When the table has one primary action, put it directly in the toolbar:
<DataTable.Toolbar.Right>
<DataTable.ResultsCount total={courses.length} itemName="Course" />
<DataTable.PrimaryAction
action={{
type: 'single',
label: 'Create Course',
action: () => setIsCreating(true),
leftAdornment: 'PlusSmall',
}}
/>
</DataTable.Toolbar.Right>This is the cleanest pattern when the action model is simple.
Multiple Actions -> Menu
When the table exposes multiple actions, use a menu instead of multiple peer buttons:
const menuActions = [
{
type: 'single',
label: 'Add Location',
action: () => setIsCreateLocationModalOpen(true),
leftAdornment: 'PlusAdd',
},
{
type: 'single',
label: 'Imports in Progress',
action: () => openLocationImportsDialog({}),
leftAdornment: 'Pop',
},
{
type: 'single',
label: 'Import From CSV',
action: () => setIsImportFromCsvModalOpen(true),
leftAdornment: 'SquareArrowTopRight',
},
];
<DataTable.Toolbar.Right>
<DataTable.ResultsCount total={totalCount} itemName="Location" />
<DataTable.Menu actions={menuActions} />
</DataTable.Toolbar.Right>The first menu item should be the creation-oriented path.
Mixed Table And Grid View
Common in learning and library-style content:
viewModes={['grid', 'table']}ViewToggleGridDensityToggleContentswitches betweentableandgridDataTableGridItemoften provides the standard card shell for the grid renderer
Selection And Bulk Actions
Use modifiers when the list supports bulk operations such as delete, duplicate, retagging, or batch updates.
Master-Detail
Common in CRM and dashboard operational flows:
onRowClickopens aPanelactiveItemhighlights the selected item- some views use
Content'spanelsupport, but many app screens wire directly intoPanelStack
Advanced Grouped Data
Use DataTable.Group and related grouped rendering patterns when the content is still tabular but organized into meaningful sections, such as phase-grouped project tasks.
Brand Dashboard Usage Patterns
Representative usages:
| Pattern | Path | Notes |
|---|---|---|
| Full list-page stack | fsai/apps/brand-dashboard/src/pages/Leads/Leads.components.tsx | Best example of the full composable table pattern with search, filters, sorting, columns, saved views, modifiers, and row-to-panel behavior. |
| Another power-user table | fsai/apps/brand-dashboard/src/pages/Marketing/MarketingTable.tsx | Similar operational list pattern with server-driven totals and saved preferences. |
| Table/grid switching | fsai/apps/brand-dashboard/src/pages/Learning/components/CoursesTab.tsx | Best current example of one Root supporting both table and grid views. |
| Single toolbar action | fsai/apps/brand-dashboard/src/pages/Learning/components/CoursesTab.tsx | Shows the correct direct DataTable.PrimaryAction pattern when there is only one main action. |
| Multi-action toolbar menu | fsai/apps/brand-dashboard/src/modules/locations/components/LocationsTable/LocationsTable.tsx | Strong example of using DataTable.Menu when the table has multiple top-level actions, with creation first. |
| Grid-oriented content | fsai/apps/brand-dashboard/src/pages/Learning/components/CourseGridItem.tsx | Best example of DataTableGridItem used as the standard card shell for grid mode. |
| Simple grid card | fsai/apps/brand-dashboard/src/pages/StudioQrTemplates/QrTemplateGridItem.tsx | Shows a leaner DataTableGridItem composition without the full hero/menu/detail stack. |
| View-mode context usage | fsai/apps/brand-dashboard/src/pages/AdminPanel/Helpdesk/ArticlesTable.tsx | Good example of using useDataTableContext for custom view controls. |
| Grouped data | fsai/apps/brand-dashboard/src/pages/Projects/ProjectDetail/PhaseView/PhaseView.primitives.tsx | Strong example of DataTable.Group and grouped task display. |
| Vendor/location tables | fsai/apps/brand-dashboard/src/modules/vendors/components/VendorsTable/VendorsTable.tsx | Shows how the pattern scales across feature modules. |
| Location-scoped table | fsai/apps/brand-dashboard/src/modules/vendors/components/VendorLocationsTable/VendorLocationsTable.tsx | Good example of factory helpers plus scoped entity navigation. |
Important Conventions
- Prefer the composable namespace API over any old monolithic table examples.
- Put shared dataset state on
DataTable.Root, not onDataTable.Content. - Put toolbar controls in
DataTable.Toolbar, not inside the table body. - Put
emptyState,modifiers,searchValue, andappliedFiltersCountonDataTable.Content. - Use
localStorageKeywhenever view mode or column preferences should persist. - Use
enableColumnHidingwhen you exposeColumnsModifier. - Use
DataTable.PrimaryActiononly when there is one main toolbar action. - Use
DataTable.Menuwhen there are multiple toolbar actions, and make the first item creation-oriented. - Use
DataTableGridItemas the default shell for card-style grid renderers unless the grid needs a genuinely different structure. - Use the factory helpers for common entity, badge, text, date, and money columns instead of rebuilding the same renderers repeatedly.
DataTable.ResultsCountoften mirrors the server total, whiletotalResultsCountonRootfeeds shared data-table state.DataTable.Contentsupports a built-inpanelprop, but many dashboard screens still use app-ownedPanelStackpatterns directly.
Guidelines
- Do treat
DataTableas a composable layout system, not just a table component - Do use
DataTable.Root+ toolbar +DataTable.Contentas the default dashboard pattern - Do use factory helpers for repeated column patterns
- Do put a single top-level toolbar action directly in
DataTable.PrimaryAction - Do use
DataTable.Menuwhen there is more than one top-level action, with creation first - Do use
DataTableGridItemfor standard card-like grid items - Do use
viewModesonly when the dataset genuinely supports multiple useful representations - Do use modifiers for bulk actions instead of inventing parallel selection bars
- Don't document or implement new tables using the outdated fictional one-component API
- Don't scatter multiple peer action buttons across the toolbar when a menu is the clearer pattern
- Don't add grid or map views just because the component supports them