[Feature] Table: match shadcn (Base UI + vega)

Merged
CI
2d ago
+485-106

Brings Table in line with shadcn's Base UI table (vega style).

The audit on main found a broken footer and several class differences:

  • TableFooter had one malformed class token, font-medium[& amp;>tr]:last:border-b-0 (an escaped & and a missing space). Tailwind generated neither rule, so the footer text wasn't medium weight and the last footer row kept its bottom border.
  • TableHead used text-muted-foreground. vega uses text-foreground.
  • TableHead and TableCell lacked whitespace-nowrap. In a 300px container RubyUI wrapped "Credit Card" and "Bank Transfer" onto two lines (table 264px tall), while shadcn keeps each cell on one line and scrolls the container sideways.
  • TableHead and TableCell carried [&>[role=checkbox]]:translate-y-[2px], which vega doesn't have.
  • TableRow lacked has-aria-expanded:bg-muted/50, so a row didn't stay highlighted while a menu inside it was open.
  • No part had a data-slot.

Cells no longer wrap

TableHead and TableCell are now whitespace-nowrap, as in shadcn. A table wider than its container scrolls horizontally inside it instead of wrapping its text. For a column that should wrap (long descriptions, for example), pass class: "whitespace-normal" to that cell.

What changed

PartNow
Tablewrapper div is data-slot="table-container" with overflow-x-auto (same computed overflow as before); the <table> is data-slot="table"
TableHeader, TableBody, TableCaptionsame classes, plus data-slot
TableFooterborder-t bg-muted/50 font-medium [&>tr]:last:border-b-0, data-slot="table-footer"
TableRowadds has-aria-expanded:bg-muted/50, data-slot="table-row"
TableHeadtext-foreground whitespace-nowrap, no translate, checkbox padding rule, data-slot="table-head"
TableCellwhitespace-nowrap, no translate, checkbox padding rule, data-slot="table-cell"

The translate never applied to RubyUI's own checkbox: it targets a direct child with a role="checkbox" attribute, and RubyUI's checkbox is a native <input> (inside a div in the DataTable examples). I checked the DataTable docs in the browser after the change: the checkboxes sit centered in their rows (within 1px) with no transform.

The docs now follow shadcn's examples: with footer (the footer uses TableCell, as shadcn's does, instead of TableHead), with badges, and with actions, where the row stays highlighted while its dropdown menu is open. The component had no audit map or fixtures, so this PR adds audit/component_map/table.yml (with the drift baseline) and audit/fixtures/table/. The MCP registry is rebuilt.

A caller's data-slot replaces a part's default instead of being joined to it (Table(data: {slot: "x"}) renders data-slot="x"), the same rule as Input. The parts share that logic through a TableSlot module. TableHead and TableCell also drop their right padding when they hold RubyUI's checkbox ([&:has([data-slot=checkbox])]:pr-0, vega's rule for the aria base): the role=checkbox rule alone never matched RubyUI's native input. Codex and Cubic raised both points during review.

Screenshots

