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
| Name | Role | Status |
|---|---|---|
| Ava Chen | Engineer | Active |
| Liam Park | Designer | Pending |
| Noah Reyes | Product | Invited |
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
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 containertable-action-slot-- bar above table: filter slot left, action buttons righttable-filter-bar-- active filter chip row, shown when filters are appliedtable-filter-button-- opens advanced filter paneltable-filter-tag-- individual applied filter with remove buttontable-container-- bordered table surfacetable-header-- sticky column headers with optional sort and resize handlestable-column-fixed-- sticky left column(s) with shadow edge indicatortable-row/table-cell-- data rows; three height variants (compact / standard / comfortable)table-row-action-cell-- fixed right column with per-row action iconstable-pagination-- below-table page navigation and page size controltable-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_horizoverflow 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 addaria-sort="ascending"/"descending"/"none". - Row selection:
aria-selected="true"on selected rows. Checkbox column hasaria-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.