Tables
Data tables for screener results, watchlists, and portfolio holdings
Overview
Tables use react-aria-components (HeroUI v3 does not include Table). They are not wrapped in a Card — they render directly in a plain <div>.
Standard Table
import {
Cell, Column, Row, Table, TableBody, TableHeader,
} from "react-aria-components";
<div>
<div className="overflow-x-auto">
<Table aria-label="Stock screener results" className="w-full min-w-max text-sm">
<TableHeader className="bg-accent text-accent-foreground">
<Column className="p-4 font-medium outline-none">
<button
type="button"
className="inline-flex items-center gap-2 text-accent-foreground"
>
Symbol
<ArrowUpDown className="size-3.5 text-accent-foreground" />
</button>
</Column>
<Column className="p-4 font-medium text-right outline-none">
<button
type="button"
className="inline-flex items-center gap-2 text-accent-foreground"
>
Price
</button>
</Column>
</TableHeader>
<TableBody>
<Row className="group border-separator border-b outline-none last:border-b-0 hover:bg-accent/5">
<Cell className="p-4 outline-none">AAPL</Cell>
<Cell className="p-4 text-right tabular-nums outline-none">$175.25</Cell>
</Row>
</TableBody>
</Table>
</div>
</div>Sticky Symbol Column
For wide tables with horizontal scroll, the symbol column stays pinned to the left.
{/* Header cell */}
<Column className="sticky left-0 z-10 border-separator border-r bg-accent p-4 font-medium outline-none">
Symbol
</Column>
{/* Body cell */}
<Cell className="sticky left-0 z-10 border-separator border-r bg-surface p-4 outline-none group-hover:bg-accent/5">
<Link href="/dashboard/stocks/aapl" className="flex items-center gap-2 hover:underline">
<CompanyLogo symbol="AAPL" size="sm" />
<div className="flex flex-col gap-2">
<span className="font-semibold text-accent">AAPL</span>
<span className="max-w-36 truncate text-muted text-xs">Apple Inc.</span>
</div>
</Link>
</Cell>Sort Indicators
Sort icons always use text-accent-foreground — never muted.
function SortIndicator({ column, currentSort, direction }) {
if (currentSort !== column)
return <ArrowUpDown className="size-3.5 text-accent-foreground" />;
return direction === "asc" ? (
<ArrowUp className="size-3.5" />
) : (
<ArrowDown className="size-3.5" />
);
}Design Rules
| Element | Classes | Notes |
|---|---|---|
| Header | bg-accent text-accent-foreground | Accent background, all text accent-foreground |
| Header text | text-accent-foreground | Never muted — full opacity on all labels and sort icons |
| Row | group border-separator border-b hover:bg-accent/5 | group enables sticky cell hover |
| Sticky header cell | bg-accent | Matches header background |
| Sticky body cell | bg-surface group-hover:bg-accent/5 | White base, accent tint on hover |
| Borders | border-separator | Never border-default or border-divider |
| Financial values | tabular-nums | Ensures proper column alignment |
| Wrapper | Plain <div> | No <Card> wrapper around tables |
| Scroll | overflow-x-auto + min-w-max | Horizontal scroll for wide tables |
Expandable Rows
For tables with row details (e.g., watchlist), use onAction on the Row and render a details row below.
<Row
className="group cursor-pointer border-separator border-b outline-none last:border-b-0 hover:bg-accent/5"
onAction={() => toggleExpanded(item.id)}
>
{/* ... cells ... */}
</Row>
{isExpanded && (
<Row className="outline-none">
<Cell className="col-span-full p-0 outline-none" colSpan={columns.length}>
<RowDetails item={item} />
</Cell>
</Row>
)}