[Feature] Table: match shadcn (Base UI + vega)
Brings Table in line with shadcn's Base UI table (vega style).
The audit on main found a broken footer and several class differences:
TableFooterhad 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.TableHeadusedtext-muted-foreground. vega usestext-foreground.TableHeadandTableCelllackedwhitespace-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.TableHeadandTableCellcarried[&>[role=checkbox]]:translate-y-[2px], which vega doesn't have.TableRowlackedhas-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
| Part | Now |
|---|---|
Table | wrapper div is data-slot="table-container" with overflow-x-auto (same computed overflow as before); the <table> is data-slot="table" |
TableHeader, TableBody, TableCaption | same classes, plus data-slot |
TableFooter | border-t bg-muted/50 font-medium [&>tr]:last:border-b-0, data-slot="table-footer" |
TableRow | adds has-aria-expanded:bg-muted/50, data-slot="table-row" |
TableHead | text-foreground whitespace-nowrap, no translate, checkbox padding rule, data-slot="table-head" |
TableCell | whitespace-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.



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 `&`, 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:126putsDataTableSelectAllCheckboxstraight inTableHead, while the body cells wrap theirs inflex justify-center. So the header checkbox sits on the left and the row checkboxes are centered, as they already were onmain.
Test steps
cd gem && bundle exec rake(tests + standardrb).cd docs && bin/rails db:test:prepare test.cd mcp && bundle exec rake; the registry is up to date.bin/devand 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