Brik Design System
Components

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 classAffordancePattern
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 textNot 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:

VariantUse
primaryPrimary action (view, edit, open)
secondaryNeutral (download, copy)
ghostLow emphasis (overflow menu)
destructiveDestructive (delete, remove) — use sparingly in-row; prefer the overflow menu

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-sort reflecting the current sort direction.
  • Selected rows use aria-selected.
  • Action buttons in cells are real buttons — keyboard, focus, screen reader announce normally.

API

Table

PropTypeDefault
stripedbooleanfalse
size'default' | 'comfortable''default'
flushbooleanfalse
childrenReactNode (required)

Plus standard <table> HTML attributes.

Subcomponents

ComponentElementNotable props
TableHeader<thead>
TableBody<tbody>
TableRow<tr>selected: booleannever 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>

On this page