Table

Rows of like things: services, bookings, staff. Framed on its own glass surface, or plain inside a card; it scrolls sideways when it runs out of room. Built on the vendored shadcn table.

Playground

Bookings and revenue per stylist, this week
StylistBookingsRevenue
Anna Kováčová14€742.00
Marek Horváth19€542.00
Total33€1,284.00
Table
Parts

Framed

The default: the table brings its own glass squircle surface, as on the services page. Controls in a row stay small. Use an xs button and a small switch, so every row keeps one height. Toggle a service.

ServiceDurationBufferPriceActiveActions
Men's haircut30 min5 min€22.00
Haircut & beard45 min10 min€32.00
Beard trim20 min5 min€15.00
Balayage150 min15 min€120.00
Kids' haircutInactive20 min5 min€14.00

Plain, in a card

When a table needs a title, put it in a Card as a direct child, not inside CardContent, and make it plain. Rows then run edge to edge, and the cells' 16px inset lines up with the card's header.

Today
Tuesday 13 October · 4 bookings
TimeClientServiceCode
09:00Martin KováčMen's haircutOK-4F2K
10:00Petra NovákováBalayageOK-7Q9D
11:30Jana HorváthováWomen's haircutOK-M3XH
13:00Tomáš BalážHaircut & beardOK-P8RC

A row that opens

Clients — a row opens them
NameBookingsReminders
14
19

Click anywhere on a row.

API reference

PropTypeDefaultDescription
variant"framed" | "plain""framed"Table only. Framed stands on its own glass squircle surface; plain has none, for a table placed directly in a Card.
numericbooleanfalseTableHead and TableCell. Right-aligned, in tabular figures, for durations, prices and counts. Set it on the header too.
onOpen() => void—TableRow only. The row opens what it is from a click anywhere on it, with a pointer and a hover wash. Its own buttons, links, switches and menus keep their clicks; keep a button in the row (the name, or Edit) for a keyboard.
classNamestring—Layout on any part: a min width on Table, alignment or weight on a cell, a muted row. Never restyle the surface or the rules.

The parts are the table's own elements: TableCaption, TableHeader, TableBody, TableFooter (a porcelain band for totals), TableRow, TableHead and TableCell. Each passes everything else through to its element.

Guidelines

  • Use a table when rows are compared across columns. A list of things each read on its own, like a day's bookings on a phone, is a list, not a table.
  • Numbers are numeric: right-aligned in tabular figures, header included, so digits line up down the column. Put the unit in the cell (“45 min”, “€32.00”) rather than the header.
  • A row that opens something — a client, a member, a service — opens from anywhere on it: give it onOpen (Jozef, 2026-10-23). Other rows don't light up on hover. Any other action goes in its own cell as an xs button; name icon-only ones.
  • Give every table a name: a TableCaption, or the title of the card it sits in, linked with aria-labelledby. Hidden headers, like an actions column, get sr-only text.
  • Show an inactive row muted and badged, never faded. Faded rows fade their controls too, and read as disabled.