Components

Table

Structural primitive for tabular data. Eight compound parts (Table / TableHeader / TableBody / TableRow / TableHead / TableCell / TableFooter / TableCaption) so every list surface in the Dashboard reads with the same rhythm. Yuno spec (per Figma): header row 40px tall with bg-sidebar + text-sm font-medium text-muted-foreground (never uppercase), body cells 52px tall (h-13) with p-2 + text-sm text-foreground, 1px border between rows, hover:bg-muted/40, selectable rows via data-state='selected'.

Updated Jul 19, 2026 by Leonardo Posada

Anatomy

Eight compound parts. Table is the outer scroll-wrapped <table>; TableHeader / TableBody / TableFooter are the thead / tbody / tfoot semantic groups; TableRow is a <tr> with hover + selected states; TableHead is the header cell (40px, bg-sidebar, muted foreground); TableCell is the body cell (52px, text-foreground); TableCaption is the caption below the table.

NameTypeStatusValue
Item oneType AActive128
Item twoType BPending64
Item threeType AActive32
  1. 1
    Table (Root)

    Wraps the <table> in a scroll container (<div className='relative w-full overflow-auto'>) so wide tables scroll horizontally without breaking the page layout. w-full caption-bottom text-sm.

  2. 2
    TableHeader

    The <thead>. Applies border-b to every <tr> inside so the header row has a rule below it.

  3. 3
    TableHead

    The header cell (<th>). Yuno Figma spec: h-10 bg-sidebar px-2 text-sm font-medium text-muted-foreground text-left align-middle. Never uppercase. Can hold an Info icon on the left and a sort ArrowDown on the right (gap-2).

  4. 4
    TableBody

    The <tbody>. Strips the border-b from the last <tr> so the table ends flush with its outer border.

  5. 5
    TableRow

    The <tr>. Ships border-b border-border + transition-colors + hover:bg-muted/40 + data-[state=selected]:bg-muted. Set data-state='selected' to keep a row highlighted after selection (e.g. inspector open).

  6. 6
    TableCell

    The body cell (<td>). Yuno Figma spec: h-13 (52px) p-2 (8px) align-middle text-sm text-foreground text-left. All content stays left-aligned — for numeric columns add tabular-nums so the digits align vertically without right-aligning.

  7. 7
    TableFooter

    The <tfoot>. border-t bg-muted/50 font-medium. Use for totals or summary rows.

  8. 8
    TableCaption

    The <caption> below the table. mt-4 text-sm text-muted-foreground. Rare in the Dashboard — reserve for accessibility captions when the surrounding heading isn't clear.

Variants

One primitive, several arrangements. Header cells can hold a plain title, a leading Info icon, a trailing sort ArrowDown, or both. Body cells can be plain text, numeric (tabular-nums for digit alignment — still left-aligned), or badge/status pills. Row height stays 52px so all lists in the Dashboard match. First and last cells across both header and body get 24px padding on the outer edge automatically (via first:pl-6 last:pr-6) — the gutter keeps content off the border.

Plain
NameTypeStatus
Item oneType AActive
<TableHead>Column title</TableHead>
Header with Info icon
ValueType
128Type A
<TableHead>
  <span className="inline-flex items-center gap-2">
    <Tooltip>
      <TooltipTrigger>
        <Info weight="light" className="size-4" />
      </TooltipTrigger>
      <TooltipContent>Explain the column</TooltipContent>
    </Tooltip>
    Column title
  </span>
</TableHead>
Header with sort indicator
Type
Item oneType A
<TableHead>
  <button type="button" className="inline-flex items-center gap-2">
    Column title
    <ArrowDown weight="light" className="size-4" />
  </button>
</TableHead>
Header with Info + sort
Type
128Type A
<TableHead>
  <span className="inline-flex items-center gap-2">
    <Tooltip>
      <TooltipTrigger asChild>
        <button type="button" aria-label="About this column">
          <Info weight="light" className="size-4" />
        </button>
      </TooltipTrigger>
      <TooltipContent>Short helper text.</TooltipContent>
    </Tooltip>
    <button type="button" className="inline-flex items-center gap-2">
      Column title
      <ArrowDown weight="light" className="size-4" />
    </button>
  </span>
