Skip to content

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

Merged
cirdes merged 3 commits into
mainfrom
audit-table
Nov 4, 2026
Merged

cirdes merged 3 commits into
mainfrom
audit-table

Conversation

@cirdes

@cirdes cirdes commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

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

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.

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

cirdes and others added 2 commits November 4, 2026 11:56
Fix TableFooter's class string: it held one malformed token
(`font-medium[& amp;>tr]:last:border-b-0`), so the footer was not
font-medium and its last row kept a bottom border.

Match vega on the rest: TableHead uses text-foreground, TableHead and
TableCell get whitespace-nowrap (wide tables now scroll in their container
instead of wrapping cells), the unused [&>[role=checkbox]]:translate-y-[2px]
goes away, TableRow is highlighted while a control inside it is expanded,
and the container is overflow-x-auto. Every part now carries its shadcn
data-slot.

Adds the audit map (with the drift baseline) and fixtures, updates the docs
to shadcn's examples (with footer, badges, actions) and rebuilds the MCP
registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 16 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread gem/lib/ruby_ui/table/table.rb Resolved
Comment thread gem/lib/ruby_ui/table/table_docs.rb Resolved
Comment thread gem/lib/ruby_ui/table/table_cell.rb Resolved
- A caller's data-slot now replaces each part's default instead of being
  joined to it by Base#mix (shared TableSlot module, same rule as Input)
- TableHead and TableCell also drop the right padding for a cell holding
  RubyUI's native checkbox ([data-slot=checkbox]), which has no role attribute

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cirdes
cirdes merged commit 7119be0 into main Nov 4, 2026
10 checks passed
@cirdes
cirdes deleted the audit-table branch November 4, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant