FSAI Design System
Components

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.Root
  • DataTable.Toolbar
  • DataTable.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

PartPurpose
DataTable.RootProvider-backed root wrapper for items, loading state, selection state, view mode, persistence, and infinite scroll.
DataTable.ProviderLower-level provider when the root shell is not wanted directly.
DataTable.useContextAccesses shared data-table state such as viewMode, selection, and scroll helpers.

Toolbar

PartPurpose
DataTable.ToolbarToolbar row above the content view.
DataTable.Toolbar.LeftLeft-aligned controls such as search, filters, sorting, and saved views.
DataTable.Toolbar.RightRight-aligned controls such as result counts and primary actions.
DataTable.SearchSearch input for table-local or server-driven search.
DataTable.FiltersFilter builder for structured available/applied filters.
DataTable.SortingSorting selector for available/applied sort options.
DataTable.ColumnsModifierColumn visibility and column-order management UI.
DataTable.CheckboxFiltersChip-style filter controls for simpler filtered list pages.
DataTable.ViewToggleSwitches between supported view modes such as table and grid.
DataTable.GridDensityToggleGrid column-density control for grid views.
DataTable.ResultsCountDisplays result totals using the current item naming.
DataTable.PrimaryActionPrimary create/add action.
DataTable.SecondaryActionSecondary toolbar action.
DataTable.MenuOverflow actions rendered from DataTableAction[].
DataTable.SavedViewsSaved 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

PartPurpose
DataTable.ContentDispatches to the correct view implementation based on view.
DataTable.TableViewLower-level table renderer.
DataTable.GridViewLower-level grid renderer.
DataTable.MapViewLower-level map renderer.
DataTable.PanelSupporting panel component for content-level detail layouts.
DataTable.ModifiersSelection modifier bar for bulk actions.
DataTable.GroupGrouped row/list rendering for advanced grouped content.

Empty And Error States

DataTable.Content owns both collection emptiness and collection failure handling.

Use:

  • emptyState when the dataset is genuinely empty
  • error when the data request failed
  • errorState to override the shared ErrorGuard copy 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:

PartPurpose
DataTableGridItemRoot clickable grid card container.
DataTableGridItem.HeroVisual top section with icon or background image.
DataTableGridItem.Hero.BadgeBadge slot inside the hero area.
DataTableGridItem.BodyMain content region.
DataTableGridItem.Body.TopTop content stack inside the body.
DataTableGridItem.Body.BottomBottom metadata/detail stack inside the body.
DataTableGridItem.TitlePrimary grid item title.
DataTableGridItem.SubTitleSecondary title text.
DataTableGridItem.DescriptionLonger supporting description text.
DataTableGridItem.DetailLabel/value metadata row.
DataTableGridItem.HighlightHighlighted inline content block.
DataTableGridItem.MenuOverflow actions for the grid item.
DataTableGridItem.Menu.ItemMenu action item.
DataTableGridItem.Menu.DividerDivider inside the item menu.

DataTable.Root Props

PropTypeDefaultDescription
itemsT[]Dataset for the current view.
itemNamestring | { singular: string; plural: string }'item'Naming used by result counts and selection UI.
isLoadingbooleanInitial loading state.
isFetchingbooleanBackground refetch state.
isFetchingNextPagebooleanInfinite-scroll pagination fetch state.
hasNextPagebooleanWhether infinite-scroll can continue.
fetchNextPage() => voidInfinite-scroll loader callback.
totalResultsCountnumberServer-driven total count.
selectionMode'single' | 'multiple''multiple'Selection behavior.
checkedIds / setCheckedIdscontrolled selection pairControlled selection state.
isTopLevelChecked / setIsTopLevelCheckedcontrolled top-level selection pairControlled select-all state.
disabledIdentifiersSet<string>Prevents certain items from being selected.
viewModes[DataTableViewMode, ...DataTableViewMode[]]['table']Supported views.
initialViewModeDataTableViewMode'table'Default view mode.
localStorageKeystringPersists view mode and column preferences.
enableColumnHidingbooleanfalseEnables column preference UI and persistence.
columnsDataTableColumn<T>[]Optional columns passed into context for column state helpers.
classNamestringAdditional 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 emptyState for 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.

