Zetta v1.6

Components · Data display & feedback

Table

A bordered data table with a muted, letter-spaced header, hairline row dividers, a hover state, and a primary-tinted selected row. Composes cleanly with badges in cells.

Preview

NameRoleStatus
Ava ChenEngineerActive
Liam ParkDesignerPending
Noah ReyesProductInvited

Build with the skill

No package to install — hand this to your AI to generate or migrate the component in your stack.

Prompt for your AI
Apply the Zetta "Table" component to this project using the zetta-design-md skill.

WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `table` spec (variants: header, row, row-hover, row-selected) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `table` spec in this project's stack.

AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.

ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility.

Anatomy & rules

┌- table-wrapper --------------------------------------------------------┐
│  ┌- table-action-slot ---------------------------------------------┐   │
│  │  [left: filter-bar + filter-button]   [right: action-buttons]   │   │
│  └------------------------------------------------------------------┘   │
│  ┌- table-filter-bar ----------------------------------------------┐   │
│  │  [filter-tag] [filter-tag] [filter-tag] ...                      │   │
│  └------------------------------------------------------------------┘   │
│  ┌- table-container -----------------------------------------------┐   │
│  │  ┌- table-header ---------------------------------------------┐ │   │
│  │  │  [checkbox] [col A ▲] [col B] ... [fixed col] [actions]    │ │   │
│  │  └------------------------------------------------------------┘ │   │
│  │  ┌- table-row ------------------------------------------------┐ │   │
│  │  │  [checkbox] [cell] [cell] ...  [fixed-cell] [action-icons]  │ │   │
│  │  └------------------------------------------------------------┘ │   │
│  │  (repeat rows)                                                   │   │
│  └------------------------------------------------------------------┘   │
│  ┌- table-pagination ----------------------------------------------┐   │
│  │  [1–25 of 340]                    [< 1 2 3 ... 14 >] [25/page]  │   │
│  └------------------------------------------------------------------┘   │
└------------------------------------------------------------------------┘

Parts:

  • table-wrapper -- outer flex column container
  • table-action-slot -- bar above table: filter slot left, action buttons right
  • table-filter-bar -- active filter chip row, shown when filters are applied
  • table-filter-button -- opens advanced filter panel
  • table-filter-tag -- individual applied filter with remove button
  • table-container -- bordered table surface
  • table-header -- sticky column headers with optional sort and resize handles
  • table-column-fixed -- sticky left column(s) with shadow edge indicator
  • table-row / table-cell -- data rows; three height variants (compact / standard / comfortable)
  • table-row-action-cell -- fixed right column with per-row action icons
  • table-pagination -- below-table page navigation and page size control
  • table-preferences-button -- column visibility and sort preference panel trigger
State Visual
Default Rows alternate or flat, header with sort icons
Row hover {colors.surface-muted} fill on hovered row
Row selected {colors.primary-bg} fill, 2px solid {colors.primary} left border
Row focus (keyboard) 2px solid {colors.border-focus} outline on row
Column sorted asc arrow_upward icon in {colors.primary}, header text in {colors.primary}
Column sorted desc arrow_downward icon in {colors.primary}, header text in {colors.primary}
Loading Skeleton rows replace data rows, header remains
Empty Empty state fills the table body area
Error Error message with retry fills the table body area

When to use

  • Displaying structured datasets where comparison across rows matters.
  • When users need to sort, filter, or act on individual rows.
  • When data has 3+ columns and a fixed header improves orientation during scroll.

When not to use

  • Do not use a table for a simple list of items with one or two attributes -- use a card list or list component instead.
  • Do not use a table inside a card that is itself inside a table row -- no nested tables.
  • Do not use a table for navigation between pages -- use a list or sidebar nav.

