Light

Table

A responsive table component.

Spec · from metadata

When to use

  • Displaying structured tabular data (rows and columns)
  • Showing lists of records with multiple attributes
  • Read-only data presentation with optional striped rows

When not to use

  • For sortable/filterable/paginated tables — use DataTable pattern with @tanstack/react-table instead
  • For key-value pairs — use a description list or Card layout instead
  • For layout purposes — use CSS Grid or Flexbox instead

Variants

PropValuesDefaultDescription
stripedtruefalsefalsePass `striped` prop to TableBody for alternating row backgrounds via bg-muted/40

Anti-patterns

Avoid<Table><TableRow><TableCell>...</TableCell></TableRow></Table>
Prefer<Table><TableHeader><TableRow><TableHead>...</TableHead></TableRow></TableHeader><TableBody><TableRow><TableCell>...</TableCell></TableRow></TableBody></Table>

Always use TableHeader/TableBody sections for proper semantics and accessibility

Avoid<div className="overflow-x-auto"><Table>...</Table></div>
Prefer<Table>...</Table>

Table already renders inside an overflow-x-auto container — wrapping again adds unnecessary nesting

Accessibility

  • Screen readerUse TableCaption to provide a description of the table for screen readers
  • ContrastHeader cells use muted-foreground; ensure sufficient contrast with background

Token bindings

TokenCategoryUsage
bg-input-bgcolorTable container background
text-muted-foregroundcolorHeader cell and caption text color
bg-muted/50colorRow hover state background
bg-muted/40colorStriped row even-child background

Import

import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from "@timelycare/helix-ui"

Structure

PartTailwind
Table (container)w-full overflow-x-auto bg-input-bg p-4
Tablew-full caption-bottom text-sm
TableHeader
TableRowborder-b border-border transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50
TableHeadh-10 px-2 text-left align-middle text-sm font-semibold text-muted-foreground
TableCellp-2 align-top text-sm font-normal text-foreground
TableFooterborder-t font-medium
TableCaptionpt-4 text-sm text-muted-foreground text-center

Sizes

SizeCell HeightUse For
defaulth-[52px]Standard data tables
mdh-[72px]Tables with more content
lgh-[96px]Tables with rich content (images, multi-line)

Variants

Striped

Apply alternating row backgrounds by passing striped to TableBody. Odd-numbered rows (starting with row 1) receive bg-muted/40; even rows stay bg-input-bg. Use for dense, read-heavy tables where the eye needs help tracking across a row.

<Table>
  <TableHeader>
    <TableRow>
      <TableHead>ID</TableHead>
      <TableHead>Name</TableHead>
      <TableHead>Status</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody striped>
    {rows.map((row) => (
      <TableRow key={row.id}>
        <TableCell>{row.id}</TableCell>
        <TableCell>{row.name}</TableCell>
        <TableCell><Badge variant="outlineSage">{row.status}</Badge></TableCell>
      </TableRow>
    ))}
  </TableBody>
</Table>

Notes:

  • Hover uses hover:bg-accent uniformly on all rows — no special casing between stripe and non-stripe rows.
  • Do not combine striped with row-level bg-* class overrides — the stripe will be overridden.
  • Use striping only on TableBody rows; header and footer rows are unaffected.

Cell Variants

VariantContentUse For
DefaultText onlySimple data
Bold TextTitle + descriptionPrimary column, identifiers
BadgeBadge componentStatus indicators
AvatarAvatar + textUser columns
SwitchSwitch toggleSettings tables
ButtonAction buttonRow actions
DropdownDropdownMenu trigger with MoreVertical iconMultiple actions
ProgressProgress barCompletion status
ImageAspectRatio imageMedia tables
InputInput fieldEditable cells
Toggle GroupToggle buttonsOptions selection

States

StateImplementation
DefaultBase styling
Hover (all rows)hover:bg-accent — uniform across striped and non-striped rows
Focus (keyboard)focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50 — built into TableRow
Selectedbg-accent — same color as hover; selected rows have no additional hover state

Common Patterns

Basic Table

<Table>
  <TableCaption>A list of your recent invoices.</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Invoice</TableHead>
      <TableHead>Status</TableHead>
      <TableHead className="text-right">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell className="font-semibold">INV001</TableCell>
      <TableCell>Paid</TableCell>
      <TableCell className="text-right">$250.00</TableCell>
    </TableRow>
  </TableBody>
</Table>

With Footer