</TableHead>
Numeric column (tabular-nums, still left-aligned)
NameValue
Item one128
Item two64
Item three32
<TableHead>Value</TableHead>
<TableCell className="tabular-nums">{value}</TableCell>

// Content stays LEFT-aligned per Yuno spec.
// tabular-nums locks digit widths so numbers line up vertically.

States

Row: idle → hover:bg-muted/40 (subtle) → data-state='selected':bg-muted (persistent). Header row inherits bg-sidebar so it separates from the body without a stripe. There is no active/focused cell state — that's the row hover.

NameTypeValue
Idle rowType A128
Selected rowType B64
Idle rowType C32
Header 40, body 52 — that's the rhythm
The Yuno table rhythm is deliberate: 40px header row (h-10) with bg-sidebar, 52px body rows (h-13), 8px padding on cells. Every list surface in the Dashboard uses this so a user can scan across screens without recalibrating. Override widths and text alignment per column — never the row heights.

Recipes

Ready-to-copy compositions covering the most common Yuno usages of this atom.

Basic table

The canonical Dashboard table: header row with plain titles, body rows with mixed types, a right-aligned numeric column. Wrap in a rounded-lg border to give it a Card surface.

NameTypeStatusValue
Item oneType AActive128
Item twoType BPending64
Item threeType AActive32
Item fourType CArchived16
<div className="w-full overflow-hidden rounded-lg border bg-card">
  <Table>
    <TableHeader>
      <TableRow>
        <TableHead>Name</TableHead>
        <TableHead>Type</TableHead>
        <TableHead>Status</TableHead>
        <TableHead>Value</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      {rows.map((r) => (
        <TableRow key={r.name}>
          <TableCell>{r.name}</TableCell>
          <TableCell>{r.type}</TableCell>
          <TableCell>{r.status}</TableCell>
          <TableCell className="tabular-nums">{r.value}</TableCell>
        </TableRow>
      ))}
    </TableBody>
  </Table>
</div>
Sortable columns (ArrowDown indicator)

Add a Phosphor ArrowDown next to the header text to signal sort state. Rotate 180° for ascending (via rotate-180) or keep down for descending. Wire the header to your sort handler.

Name
Item one128
Item two64
Item three32
<TableHead>
  <button
    type="button"
    onClick={() => setSortDir((d) => (d === "asc" ? "desc" : "asc"))}
    className="inline-flex items-center gap-2"
  >
    Value
    <ArrowDown
      weight="light"
      className={cn("size-4 transition-transform", sortDir === "asc" && "rotate-180")}
    />
  </button>
</TableHead>
Header with Info tooltip

Prepend a Phosphor Info icon inside the header cell + a Tooltip to explain what the column means. Common for metric columns whose formula isn't obvious.

NameValue
Item one128
<TableHead>
  <span className="inline-flex items-center gap-2">
    <Tooltip>
      <TooltipTrigger asChild>
        <button type="button" aria-label="About this column">
          <Info weight="light" className="size-4" />
        </button>
      </TooltipTrigger>
      <TooltipContent>Short helper text.</TooltipContent>
    </Tooltip>
    Value
  </span>
</TableHead>
Selected row (inspector open)

Set data-state='selected' on the row you want to keep highlighted while an inspector Sheet is open. The row picks up bg-muted (stronger than the hover tint) so the user tracks which record they're editing.

NameType
Item oneType A
Item two (selected)Type B
Item threeType C
<TableRow data-state="selected">
  {/* cells */}
</TableRow>
Row-level actions column (Eye or Dots)

The trailing action column always uses one of two documented cell types (see 'Cell types' below). Eye + Tooltip when the ONLY row-level action is 'view details'. Dots (DotsThreeOutline fill) + Tooltip + DropdownMenu when the row supports 2+ actions (edit / duplicate / delete / open). Both share the same 'Icon-only with tooltip' Button treatment and both must be sticky right-0 border-l bg-card so they stay visible when the table scrolls horizontally.

NameTypeSingleMultiple
Item oneType A
Item twoType B
Item threeType A
{/* Reuse the two documented cell types — see "Cell types" below.
   Eye when the ONLY action is view-details.
   Dots + DropdownMenu when there are 2+ actions per row. */}