Do

  • Define column types upfront -- text, number, date, status, action. Apply the correct alignment: text left, numbers right, status centered.
  • Always define an empty state for zero-row scenarios.
  • Use Compact density when two or more tables appear in the same view.
  • Keep the action column (table-row-action-cell) as the rightmost column, right-aligned.
  • Show a maximum of 3 row action icons before collapsing to a more_horiz overflow menu.
  • Truncate long text with ellipsis -- never wrap cell text in Standard or Compact density.

Don't

  • Don't show more than 3 simple filters in the filter slot -- move to advanced filter panel if more are needed.
  • Don't put primary actions inside the table -- use the action slot above the table.
  • Don't resize the table container to less than 320px -- horizontal scroll is the correct response to narrow viewports.
  • Don't use Comfortable density for tables with more than 8 columns.
  • Don't fix more than 2 columns on the left.
  • Column headers: Short nouns or noun phrases. Title case. No verbs. Maximum 2 words.
  • Action labels: Verb only when in an overflow menu (e.g. "Edit", "Delete"). Icon-only with tooltip for inline action icons.
  • Status labels: Use Lozenge component for status text values in cells. Use status icons for compact status indicators.
  • Numbers: Right-align numeric columns. Use locale-aware number formatting.
  • Dates: Use consistent format across all date columns within a table. Prefer relative dates ("2 days ago") for recency-focused data.
  • Empty cells: Show a dash ("--") for null or missing values. Never leave a cell visually empty.

Keyboard

Key Behavior
Tab Move focus into the table, then through interactive cells
Up / Down Arrow Move focus between rows (when row interaction is enabled)
Space Select / deselect the focused row
Enter Activate the focused row's primary action
Shift + Space Extend row selection to the focused row
Ctrl + A Select all rows
Enter / Space Activate sort on a focused sortable header; activate a focused action icon
Left / Right Arrow Move focus between cells in the same row (grid navigation pattern)
Escape Close any open row action dropdown or filter panel

Accessibility

  • ARIA role: role="grid" on the table element when rows are selectable or keyboard-navigable between cells. role="table" for display-only tables.
  • Column headers: scope="col" on <th> elements. Sortable headers add aria-sort="ascending" / "descending" / "none".
  • Row selection: aria-selected="true" on selected rows. Checkbox column has aria-label="Select row".
  • Pagination: Wrap in <nav aria-label="Table pagination">. Current page button: aria-current="page".
  • Screen reader: Sort state changes are announced via aria-live="polite" region. Filter changes announce updated row count.
  • Contrast: Header text ({colors.muted}) achieves 4.5:1 against {colors.surface-table-header}. Row text ({colors.ink}) achieves 4.5:1 against white surface.

Spec

Values and token references resolved straight from the Zetta spec.