Audit fixture (shadcn's "With Footer" example plus a selected row), 1280×800, same strings on both sides.

Closed (light)

Closed (dark)

300px container (light)

Audit on main
shadcn audit  ruby-ui/ruby_ui
  base 1998c5e8  head 1998c5e8  shadcn 8a353667
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (light)
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (dark)
✅ Theme  neutral tokens match shadcn
Table  (ref: bases/base/ui/table.tsx + style-vega.css @ 8a353667)
  ❌ Look       border-bottom-width: 1px
               vega: border-bottom-width: 0px
               TableRow — closed/light, closed/dark. Only the footer's row (index 4). Root cause is TableFooter: its class string `font-medium[& amp;>tr]:last:border-b-0` is one malformed token (HTML-escaped `&` and no space), so Tailwind generates neither `font-medium` nor `[&>tr]:last:border-b-0` (style-vega.css:1284). The footer row keeps TableRow's `border-b`. Requested: fix TableFooter to `bg-muted/50 border-t font-medium [&>tr]:last:border-b-0`. Requested: run `baseline table 8a3536678a528cc012b9e62484290e79d0be410f` in this PR and commit audit/component_map/table.yml + audit/fixtures/table/ (created locally for this audit).
               ↳ computed styles (shadcn-audit collect)
  ❌ Look       color: oklch(0.556 0 0); color: oklch(0.708 0 0)
               vega: color: oklch(0.145 0 0); color: oklch(0.985 0 0)
               TableHead — closed/light, closed/dark. RubyUI `text-muted-foreground` vs vega `text-foreground` (style-vega.css:1296). Requested: use `text-foreground` (closes the TableHead color item deferred by the DataTable audit).
               ↳ computed styles (shadcn-audit collect)
  ⏸ Look       properties: font-family, font-weight, height, width
               Table, TableContainer, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption — explained by environment diff: font. Checked per element: font-family differs only in the fallback list (both render Inter Variable). The footer's font-weight 400 vs 500 and height 37px vs 36.5px are real (TableFooter line below); the column widths (±2px) follow from the footer's weight on `$2,500.00`. Kept as is: environment only after the TableFooter fix.
  ✅ ARIA       Table, TableContainer, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption: accessibility tree matches
               ↳ accessibility tree (shadcn-audit collect)
  ❌ Look       TableFooter class `border-t bg-muted/50 font-medium[& amp;>tr]:last:border-b-0` → font-weight 400, last row border-bottom 1px
               vega: `bg-muted/50 border-t font-medium [&>tr]:last:border-b-0` → font-weight 500, last row border-bottom 0
               Malformed token in gem/lib/ruby_ui/table/table_footer.rb (escaped `&amp;`, missing space). Verified with getComputedStyle on the footer cells (closed/light and dark). Requested: fix TableFooter to `bg-muted/50 border-t font-medium [&>tr]:last:border-b-0`.
               ↳ style-vega.css:1284
  ❌ Look       TableHead/TableCell: no `whitespace-nowrap` (white-space: normal)
               vega: `whitespace-nowrap` on table-head and table-cell (white-space: nowrap)
               Also style-vega.css:1304. Same at 1280px, but in a 300px container RubyUI wraps the cells (table 264.5px tall, no scroll) while shadcn keeps one line and scrolls the container horizontally (scrollWidth 331 > 300). Screens table-narrow-*. Requested: add `whitespace-nowrap` to TableHead and TableCell; mention in the changelog.
               ↳ style-vega.css:1296
  ❌ Look       TableHead/TableCell: extra `[&>[role=checkbox]]:translate-y-[2px]`
               vega: only `[&:has([role=checkbox])]:pr-0`, no translate
               Also style-vega.css:1304. Not exercised by the fixture (no checkbox in a cell); graded from the class strings. Requested: remove the translate; check checkbox alignment in DataTable afterwards.
               ↳ style-vega.css:1296
  ❌ Look       TableRow: no `has-aria-expanded:bg-muted/50`
               vega: TableRow `has-aria-expanded:bg-muted/50` (row with an open disclosure/menu inside gets the hover tint)
               Not exercised by the fixture (no aria-expanded control in a row); graded from the class strings. Requested: add `has-aria-expanded:bg-muted/50` to TableRow.
               ↳ bases/base/ui/table.tsx:47
  ✅ Look       TableContainer `relative w-full overflow-auto` → overflow-x auto, overflow-y auto
               Different token, same computed value: a non-visible overflow-x forces overflow-y from visible to auto. Confirmed with getComputedStyle on both sides.
               ↳ style-vega.css:1267
  ✅ Look       TableRow hover bg oklab(0.97 0 0 / 0.5), selected bg oklch(0.97 0 0), transition 150ms colors
               Hovered the first body row and read the selected (data-state=selected) row in the browser, light theme. Screens table-hover-*.
               ↳ style-vega.css:1288
  ✅ HTML       div > table > caption, thead > tr > th, tbody > tr > td, tfoot > tr > td
               Rendered DOM of both fixtures.
               ↳ bases/base/ui/table.tsx:8
  ❌ HTML       no data-slot on any part
               shadcn: data-slot table-container, table, table-caption, table-header, table-row, table-head, table-body, table-cell, table-footer
               Read from the sandbox DOM. Other matched components (Card, Sheet, Select…) already emit data-slot. Requested: add data-slot to every part (table-container on the wrapper div, table on <table>); switch the map selectors to [data-slot=…].
               ↳ bases/base/ui/table.tsx:8
  ✅ Props      every part forwards **attrs (class merged)
               Reference parts declare no props.
               ↳ bases/base/ui/table.tsx:6
  ✅ Native     native <table> elements, no JS
               Nothing hand-rolled on either side.
               ↳ https://developer.mozilla.org/pt-BR/docs/Web/HTML/Element/table
  ✅ Spec       same tree as shadcn
               Content model of <table> (optional caption first, then optional thead, tbodies, optional tfoot) met by shadcn's rendered DOM; RubyUI renders the same.
               ↳ https://html.spec.whatwg.org/multipage/tables.html#the-table-element
Audit on this branch
shadcn audit  ruby-ui/ruby_ui
  base 1998c5e8  head c88ef89a  shadcn 8a353667
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (light)
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (dark)
✅ Theme  neutral tokens match shadcn
Table  (ref: bases/base/ui/table.tsx + style-vega.css @ 8a353667)
  ⏸ Look       properties: font-family
               Table, TableContainer, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption — explained by environment diff: font Only the font fallback list differs now; weight, height and width match per element.
  ✅ ARIA       Table, TableContainer, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption: accessibility tree matches
               ↳ accessibility tree (shadcn-audit collect)
  ✅ Look       TableFooter `border-t bg-muted/50 font-medium [&>tr]:last:border-b-0` → font-weight 500, last row border-bottom 0
               Computed styles match per element (collect, light and dark).
               ↳ style-vega.css:1284
  ✅ Look       TableHead text-foreground; TableHead/TableCell whitespace-nowrap
               Also style-vega.css:1304. In a 300px container both tables are 331px wide and 223.5px tall and scroll horizontally. Screens table-narrow-*-now.
               ↳ style-vega.css:1296
  ✅ Look       TableHead/TableCell: only `[&:has([role=checkbox])]:pr-0`
               translate removed. DataTable checkboxes checked in the browser: centered within 1px, transform none (the old selector never matched RubyUI's native checkbox).
               ↳ style-vega.css:1304
  ✅ Look       TableRow `has-aria-expanded:bg-muted/50`
               Docs 'With actions': row bg rgba(0,0,0,0) → oklab(0.97 0 0 / 0.5) while its menu is open, back after Esc.
               ↳ bases/base/ui/table.tsx:47
  ✅ Look       TableContainer `relative w-full overflow-x-auto`
               Same token now.
               ↳ style-vega.css:1267
  ✅ Look       TableRow hover bg oklab(0.97 0 0 / 0.5), selected bg oklch(0.97 0 0)
               Hovered in the browser. Screens table-hover-*-now.
               ↳ style-vega.css:1288
  ✅ HTML       div > table > caption, thead > tr > th, tbody > tr > td, tfoot > tr > td
               Rendered DOM.
               ↳ bases/base/ui/table.tsx:8
  ✅ HTML       data-slot on all 9 parts
               Read from the rendered fixture; the map now selects by [data-slot].
               ↳ bases/base/ui/table.tsx:8
  ✅ Props      every part forwards **attrs (class merged)
               ↳ bases/base/ui/table.tsx:6
  ✅ Native     native <table>, no JS
               ↳ https://developer.mozilla.org/pt-BR/docs/Web/HTML/Element/table
  ✅ Spec       same tree as shadcn
               ↳ https://html.spec.whatwg.org/multipage/tables.html#the-table-element

Follow-ups

  • In the DataTable docs, one example in docs/app/views/docs/data_table.rb:126 puts DataTableSelectAllCheckbox straight in TableHead, while the body cells wrap theirs in flex justify-center. So the header checkbox sits on the left and the row checkboxes are centered, as they already were on main.

Test steps

  1. cd gem && bundle exec rake (tests + standardrb).
  2. cd docs && bin/rails db:test:prepare test.
  3. cd mcp && bundle exec rake; the registry is up to date.
  4. bin/dev and open /docs/table: the footer is medium weight with no bottom border; narrow the window and the tables scroll instead of wrapping; open a row's menu in "With actions" and the row stays highlighted.

🤖 Generated with Claude Code