HelperPurpose
createEntityColumnAvatar/icon + strong primary entity label column.
createEntityReferenceColumnSingle related-entity badge column, optionally clickable.
createEntityMultiReferenceColumnMultiple related-entity badges with overflow summary.
createTextColumnStandard text/truncation column.
createDateColumnStandard formatted date column.
createMoneyColumnStandard money-formatting column.
createBadgeColumnFixed-color badge column.
createDynamicBadgeColumnBadge column with per-row icon/color mapping.
formatGenericTableValueShared 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.Root
  • Search
  • Filters
  • Sorting
  • ColumnsModifier
  • SavedViews
  • ResultsCount
  • Menu or PrimaryAction
  • Content 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']}
  • ViewToggle
  • GridDensityToggle
  • Content switches between table and grid
  • DataTableGridItem often 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:

  • onRowClick opens a Panel
  • activeItem highlights the selected item
  • some views use Content's panel support, but many app screens wire directly into PanelStack

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:

PatternPathNotes
Full list-page stackfsai/apps/brand-dashboard/src/pages/Leads/Leads.components.tsxBest example of the full composable table pattern with search, filters, sorting, columns, saved views, modifiers, and row-to-panel behavior.
Another power-user tablefsai/apps/brand-dashboard/src/pages/Marketing/MarketingTable.tsxSimilar operational list pattern with server-driven totals and saved preferences.
Table/grid switchingfsai/apps/brand-dashboard/src/pages/Learning/components/CoursesTab.tsxBest current example of one Root supporting both table and grid views.
Single toolbar actionfsai/apps/brand-dashboard/src/pages/Learning/components/CoursesTab.tsxShows the correct direct DataTable.PrimaryAction pattern when there is only one main action.
Multi-action toolbar menufsai/apps/brand-dashboard/src/modules/locations/components/LocationsTable/LocationsTable.tsxStrong example of using DataTable.Menu when the table has multiple top-level actions, with creation first.
Grid-oriented contentfsai/apps/brand-dashboard/src/pages/Learning/components/CourseGridItem.tsxBest example of DataTableGridItem used as the standard card shell for grid mode.
Simple grid cardfsai/apps/brand-dashboard/src/pages/StudioQrTemplates/QrTemplateGridItem.tsxShows a leaner DataTableGridItem composition without the full hero/menu/detail stack.
View-mode context usagefsai/apps/brand-dashboard/src/pages/AdminPanel/Helpdesk/ArticlesTable.tsxGood example of using useDataTableContext for custom view controls.
Grouped datafsai/apps/brand-dashboard/src/pages/Projects/ProjectDetail/PhaseView/PhaseView.primitives.tsxStrong example of DataTable.Group and grouped task display.
Vendor/location tablesfsai/apps/brand-dashboard/src/modules/vendors/components/VendorsTable/VendorsTable.tsxShows how the pattern scales across feature modules.
Location-scoped tablefsai/apps/brand-dashboard/src/modules/vendors/components/VendorLocationsTable/VendorLocationsTable.tsxGood 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 on DataTable.Content.
  • Put toolbar controls in DataTable.Toolbar, not inside the table body.
  • Put emptyState, modifiers, searchValue, and appliedFiltersCount on DataTable.Content.
  • Use localStorageKey whenever view mode or column preferences should persist.
  • Use enableColumnHiding when you expose ColumnsModifier.
  • Use DataTable.PrimaryAction only when there is one main toolbar action.
  • Use DataTable.Menu when there are multiple toolbar actions, and make the first item creation-oriented.
  • Use DataTableGridItem as 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.ResultsCount often mirrors the server total, while totalResultsCount on Root feeds shared data-table state.
  • DataTable.Content supports a built-in panel prop, but many dashboard screens still use app-owned PanelStack patterns directly.

Guidelines

  • Do treat DataTable as a composable layout system, not just a table component
  • Do use DataTable.Root + toolbar + DataTable.Content as 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.Menu when there is more than one top-level action, with creation first
  • Do use DataTableGridItem for standard card-like grid items
  • Do use viewModes only 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

On this page