Data Table
Purpose
A Data Table is the primary pattern for displaying, sorting, filtering, and acting on collections of structured records in Force UI. Use it for list pages showing datasets, engines, VDBs, users, jobs, policies, or any other entity with multiple properties. The Data Table is always the full-width, vertically-filling primary content of a list page — it is not used for small embedded comparisons or summary data (use a plain HTML table or a MetadataList for those).
Do NOT use a Data Table for: displaying a small set of key-value pairs about a single entity (use MetadataList), showing hierarchical tree structures (use a Tree component), or displaying read-only tabular data in a card on a detail page (use a plain semantic table).
Anatomy
The Data Table is composed of three main zones:
Table container:
- A bordered, overflow-hidden wrapper that visually groups the table. Border is
--force-color-border-default, border radius is--force-radius-card(8px). No shadow — cards and containers use borders only. Horizontally scrollable when column count exceeds viewport width.
Table header (thead):
- A single sticky header row. Background is
--force-color-bg-muted(neutral-50). Each header cell contains:- Column label: uppercase, 12px, medium weight,
--force-color-text-tertiary. Column labels are the display name of the column. - Sort indicator (optional): a sort icon (chevron up / chevron down) that appears on sortable columns. Active sort direction is shown; inactive sortable columns may show a neutral double-chevron icon on hover.
- Column resize handle (optional): a draggable edge between header cells for resizable columns.
- Column label: uppercase, 12px, medium weight,
- The first column may contain a select-all Checkbox (indeterminate when some rows are selected, checked when all are selected).
Table body (tbody):
- A series of rows, each representing one record. Row height is 40px by default (compact: 32px, comfortable: 48px).
- Each row contains:
- Row selection Checkbox (optional, leftmost cell): for multi-select workflows.
- Data cells: content cells corresponding to the header columns. Text in
--force-color-text-primaryat 14px, regular weight. Cell padding is 8px horizontal. - Actions cell (required in list page tables): pinned to the right edge. Contains ghost button(s) for row-level actions. See the Actions Column specification below.
- Rows have a bottom border in
--force-color-border-defaultseparating each row. The last row has no bottom border (it is contained by the table container's border).
Table footer / pagination (outside the table container):
- A row below the table container showing pagination controls: page size selector (Select), current range indicator ("1–25 of 312"), and previous/next page buttons. This row is part of the page layout, not inside the table container itself.
Variants
The Data Table does not have named visual variants. Configuration varies along these axes:
Row density
- Compact (32px row height): use in admin or developer screens where information density is the priority.
- Default (40px row height): the standard for all list pages.
- Comfortable (48px row height): use when rows contain multi-line content, status badges, or when the table is the primary visual element on an overview dashboard.
Selection mode
- None: no checkboxes, no row selection. Use for read-only reference tables.
- Single-select: one row can be selected at a time (radio-button behavior). Use when the selected row drives another panel's content (master-detail layout).
- Multi-select: one or more rows can be selected. A select-all checkbox appears in the header. Use for bulk-action workflows (bulk delete, bulk export, bulk status change).
Sortable columns
Individual columns are marked as sortable. Clicking the header of a sortable column changes the sort order. Only one column is the active sort column at a time; the sort direction (ascending/descending) is tracked per column. Clicking an already-sorted column's header toggles the direction.
Column pinning
Columns can be pinned to the left or right edge. The Actions column is always pinned to the right. A "Name" or primary identifier column should be pinned to the left when the table has many columns and horizontal scrolling is expected.
States
Default row: Background is --force-color-bg-surface. Text is --force-color-text-primary. Bottom border is --force-color-border-default.
Hover row: Background transitions to --force-color-bg-interactive-hover (neutral-50). Transition at --force-duration-fast (150ms). The hover state is applied to the entire row.
Selected row (checked): Background is --force-color-bg-interactive-active (indigo-50). Text remains --force-color-text-primary for body text, but the row-level accent is set by the active state background. All cells in the row share this background.
Focused cell (keyboard navigation): A focus ring appears on the focused cell. Border is --force-color-border-focus. Focus ring is --force-shadow-focus-ring within the cell. When grid navigation is enabled, the focused cell receives a clear visible indicator.
Sorted column: The sorted column's header label has a visible sort icon indicating direction. The header cell background may be slightly emphasized with --force-color-bg-emphasis to reinforce which column is active. Column data cells in the sorted column are not visually distinct.
Empty state: When the table has no rows (no data, or all rows filtered out), the table container shows an empty state centered in the body area. The empty state consists of: an icon, a headline ("No datasets found"), and a description with the reason (e.g., "Try adjusting your filters"). If the empty state is due to no data existing at all, include a primary CTA ("Create your first dataset"). Do NOT collapse the table container entirely — keep it visible to maintain layout stability.
Loading state: When data is being fetched, show Skeleton rows (3–5 rows of placeholder shimmer) inside the table body. The header remains visible. Do NOT show a full-page spinner — inline skeleton rows preserve the table's spatial context.
Error state: When data fetching has failed, show an error callout inside the table body area: error icon, "Failed to load data" message, and a "Retry" button (tertiary variant). Do NOT collapse the table.
Behavior
- Sorting: Clicking a sortable column header sorts the table by that column. First click: ascending. Second click: descending. Third click (optional): removes sort and returns to default order. Only one column is sorted at a time. The sort state persists within the page session.
- Row selection: Clicking a row checkbox selects/deselects that row. Clicking the header checkbox selects all rows on the current page (not all pages). When rows are selected, a bulk action bar or contextual action area becomes visible above the table.
- Row click navigation: In list pages, clicking a row (anywhere other than the checkbox) navigates to the detail page for that record. This is the primary navigation action — the "View" ghost button in the actions column is a secondary affordance. If row-click navigation is enabled, the row cursor is
pointeron hover. - Pagination: The table loads one page of data at a time. Page size options are typically 25, 50, and 100. Changing the page size resets to page 1. Loading the next page shows a brief skeleton state in the rows while the new page loads.
- Column resizing: Drag the resize handle in the column header to adjust column widths. Minimum column width is 80px.
- Horizontal scroll: When columns exceed the available width, the table body scrolls horizontally while the page header and filter bar remain fixed. Pinned columns (left: identifier, right: actions) remain visible during scroll.
- Keyboard navigation: The table supports keyboard navigation. Tab enters the table. Arrow keys navigate between cells when grid navigation is active. Enter activates the focused cell's primary action (row navigation or cell action button). Space toggles a focused checkbox.
Actions Column
The Actions column MUST be present in all list page Data Tables. It provides row-level controls without competing with cell content in the main columns.
- Header label: "Actions"
- Width: 100px fixed
- Pinned to the right edge
- Not sortable, not filterable, not resizable
- Cell content: one or two ghost (
transparent / default) Buttons of sizesm- Primary action: "View" with a trailing
chevron_righticon — navigates to the entity's detail page - Secondary action (optional): "Edit," "Clone," or context-specific, shown as an icon-only ghost button (e.g., a three-dot "More" button opening a Dropdown Menu for additional actions)
- Primary action: "View" with a trailing
- Do NOT use more than two visible action buttons in the Actions column. If more actions exist, collapse them behind a "More" dropdown.
Accessibility
- The table element MUST be a semantic
<table>(not a div-based grid) unless using a virtualized large dataset where a fullrole="grid"implementation is required. Semantic tables give screen readers the ability to announce row and column counts and navigate between cells. - Column header cells MUST use
<th scope="col">. If row headers are used, use<th scope="row">. - Sortable column headers MUST have
aria-sortattribute:aria-sort="ascending",aria-sort="descending", oraria-sort="none"(for unsorted sortable columns). Non-sortable columns omitaria-sort. - The table container MUST have a caption or
aria-labelthat names the table (e.g., "Datasets" or "Search results for 'production'"). This is announced by screen readers as the table heading. - Row checkboxes MUST have
aria-label="Select row for [entity name]"— never a generic "Select" label. The header checkbox must havearia-label="Select all rows on this page". - Keyboard: the table should support full keyboard navigation. Users should be able to reach all interactive elements (sort headers, checkboxes, row action buttons) via Tab and Arrow keys.
- When the table is loading, add
aria-busy="true"to the table container. When data has loaded, remove it. - When the table is empty, the empty state content should be announced. Include
role="status"oraria-live="polite"on the container that changes between loaded content and the empty state.
Composition
- A Data Table on a list page is always preceded by a PageHeader (title, description, primary action button) and a ControlsBar (search Input, filter Selects, and optional bulk action buttons).
- The ControlsBar's search Input drives live filtering of the table. The Input uses
type="search"with a leading search icon. - When rows are selected in multi-select mode, a bulk action bar appears between the ControlsBar and the table. It shows the count of selected rows and bulk action Buttons ("Delete selected", "Export selected").
- The Pagination row appears below the table container, outside the border.
- A Data Table should NOT be placed inside a Card component — the table container itself provides the bordered structural frame.
- Do NOT nest a Data Table inside a Dialog. If a dialog needs to show a list of items for selection, use a simple unordered list with clickable rows or a Combobox.
Guidance
Table design decisions:
- Determine the most important three to five columns for the default view. Users can add/show/hide additional columns via a column visibility control (a Dropdown Menu with checkboxes) if column count exceeds the default viewport width.
- The primary identifier column (name, title, ID) should be first and pinned to the left if horizontal scrolling is possible.
- Status columns should use Badges, not text, for scannable status values. Use the appropriate semantic variant (success, warning, error, info) to match the status meaning.
- Date columns should display in a consistent, human-readable format ("Jan 15, 2026" or relative "3 days ago"). Avoid ambiguous formats like "01/15/26."
Common mistakes to avoid:
- Do NOT apply a shadow to the table container — it uses borders only.
- Do NOT put all possible actions as visible buttons in the Actions column. Limit to one or two; hide the rest in a Dropdown Menu.
- Do NOT use a Data Table for fewer than three rows — an empty or near-empty table on a detail page looks unfinished. Use a MetadataList or a descriptive empty state.
- Do NOT load all records at once without pagination. Implement server-side pagination for collections with more than 100 records.
- Do NOT use colored text in data cells to convey status — use Badges instead so that color is supplemented by text.
Token Usage
Table container
- Border:
1px solid --force-color-border-default - Border radius:
--force-radius-card(8px) - Overflow: hidden
- Shadow: none
Header row (thead tr)
- Background:
--force-color-bg-muted - Bottom border:
1px solid --force-color-border-default
Header cell text
- Color:
--force-color-text-tertiary - Font size:
--force-font-size-xs(12px) - Font weight:
--force-font-weight-medium(500) - Text transform: uppercase
- Letter spacing:
--force-font-letter-spacing-wide(0.025em)
Body row (default)
- Background:
--force-color-bg-surface - Bottom border:
1px solid --force-color-border-default
Body row (hover)
- Background:
--force-color-bg-interactive-hover
Body row (selected)
- Background:
--force-color-bg-interactive-active
Body cell text
- Color:
--force-color-text-primary - Font size:
--force-font-size-sm(14px) - Font weight:
--force-font-weight-regular(400) - Cell padding:
--force-spacing-2(8px) horizontal
Row Actions column buttons
- Variant:
transparent / defaultButton,smsize - Text:
--force-color-text-primary - Icon: 16px,
--force-color-text-primary
Focus ring (keyboard navigation)
- Border:
--force-color-border-focus - Focus ring:
--force-shadow-focus-ring
Transition (hover)
- Duration:
--force-duration-fast(150ms) - Easing:
--force-easing-standard
Pagination area
- Positioned below the table container, outside the border
- Text:
--force-color-text-secondary,--force-font-size-sm - Buttons:
secondary / defaultButton,smsize for prev/next - Page size Select:
smsize