<TableCell className="sticky right-0 z-10 w-10 pl-4 pr-4! border-l border-border bg-card">
  {/* single: Eye + Tooltip */}
  {/* multiple: Dots + Tooltip + DropdownMenu */}
</TableCell>
Sticky action column (wide table with horizontal scroll)

When the table exceeds its container width, the outer wrapper's overflow-auto lets it scroll horizontally. Reuse either documented action cell type — 'Eye action (single)' or 'Dots action (multiple)' from Cell types below — and add sticky right-0 z-10 border-l bg-card to BOTH the TableHead and TableCell of the action column. The border-l acts as the visible seam; the bg-card layer prevents scrolling content from bleeding through.

NameTypeStatusExtra column 1Extra column 2Extra column 3Extra column 4
Item oneType AActiveValue AValue BValue CValue D
Item twoType BPendingValue AValue BValue CValue D
Item threeType AActiveValue AValue BValue CValue D

← scroll horizontally · the eye column stays pinned right →

{/* When the table has more columns than the container can show,
   the outer overflow-auto scrolls horizontally.
   Reuse either documented cell type (see "Cell types" below):
   - Eye action (single: view details)
   - Dots action (multiple: DropdownMenu)
   Both must be sticky right-0 with border-l bg-card. */}

<TableHead className="sticky right-0 z-10 w-10 pl-4 pr-4! border-l border-border bg-sidebar" />

{/* Eye — single action */}
<TableCell className="sticky right-0 z-10 w-10 pl-4 pr-4! border-l border-border bg-card">
  <Tooltip>
    <TooltipTrigger asChild>
      <button
        type="button"
        aria-label="View details"
        className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-transparent bg-transparent text-muted-foreground transition-all hover:border-border hover:bg-background hover:text-foreground hover:shadow-xs"
      >
        <Eye weight="light" className="size-4" />
      </button>
    </TooltipTrigger>
    <TooltipContent side="top" sideOffset={6}>View details</TooltipContent>
  </Tooltip>
</TableCell>

{/* Dots — multiple actions (see cellTypes.dotsAction for the full snippet) */}
Row selection (checkbox column with bulk-select)