<Table>
  <TableHeader>
    <TableRow>
      <TableHead>Invoice</TableHead>
      <TableHead className="text-right">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    {/* rows */}
  </TableBody>
  <TableFooter>
    <TableRow>
      <TableCell>Total</TableCell>
      <TableCell className="text-right font-semibold">$2,500.00</TableCell>
    </TableRow>
  </TableFooter>
</Table>

Cell with Title + Description

<TableCell>
  <div className="flex flex-col">
    <span className="font-semibold">John Doe</span>
    <span className="text-muted-foreground">john@example.com</span>
  </div>
</TableCell>

Multi-line Cell Content

Cells default to align-top. Row height expands automatically to fit content — no fixed height is applied. Wrap long text inside a max-w-* class on the cell to control column width.

<TableCell className="max-w-[260px]">
  A longer description that wraps across multiple lines. The row height
  expands to fit the content, and all other cells in the row align to the top.
</TableCell>

Notes:

  • whitespace-nowrap is not applied by default — text wraps naturally.
  • All cells use align-top, so multi-line and single-line cells in the same row stay top-aligned.
  • Avoid setting explicit row heights (h-[52px] etc.) on rows with variable-length content.

Cell with Avatar

<TableCell>
  <div className="flex items-center gap-2">
    <Avatar className="size-8">
      <AvatarImage src="/avatar.jpg" />
      <AvatarFallback>JD</AvatarFallback>
    </Avatar>
    <span>John Doe</span>
  </div>
</TableCell>

Cell with Badge

<TableCell>
  <Badge variant="outlineSage">Active</Badge>
</TableCell>

Right-Aligned Column (Numbers)

<TableHead className="text-right">Amount</TableHead>
<TableCell className="text-right">$250.00</TableCell>

Table with Toolbar

Data tables should include filtering and actions above.

<div className="space-y-4">
  {/* Toolbar */}
  <div className="flex items-center justify-between">
    <Input placeholder="Filter..." className="max-w-sm" />
    <div className="flex items-center gap-2">
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button variant="outline" size="sm">
            <SlidersHorizontal className="size-4 mr-2" />
            View
          </Button>
        </DropdownMenuTrigger>
        {/* Menu content */}
      </DropdownMenu>
      <Button size="sm">
        <Plus className="size-4 mr-2" />
        Add
      </Button>
    </div>
  </div>
  
  {/* Table */}
  <div className="rounded-md border">
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead className="w-[50px]"><Checkbox /></TableHead>
          <TableHead>Name</TableHead>
          <TableHead>Status</TableHead>
          <TableHead className="w-[50px]"></TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {/* Rows */}
      </TableBody>
    </Table>
  </div>
  
  {/* Pagination */}
  <div className="flex items-center justify-between">
    <p className="text-sm text-muted-foreground">0 of 10 selected</p>
    <div className="flex items-center gap-2">
      <Button variant="outline" size="sm" disabled>Previous</Button>
      <Button variant="outline" size="sm">Next</Button>
    </div>
  </div>
</div>

Badge in Table Cell

Status badges in table cells always use Helix outline primitive badge variants — never the shadcn default, secondary, destructive, or outline variants in table contexts.

<TableCell>
  <Badge variant="outlineSage">Active</Badge>
</TableCell>

Status → Badge Variant Mapping

StatusBadge Variant
Active / SuccessoutlineSage
Pending / In ProgressoutlineNavy
Error / FailedoutlineBerry
Inactive / DisabledoutlineNeutral
CancelledoutlineBerry

Accessibility

  • Standard HTML table — uses semantic markup for accessibility
  • Use <thead>, <tbody>, <th> with scope="col" for proper structure
  • Screen reader: header/cell associations handled automatically with proper markup
  • Clickable rows: add tabIndex={0} to <TableRow> and handle onKeyDown for Enter/Space — the focus ring is built in via focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50
  • Sticky checkbox column: apply sticky left-0 z-10 bg-input-bg (or the row's active background) to both <TableHead> and <TableCell> in the checkbox column so selection state remains visible during horizontal scroll
  • Add role="grid" only if table is interactive

Gotchas

ProblemSolution
Caption at topCaption is always bottom by default
Missing hoverEnsure hover:bg-accent on TableRow
No keyboard focus ringAdd tabIndex={0} to the row — the ring is already in TableRow base styles and activates automatically
Sticky column bleedsSet explicit bg-input-bg (or the row's active bg) on sticky <TableHead> and <TableCell> — transparent sticky cells let scrolling content show through
Alignment issuesUse align-middle on cells
Last cell borderRemove with border-b-0 if needed
Wide tablesWrap in overflow-x-auto container

See Also


Last updated: May 1, 2026