table-wrapper
display
flex
flexDirection
column
gap
{spacing.sm}
table-container
backgroundColor
{colors.surface-card}
borderColor
{colors.hairline}
borderWidth
1px
borderRadius
{rounded.base}
overflow
hidden
table-action-slot
display
flex
alignItems
center
justifyContent
space-between
gap
{spacing.sm}
paddingBottom
{spacing.sm}
table-action-slot-left
display
flex
alignItems
center
gap
{spacing.sm}
table-action-slot-right
display
flex
alignItems
center
gap
{spacing.sm}
table-filter-bar
display
flex
alignItems
center
flexWrap
wrap
gap
{spacing.sm}
paddingBottom
{spacing.sm}
table-filter-button
height
32px
paddingX
{spacing.md}
borderRadius
{rounded.base}
backgroundColor
transparent
borderColor
{colors.border-strong}
borderWidth
1px
textColor
{colors.ink}
fontFamily
Geist
fontSize
14px
fontWeight
500
iconSize
{icons.sizeMap.md}
iconColor
{colors.muted}
gap
{spacing.xs}
cursor
pointer
table-filter-button-active
backgroundColor
{colors.primary-bg}
borderColor
{colors.primary}
textColor
{colors.ink}
table-filter-button-hover
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
table-filter-button-focus
outline
2px solid {colors.border-focus}
outlineOffset
2px
table-filter-tag
display
inline-flex
alignItems
center
gap
{spacing.xs}
height
28px
paddingX
{spacing.sm}
borderRadius
{rounded.sm}
backgroundColor
{colors.surface-secondary}
borderColor
{colors.hairline}
borderWidth
1px
textColor
{colors.ink}
fontFamily
Geist
fontSize
13px
fontWeight
400
removeIconSize
{icons.sizeMap.sm}
removeIconColor
{colors.muted}
table-filter-tag-remove
cursor
pointer
table-filter-tag-remove-hover
removeIconColor
{colors.ink}
table-header
backgroundColor
{colors.surface-table-header}
textColor
{colors.muted}
fontFamily
Geist
fontSize
13px
fontWeight
500
letterSpacing
0.05em
textAlign
left
borderBottomColor
{colors.border-strong}
borderBottomWidth
2px
paddingX
{spacing.md}
paddingY
{spacing.sm}
table-header-sortable
cursor
pointer
userSelect
none
display
flex
alignItems
center
gap
{spacing.xs}
table-header-sortable-hover
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
table-header-sortable-focus
outline
2px solid {colors.border-focus}
outlineOffset
-2px
table-sort-icon
size
{icons.sizeMap.md}
colorInactive
{colors.muted}
colorActive
{colors.primary}
table-column-fixed
position
sticky
left
0
zIndex
1
backgroundColor
{colors.surface-card}
borderRightColor
{colors.border-strong}
borderRightWidth
1px
boxShadow
2px 0 4px rgba(0,0,0,0.06)
table-column-fixed-header
position
sticky
left
0
zIndex
2
backgroundColor
{colors.surface-table-header}
borderRightColor
{colors.border-strong}
borderRightWidth
1px
table-column-resizer
width
4px
backgroundColor
transparent
cursor
col-resize
position
absolute
right
0
top
0
bottom
0
table-column-resizing
backgroundColor
{colors.primary}
width
2px
table-row
backgroundColor
transparent
textColor
{colors.ink}
fontFamily
Geist
fontSize
14px
fontWeight
400
lineHeight
1.4
borderBottomColor
{colors.hairline}
borderBottomWidth
1px
table-row-hover
backgroundColor
{colors.primary-bg}
table-row-selected
backgroundColor
{colors.primary-bg}
table-row-selected-focus
outline
2px solid {colors.border-focus}
outlineOffset
-2px
table-cell
paddingX
{spacing.md}
paddingY
{spacing.sm}
verticalAlign
middle
overflow
hidden
textOverflow
ellipsis
whiteSpace
nowrap
table-cell-compact
paddingX
{spacing.md}
paddingY
{spacing.xs}
verticalAlign
middle
overflow
hidden
textOverflow
ellipsis
whiteSpace
nowrap
table-cell-comfortable
paddingX
{spacing.md}
paddingY
{spacing.md}
verticalAlign
middle
table-icon-action
size
{icons.sizeMap.md}
color
{colors.muted}
table-icon-action-hover
color
{colors.ink}
table-icon-status
size
{icons.sizeMap.md}
table-icon-type
size
{icons.sizeMap.md}
color
{colors.muted}
table-row-action-cell
width
auto
minWidth
88px
display
flex
alignItems
center
gap
{spacing.xs}
justifyContent
flex-end
paddingX
{spacing.sm}
table-pagination
display
flex
alignItems
center
justifyContent
space-between
paddingTop
{spacing.sm}
gap
{spacing.sm}
table-pagination-info
textColor
{colors.muted}
fontFamily
Geist
fontSize
13px
fontWeight
400
table-pagination-controls
display
flex
alignItems
center
gap
{spacing.xs}
table-pagination-button
width
32px
height
32px
borderRadius
{rounded.base}
backgroundColor
transparent
borderColor
transparent
borderWidth
1px
textColor
{colors.ink}
fontFamily
Geist
fontSize
14px
fontWeight
400
cursor
pointer
table-pagination-button-hover
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
table-pagination-button-active-page
backgroundColor
{colors.primary}
textColor
{colors.primary-text}
fontWeight
500
table-pagination-button-disabled
textColor
{colors.disabled-text}
cursor
not-allowed
table-pagination-button-focus
outline
2px solid {colors.border-focus}
outlineOffset
2px
table-page-size-select
height
32px
paddingX
{spacing.sm}
borderRadius
{rounded.base}
borderColor
{colors.border-strong}
borderWidth
1px
textColor
{colors.ink}
fontFamily
Geist
fontSize
13px
table-preferences-button
iconSize
{icons.sizeMap.md}
iconColor
{colors.muted}
table-preferences-panel
minWidth
240px
padding
{spacing.md}
borderRadius
{rounded.base}
table-row-selection-checkbox
width
16px
height
16px
borderRadius
{rounded.sm}
borderColor
{colors.border-strong}
borderWidth
2px
backgroundColor
transparent
cursor
pointer
paddingX
{spacing.md}
table-row-selection-checkbox-checked
backgroundColor
{colors.primary}
borderColor
{colors.primary}
iconColor
{colors.primary-text}
table-select-all-checkbox
indeterminateIconColor
{colors.primary}
table-bulk-action-bar
backgroundColor
{colors.primary-bg}
borderColor
{colors.primary}
borderWidth
1px
borderRadius
{rounded.base}
paddingX
{spacing.md}
paddingY
{spacing.sm}
display
flex
alignItems
center
gap
{spacing.sm}
fontFamily
Geist
fontSize
14px
fontWeight
500
textColor
{colors.primary}
table-bulk-action-count
fontWeight
700
table-column-config-trigger
iconSize
{icons.sizeMap.md}
iconColor
{colors.muted}
borderRadius
{rounded.base}
padding
{spacing.xs}
cursor
pointer
table-column-config-panel
backgroundColor
{colors.popover}
borderColor
{colors.hairline}
borderWidth
1px
borderRadius
{rounded.base}
shadow
{elevation.shadow.overlay}
padding
{spacing.md}
minWidth
220px
zIndex
{elevation.z-index.popup}
table-saved-views-bar
display
flex
alignItems
center
gap
{spacing.sm}
paddingBottom
{spacing.sm}
table-saved-view-chip
height
28px
paddingX
{spacing.sm}
borderRadius
{rounded.full}
backgroundColor
{colors.surface-secondary}
textColor
{colors.body}
fontFamily
Geist
fontSize
13px
fontWeight
500
cursor
pointer
borderColor
{colors.hairline}
borderWidth
1px
table-saved-view-chip-active
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
borderColor
{colors.primary}
table-column-sticky
position
sticky
left
0
zIndex
2
backgroundColor
{colors.surface-card}
borderRightColor
{colors.border-strong}
borderRightWidth
1px
table-header-sticky
position
sticky
top
0
zIndex
3
backgroundColor
{colors.surface-table-header}
table-mobile-card
backgroundColor
{colors.surface-card}
borderColor
{colors.hairline}
borderWidth
1px
borderRadius
{rounded.base}
padding
{spacing.md}
marginBottom
{spacing.sm}
display
flex
flexDirection
column
gap
{spacing.xs}
table-mobile-card-label
fontFamily
Geist
fontSize
11px
fontWeight
500
textColor
{colors.muted}
textTransform
uppercase
letterSpacing
0.05em
table-mobile-card-value
fontFamily
Geist
fontSize
14px
fontWeight
400
textColor
{colors.ink}

Known gaps

  • Virtualized table scroll performance spec — implementation concern; no token gaps remain.
  • Column reorder drag-and-drop animation — not yet specified with motion tokens.