When the table supports bulk actions (delete selected, export, tag) add a leading checkbox column: a select-all Checkbox in the header + a per-row Checkbox in each body cell. The header Checkbox uses the 3-state pattern (checked / indeterminate / unchecked) and toggles ALL rows. Mirror the sticky rule from the action column but pin LEFT: sticky left-0 z-10 border-r border-border bg-card + w-10 pl-4! pr-4 (16px each side, override the primitive's first:pl-6 rule). Gutter shift: when a checkbox column is first, the 24px outer gutter shifts to the SECOND column (Name) — add pl-6 to it manually. Same on the right when there's an action column: add pr-6 to the second-to-last data column.

NameTypeStatusExtra column 1Extra column 2Extra column 3Extra column 4
Item oneType AActiveValue AValue BValue CValue D
Item twoType BPendingValue AValue BValue CValue D
Item threeType AActiveValue AValue BValue CValue D
Item fourType CArchivedValue AValue BValue CValue D

1 of 4 selected · click the header checkbox to toggle all

{/* Header Checkbox = select-all (with indeterminate state).
   Row Checkbox = per-row toggle. */}

const [checked, setChecked] = React.useState<Set<string>>(new Set());

const allChecked = checked.size === rows.length;
const someChecked = checked.size > 0 && checked.size < rows.length;
const headerState = allChecked ? true : someChecked ? "indeterminate" : false;

const toggleAll = () => {
  setChecked(allChecked ? new Set() : new Set(rows.map((r) => r.id)));
};

const toggleRow = (id: string) => {
  setChecked((prev) => {
    const next = new Set(prev);
    if (next.has(id)) next.delete(id);
    else next.add(id);
    return next;
  });
};

<TableHead className="sticky left-0 z-10 w-10 pl-4! pr-4 border-r border-border bg-sidebar">
  <Checkbox
    checked={headerState}
    onCheckedChange={toggleAll}
    aria-label="Select all rows"
  />
</TableHead>

<TableCell className="sticky left-0 z-10 w-10 pl-4! pr-4 border-r border-border bg-card">
  <Checkbox
    checked={checked.has(row.id)}
    onCheckedChange={() => toggleRow(row.id)}
    aria-label={`Select ${row.name}`}
  />
</TableCell>

Cell types

Not every cell is plain text. Below are the canonical cell types used across the Yuno Dashboard so a PM can copy the right shape when a column shows more than a label. All still follow the two Yuno cell rules: left-aligned content, and 24px outer gutter on the first / last column.

TypeExample cell
TextItem name
Numeric1,284
Currency amount1,284.50USD
Two lines (title + subtitle)
Primary line
Secondary line
Date / time (Yuno format)Feb 8, 2023, 10:00 a.m.
Status badgeSucceeded
Progress
68%
User (avatar + name)
A
Alex Doe
alex@example.com
Provider (logo + name)
Stripe
Country (flag + name)
Brazil
ID / link
Button outline sm
Switch
Input (inline edit)
Eye action (single)
Dots action (multiple)
TextNames, labels, descriptions. The default.
<TableCell>{value}</TableCell>
NumericCounts, values without a currency. tabular-nums lines up digits vertically without right-aligning.
<TableCell className="tabular-nums">{value}</TableCell>
Currency amountMonetary values. Amount + currency code in the same cell, tabular-nums for digit alignment.
<TableCell className="tabular-nums">
  <span className="font-medium text-foreground">{amount}</span>
  <span className="ml-1 text-muted-foreground">{currency}</span>
</TableCell>
Two lines (title + subtitle)Generic two-row cell — a primary line and a secondary supporting line. Any column where a label + qualifier reads better as a stack than inline (a name + role, a value + delta, an id + last-seen). Primary text-foreground + secondary text-xs text-muted-foreground.
<TableCell>
  <div className="text-foreground">{primary}</div>
  <div className="text-xs text-muted-foreground">{secondary}</div>
</TableCell>
Date / time (Yuno format)Dates and times inline in ONE cell — this is how Yuno backend sends them, so no two-line split. Same text-sm text-foreground as a plain Text cell. Formats: date-only 'Dic 14, 2023' · time-only '8:00 p.m.' or '12:00 m.' (noon) · combined 'Feb 8, 2023, 10:00 a.m.'.
<TableCell>{dateTime}</TableCell>

// Backend sends the string pre-formatted. Just render it — no client-side formatting.
// Formats:
//   date-only:  Dic 14, 2023
//   time-only:  8:00 p.m.   or   12:00 m.   or   5:00 p.m.
//   combined:   Feb 8, 2023, 10:00 a.m.
Status badgeAny state that reads as a discrete label — transactions, connections, users, invoices. Uses the StatusBadge preset.
<TableCell>
  <StatusBadge status="succeeded" />
</TableCell>
ProgressCapacity, completion, or impact percentage. Pair the bar with the numeric value on its right.
<TableCell>
  <div className="flex items-center gap-3">
    <Progress value={value} className="h-1.5 w-20" />
    <span className="text-sm tabular-nums text-muted-foreground">{value}%</span>
  </div>
</TableCell>
User (avatar + name)Any row that represents a person — team members, transaction customer, comment author. Optional email/subtitle below the name.
<TableCell>
  <div className="flex items-center gap-3">
    <Avatar size="sm">
      <AvatarFallback>{name[0]}</AvatarFallback>
    </Avatar>
    <div>
      <div className="text-foreground">{name}</div>
      <div className="text-xs text-muted-foreground">{email}</div>
    </div>
  </div>
</TableCell>
Provider (logo + name)Rows that reference a payment provider (Stripe, Adyen, dLocal, PayU, etc). Kit convention: pull the logo from Yuno's icon CDN — https://icons.prod.y.uno/{provider}_logosimbolo.png — swap the provider slug and you get the correct logo. 16×16 rendered (size-4 token), 8px gap, provider name in text-sm.
<TableCell>
  <div className="flex items-center gap-2">
    <img
      src={`https://icons.prod.y.uno/${providerSlug}_logosimbolo.png`}
      alt=""
      className="size-4"
    />
    <span>{providerName}</span>
  </div>
</TableCell>

// providerSlug is the provider's lowercase name — stripe / adyen / dlocal / payu / mercadopago / etc.
Country (flag + name)Rows that reference a country — merchant location, transaction origin, currency country. 16×16 circular flag (size-4 token) on the left, 8px gap, country name inherits the default cell typography (text-sm text-foreground, font-normal — same as a plain Text cell). Kit convention: circle-flags CDN (HatScripts, MIT) — SVGs are already circular, zero dependencies, PMs just swap the ISO-2 country code in the URL.
<TableCell>
  <div className="flex items-center gap-2">
    <img
      src={`https://hatscripts.github.io/circle-flags/flags/${countryCode}.svg`}
      alt=""
      className="size-4"
    />
    <span>{countryName}</span>
  </div>
</TableCell>

// countryCode is the ISO 3166-1 alpha-2 in lowercase — br / us / ar / mx / cl / co / pe / etc.
// The name inherits the default cell typography — no font-medium override.
ID / linkClickable IDs (transaction ID, rule ID, invoice number). Renders as a Button link that opens the detail Sheet.
<TableCell>
  <button type="button" className="text-primary hover:underline">
    {id}
  </button>
</TableCell>
Button outline smInline row-level action that isn't hidden behind a menu — e.g. Retry / Approve / Resend. Use variant='outline' size='sm' so it stays compact inside the 52px row.
<TableCell>
  <Button variant="outline" size="sm">Action</Button>
</TableCell>
SwitchPer-row on/off toggle — enabling a rule, activating a webhook, marking a row as visible. Radix Switch inline; wire onCheckedChange to your handler.
<TableCell>
  <Switch checked={value} onCheckedChange={handleToggle} />
</TableCell>
Input (inline edit)Editable cell inside the table — quick per-row edits (rename, tweak an amount, set a label). Use the compact h-8 Input so it fits inside the 52px row. Focus reveals the ring; hover surface is intentionally subtle so idle rows don't look busy.
<TableCell>
  <Input
    value={value}
    onChange={(e) => handleChange(e.target.value)}
    className="h-8"
  />
</TableCell>
Eye action (single: view details)Trailing action column when the ONLY row-level action is 'view details'. Same 'Icon-only with tooltip' treatment from Button — no dropdown, direct action. Tooltip says 'View details' (or equivalent). The sticky right-0 pin keeps the icon visible when the table scrolls horizontally — do the same in the matching TableHead.
<TableCell className="sticky right-0 z-10 w-10 pl-4 pr-4! border-l border-border bg-card">
  <Tooltip>
    <TooltipTrigger asChild>
      <button
        type="button"
        aria-label="View details"
        className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-transparent bg-transparent text-muted-foreground transition-all hover:border-border hover:bg-background hover:text-foreground hover:shadow-xs"
      >
        <Eye weight="light" className="size-4" />
      </button>
    </TooltipTrigger>
    <TooltipContent side="top" sideOffset={6}>View details</TooltipContent>
  </Tooltip>
</TableCell>
Dots action (multiple: DropdownMenu)Trailing action column when the row supports MULTIPLE actions — Edit / Delete / Duplicate / Open. Same 'Icon-only with tooltip' Button treatment, but the button is a DropdownMenuTrigger. Icon is DotsThreeOutline (fill). The sticky right-0 pin keeps the icon visible when the table scrolls horizontally — do the same in the matching TableHead.
<TableCell className="sticky right-0 z-10 w-10 pl-4 pr-4! border-l border-border bg-card">
  <DropdownMenu>
    <Tooltip>
      <TooltipTrigger asChild>
        <DropdownMenuTrigger asChild>
          <button
            type="button"
            aria-label="Actions"
            className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-transparent bg-transparent text-muted-foreground transition-all hover:border-border hover:bg-background hover:text-foreground hover:shadow-xs data-[state=open]:border-border data-[state=open]:bg-background data-[state=open]:text-foreground"
          >
            <DotsThreeOutline weight="fill" className="size-4" />
          </button>
        </DropdownMenuTrigger>
      </TooltipTrigger>
      <TooltipContent side="top" sideOffset={6}>Actions</TooltipContent>
    </Tooltip>
    <DropdownMenuContent align="end">
      <DropdownMenuItem>Edit</DropdownMenuItem>
      <DropdownMenuItem>Duplicate</DropdownMenuItem>
      <DropdownMenuItem>Delete</DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
</TableCell>

Import

Full compound import from @/components/ui/table.

Import only the parts you use. The compound is fully tree-shakable.
import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

Props

Every part forwards standard HTML table props (<th>, <td>, <tr>, etc). The most useful in prototypes:

PropTypeDefaultDescription
Table.classNamestringMerged onto the inner <table>. Do not use it to control width — the outer scroll wrapper handles that.
TableHead.classNamestringMerged onto the <th>. Common overrides: w-10 (icon/action column), tabular-nums (numeric column header). Do not remove bg-sidebar — it's the Yuno rhythm. NEVER use text-right — Yuno spec is all content left-aligned.
TableCell.classNamestringMerged onto the <td>. Common overrides: tabular-nums (numeric column, for consistent digit width — still left-aligned), w-10 (action column). Never override the h-13 height. NEVER use text-right — Yuno spec is all content left-aligned.
TableRow (data-state)"selected"Set data-state='selected' to keep a row highlighted (bg-muted, stronger than the hover tint). Used when an inspector Sheet is open so the user tracks which record they're editing.
TableRow.onClick(event) => voidOptional click handler on the whole row. Common pattern: open a Sheet with the row's detail. Pair with hover:bg-muted/40 (already default) for a visible affordance.
TableHead / TableCell colSpannumberStandard HTML colSpan. Use for header groups (TableHead with colSpan={4}) or spanning empty-state cells across the body.

When to use

  • Any Dashboard list where column alignment matters (transactions, rules, connections, teams, invoices).
  • Reports, ledgers, and structured records where users need to compare rows side by side.
  • Any surface where the data is genuinely tabular — one row per record, one column per field.

When not to use

  • Key-value pairs (single record with named fields) — use a description list.
  • A flat vertical list of items with a single label — use a Card list or a Sheet body.
  • Content that reads better as cards (image + copy + CTA) — Table forces horizontal comparison and it fights image-led content.

Usage

Do
  • Keep the header at h-10 and body cells at h-13 — the Yuno rhythm.
  • Left-align every column, including numeric ones. Use tabular-nums on numeric cells so the digits line up vertically without right-aligning the content.
  • The first and last cell's 24px outer padding (first:pl-6 last:pr-6) is baked in — don't override it or the content will kiss the outer border.
  • Add hover:bg-muted/40 (already the default) so the pointer position is obvious across wide tables.
  • Wrap the whole Table in a rounded-lg border to give it a Card surface — the primitive doesn't ship this.
  • Use TableCaption or a heading above the table so screen readers announce what it contains.
Don't
  • Don't right-align any cell content — Yuno spec is left-aligned across every column (numeric columns included). Use tabular-nums for digit alignment instead of text-right.
  • Don't uppercase the header text — the Yuno spec (per Figma) is text-sm font-medium normal case.
  • Don't stripe rows — the hover state covers the same purpose without visual noise.
  • Don't inflate the body row past h-13 (52px) — the rhythm breaks and dense lists get too tall.
  • Don't embed full-size Inputs inside cells — either drop into a Sheet to edit or use a compact inline treatment.
  • Don't skip the outer wrapper — a bare <Table> without a border reads as raw HTML.
  • Don't override the first/last column's 24px padding — it's the gutter that keeps content off the outer border.

Related

Cross-links to atoms and patterns you may reach for next.

  • PaginationEvery table over ~20 rows gets Pagination — the canonical Dashboard pattern. Space it 24px below the table (mt-6) — Pagination is not part of the Table primitive, it lives as a sibling under it.
  • Scroll areaWrap wide tables in a horizontal ScrollArea so the page itself doesn't force horizontal scroll.
  • CheckboxFor row-selection tables, put a Checkbox in the first cell and the same in the header (select-all).
  • Dropdown menuRow-level actions live in a DropdownMenu triggered by an icon-only Button in the last column.
  • SkeletonWhile the rows load, skeleton the tbody cells — keep the thead labels visible so the user reads the shape of the data.