Table
Data table with composable subcomponents. Sorting, selection, striped rows, size variants.
Table is a composable data-display primitive. The container plus seven subcomponents (TableHeader, TableBody, TableRow, TableHead, TableCell, TableActionsCell, TableSubheader) handle the structural HTML; the consumer composes cells with whatever content the row needs — text, badges, links, action buttons.
Use it for
- Tabular records with consistent columns (users, projects, invoices)
- Sortable lists where the user picks a column to order by
- Comparison tables on settings or pricing pages
- Any "rows of structured data" where columns share semantics across rows
For card-like rows where each entry is its own self-contained block, use CardList. For settings panels with action-per-row, use CardControl.
Import
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
TableActionsCell,
} from '@brikdesigns/bds';Variants
Sizes
<Table size="default">...</Table> {/* compact, dense data */}
<Table size="comfortable">...</Table> {/* generous padding, 72px cell height */}Striped
Alternating row backgrounds for readability on dense tables.
<Table striped>...</Table>Flush
flush removes the left padding on the first cell and right padding on the last — use when the table aligns to a container's edge with no internal padding.
<Table flush>...</Table>Sortable headers
TableHead accepts sortable and sortDirection. The consumer owns the click handler and sort state.
<TableHead sortable sortDirection="asc" onClick={() => sortBy('name')}>
Name
</TableHead>Selected rows
TableRow accepts a selected boolean for highlight styling.
<TableRow selected>
<TableCell>Active row</TableCell>
</TableRow>Cell-level interactivity (read/edit canon)
Tables host three classes of click target — no others. This is the
canon locked in portal#837 — Read and edit conventions.
Don't reach for raw <a> or <button> — use the BDS components below.
| Cell class | Affordance | Pattern |
|---|---|---|
| Identifier (Name) | <TextLink size="small"> opening the read sheet for that row (or navigating to the read page when the table has no sheet, e.g. customer_stories, blog_posts) | Click target |
| Foreign-key reference | <TextLink size="small"> opening the read sheet of the referenced entity (Service Line in a Services row, Service in an Offerings row, Company in admin rows) | Click target |
| Status / Public / Featured (read-only) | <Badge> or plain text | Not interactive |
| Actions | <TableActionsCell> with [View][Edit][⋯] icon buttons in size="sm" | Click targets |
Forbidden — whole-row click
Never bind onClick to <TableRow>. It violates table cell semantics,
breaks screen-reader expectations, and conflicts with cell-level
affordances. Move the handler down to the appropriate cell — Name and
FK cells become <TextLink>s; trailing actions live in
<TableActionsCell>.
TableActionsCell
The right-aligned trailing actions column. Owns three things — and
nothing else: alignment (canonical right, optional center for
symmetric tables), shrink-to-content width, and the --gap-sm rhythm
between buttons. Carries aria-label="Actions" automatically.
<TableActionsCell>
<Button variant="primary" size="sm" icon={<EyeIcon />} label="View" onClick={openSheet} />
<Button variant="primary" size="sm" icon={<PenIcon />} label="Edit" onClick={navigateEdit} />
{/* Optional overflow for tertiary actions (Duplicate, Archive, Delete) */}
<Button variant="ghost" size="sm" icon={<EllipsisIcon />} label="More" onClick={openMenu} />
</TableActionsCell>Common icon-button variants:
| Variant | Use |
|---|---|
primary | Primary action (view, edit, open) |
secondary | Neutral (download, copy) |
ghost | Low emphasis (overflow menu) |
destructive | Destructive (delete, remove) — use sparingly in-row; prefer the overflow menu |
Text links (Name + FK cells)
Use <TextLink size="small"> for the Name cell and any FK cell. Never use raw <a> or full-size text links.
{/* Name cell — opens the row's read sheet */}
<TextLink size="small" onClick={() => openSheet(service.id)}>
{service.name}
</TextLink>
{/* FK cell — opens the related entity's read sheet */}
<TextLink size="small" onClick={() => openSheet(service.serviceLineId, 'service-line')}>
{service.serviceLine}
</TextLink>Text link vs button:
- TextLink — navigation that takes you somewhere (open read sheet, navigate to profile)
- Button — actions that change state (Approve, Assign, Archive)
Tooltip indicators
Wrap info icons in a Tooltip so they reveal context on hover/focus. Use cursor: help to signal interactivity.
<Tooltip content="Primary contact email" placement="top">
<span style={{ cursor: 'help' }}>
<Icon icon="ph:info" />
</span>
</Tooltip>Icon-left cells
Pair a 24px icon with text using gap: var(--gap-xs).
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--gap-xs)' }}>
<Icon icon="ph:palette" />
Design
</span>When not to use
- Don't use Table for self-contained card grids. Cards belong in CardList.
- Don't use Table for settings rows. Use CardControl — locked layout for badge + title + description + action.
- Don't use Table for narrow mobile views. Tables don't gracefully wrap; consider a card-list layout for mobile-first data.
Accessibility
- Renders real
<table>/<thead>/<tbody>/<tr>/<th>/<td>— semantics are platform-native. - Sortable headers carry
aria-sortreflecting the current sort direction. - Selected rows use
aria-selected. - Action buttons in cells are real buttons — keyboard, focus, screen reader announce normally.
API
Table
| Prop | Type | Default |
|---|---|---|
striped | boolean | false |
size | 'default' | 'comfortable' | 'default' |
flush | boolean | false |
children | ReactNode (required) | — |
Plus standard <table> HTML attributes.
Subcomponents
| Component | Element | Notable props |
|---|---|---|
TableHeader | <thead> | — |
TableBody | <tbody> | — |
TableRow | <tr> | selected: boolean — never onClick (see callout) |
TableHead | <th> | sortable: boolean, sortDirection: 'asc' | 'desc' |
TableCell | <td> | — |
TableActionsCell | <td> | align: 'right' | 'center' — owns the trailing [View][Edit][⋯] cluster |
All subcomponents accept their respective HTML attributes.
Usage
import {
Table, TableHeader, TableBody, TableRow, TableHead,
TableCell, TableActionsCell,
} from '@brikdesigns/bds';
import { Badge, Button, TextLink } from '@brikdesigns/bds';
<Table striped size="comfortable">
<TableHeader>
<TableRow>
<TableHead sortable sortDirection="asc">Name</TableHead>
<TableHead>Service line</TableHead>
<TableHead>Status</TableHead>
<TableHead style={{ textAlign: 'right' }}>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>
<TextLink size="small" onClick={() => openSheet(service.id)}>
{service.name}
</TextLink>
</TableCell>
<TableCell>
<TextLink size="small" onClick={() => openSheet(service.serviceLineId, 'service-line')}>
{service.serviceLine}
</TextLink>
</TableCell>
<TableCell><Badge status="positive" size="sm">Active</Badge></TableCell>
<TableActionsCell>
<Button variant="primary" size="sm" icon={<EyeIcon />} label="View" onClick={() => openSheet(service.id)} />
<Button variant="primary" size="sm" icon={<PenIcon />} label="Edit" onClick={() => navigateEdit(service.id)} />
</TableActionsCell>
</TableRow>
</TableBody>
</Table>Related
- CardList — card grid alternative
- CardControl — settings-row pattern
- Pagination — pair below large tables
- Badge / Tag — common cell content
- Storybook playground