Table
A responsive table component.
Usage
With badges
| Invoice | Status | Amount |
|---|---|---|
| INV001 | Paid | $250.00 |
| INV002 | Pending | $150.00 |
| INV003 | Unpaid | $350.00 |
Table do
TableHeader do
TableRow do
TableHead { "Invoice" }
TableHead { "Status" }
TableHead(class: "text-right") { "Amount" }
end
end
TableBody do
invoices.each do |invoice|
TableRow do
TableCell(class: "font-medium") { invoice.identifier }
TableCell { render_status_badge(invoice.status) }
TableCell(class: "text-right") { format_amount(invoice.amount) }
end
end
end
end
With actions
A row stays highlighted while a menu inside it is open (anything in it with aria-expanded=true).
| Product | Price | Actions |
|---|---|---|
| Wireless Mouse | $29.99 | |
| Mechanical Keyboard | $129.99 | |
| USB-C Hub | $49.99 |
Table do
TableHeader do
TableRow do
TableHead { "Product" }
TableHead { "Price" }
TableHead(class: "text-right") { "Actions" }
end
end
TableBody do
[["Wireless Mouse", "$29.99"], ["Mechanical Keyboard", "$129.99"], ["USB-C Hub", "$49.99"]].each do |product, price|
TableRow do
TableCell(class: "font-medium") { product }
TableCell { price }
TableCell(class: "text-right") do
DropdownMenu do
DropdownMenuTrigger do
Button(variant: :ghost, size: :icon, class: "size-8") do
more_icon
span(class: "sr-only") { "Open menu" }
end
end
DropdownMenuContent(align: :end) do
DropdownMenuItem { "Edit" }
DropdownMenuItem { "Duplicate" }
DropdownMenuSeparator()
DropdownMenuItem(variant: :destructive) { "Delete" }
end
end
end
end
end
end
end
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component Table
Manual installation
1
Add RubyUI::Table to app/components/ruby_ui/table/table.rb
# frozen_string_literal: true
module RubyUI
# A native <table> inside a horizontally scrolling container, like shadcn's
# Table: wide tables scroll instead of wrapping their cells. Attributes go
# to the <table>; the wrapper is data-slot="table-container".
class Table < Base
include TableSlot
def view_template(&block)
div(data: {slot: "table-container"}, class: "relative w-full overflow-x-auto") do
table(**attrs, &block)
end
end
private
def default_attrs
{
data: {slot: default_slot("table")}.compact,
class: "w-full caption-bottom text-sm"
}
end
end
end
2
Add RubyUI::TableBody to app/components/ruby_ui/table/table_body.rb
# frozen_string_literal: true
module RubyUI
class TableBody < Base
include TableSlot
def view_template(&)
tbody(**attrs, &)
end
private
def default_attrs
{
data: {slot: default_slot("table-body")}.compact,
class: "[&_tr:last-child]:border-0"
}
end
end
end
3
Add RubyUI::TableCaption to app/components/ruby_ui/table/table_caption.rb
# frozen_string_literal: true
module RubyUI
class TableCaption < Base
include TableSlot
def view_template(&)
caption(**attrs, &)
end
private
def default_attrs
{
data: {slot: default_slot("table-caption")}.compact,
class: "mt-4 text-sm text-muted-foreground"
}
end
end
end
4
Add RubyUI::TableCell to app/components/ruby_ui/table/table_cell.rb
# frozen_string_literal: true
module RubyUI
class TableCell < Base
include TableSlot
def view_template(&)
td(**attrs, &)
end
private
def default_attrs
{
data: {slot: default_slot("table-cell")}.compact,
class: "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&:has([data-slot=checkbox])]:pr-0"
}
end
end
end
5
Add RubyUI::TableDocs to app/components/ruby_ui/table/table_docs.rb
# frozen_string_literal: true
class Views::Docs::Table < Views::Base
Invoice = Struct.new(:identifier, :status, :method, :amount)
def view_template
component = "Table"
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Table", description: "A responsive table component.")
Heading(level: 2) { "Usage" }
render Docs::VisualCodeExample.new(title: "With footer", description: "Cells don't wrap: when the table is wider than its container, the container scrolls horizontally.", context: self) do
<<~RUBY
Table do
TableCaption { "A list of your recent invoices." }
TableHeader do
TableRow do
TableHead(class: "w-[100px]") { "Invoice" }
TableHead { "Status" }
TableHead { "Method" }
TableHead(class: "text-right") { "Amount" }
end
end
TableBody do
invoices.each do |invoice|
TableRow do
TableCell(class: "font-medium") { invoice.identifier }
TableCell { invoice.status }
TableCell { invoice.method }
TableCell(class: "text-right") { format_amount(invoice.amount) }
end
end
end
TableFooter do
TableRow do
TableCell(colspan: 3) { "Total" }
TableCell(class: "text-right") { format_amount(invoices.sum(&:amount)) }
end
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "With badges", context: self) do
<<~RUBY
Table do
TableHeader do
TableRow do
TableHead { "Invoice" }
TableHead { "Status" }
TableHead(class: "text-right") { "Amount" }
end
end
TableBody do
invoices.each do |invoice|
TableRow do
TableCell(class: "font-medium") { invoice.identifier }
TableCell { render_status_badge(invoice.status) }
TableCell(class: "text-right") { format_amount(invoice.amount) }
end
end
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "With actions", description: "A row stays highlighted while a menu inside it is open (anything in it with aria-expanded=true).", context: self) do
<<~RUBY
Table do
TableHeader do
TableRow do
TableHead { "Product" }
TableHead { "Price" }
TableHead(class: "text-right") { "Actions" }
end
end
TableBody do
[["Wireless Mouse", "$29.99"], ["Mechanical Keyboard", "$129.99"], ["USB-C Hub", "$49.99"]].each do |product, price|
TableRow do
TableCell(class: "font-medium") { product }
TableCell { price }
TableCell(class: "text-right") do
DropdownMenu do
DropdownMenuTrigger do
Button(variant: :ghost, size: :icon, class: "size-8") do
more_icon
span(class: "sr-only") { "Open menu" }
end
end
DropdownMenuContent(align: :end) do
DropdownMenuItem { "Edit" }
DropdownMenuItem { "Duplicate" }
DropdownMenuSeparator()
DropdownMenuItem(variant: :destructive) { "Delete" }
end
end
end
end
end
end
end
RUBY
end
render Components::ComponentSetup::Tabs.new(component_name: component)
render Docs::ComponentsTable.new(component_files(component))
end
end
private
def invoices
[
Invoice.new(identifier: "INV001", status: "Paid", method: "Credit Card", amount: 250),
Invoice.new(identifier: "INV002", status: "Pending", method: "PayPal", amount: 150),
Invoice.new(identifier: "INV003", status: "Unpaid", method: "Bank Transfer", amount: 350)
]
end
def format_amount(amount)
"$#{amount}.00"
end
def render_status_badge(status)
case status
when "Paid"
Badge(size: :sm, class: "bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300") { status }
when "Pending"
Badge(size: :sm, class: "bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300") { status }
else
Badge(variant: :destructive, size: :sm) { status }
end
end
def more_icon
svg(xmlns: "http://www.w3.org/2000/svg", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor",
stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", aria_hidden: "true") do |s|
s.circle(cx: "12", cy: "12", r: "1")
s.circle(cx: "19", cy: "12", r: "1")
s.circle(cx: "5", cy: "12", r: "1")
end
end
end
6
Add RubyUI::TableFooter to app/components/ruby_ui/table/table_footer.rb
# frozen_string_literal: true
module RubyUI
class TableFooter < Base
include TableSlot
def view_template(&)
tfoot(**attrs, &)
end
private
def default_attrs
{
data: {slot: default_slot("table-footer")}.compact,
class: "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0"
}
end
end
end
7
Add RubyUI::TableHead to app/components/ruby_ui/table/table_head.rb
# frozen_string_literal: true
module RubyUI
class TableHead < Base
include TableSlot
def view_template(&)
th(**attrs, &)
end
private
def default_attrs
{
data: {slot: default_slot("table-head")}.compact,
class: "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&:has([data-slot=checkbox])]:pr-0"
}
end
end
end
8
Add RubyUI::TableHeader to app/components/ruby_ui/table/table_header.rb
# frozen_string_literal: true
module RubyUI
class TableHeader < Base
include TableSlot
def view_template(&)
thead(**attrs, &)
end
private
def default_attrs
{
data: {slot: default_slot("table-header")}.compact,
class: "[&_tr]:border-b"
}
end
end
end
9
Add RubyUI::TableRow to app/components/ruby_ui/table/table_row.rb
# frozen_string_literal: true
module RubyUI
class TableRow < Base
include TableSlot
def view_template(&)
tr(**attrs, &)
end
private
def default_attrs
{
data: {slot: default_slot("table-row")}.compact,
class: "border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted"
}
end
end
end
10
Add RubyUI::TableSlot to app/components/ruby_ui/table/table_slot.rb
# frozen_string_literal: true
module RubyUI
# shadcn spreads props after data-slot, so a caller's slot replaces a Table
# part's default instead of Base#mix joining both values (as in Input).
module TableSlot
SLOT_ATTRS = [:data_slot, :"data-slot", "data-slot", "data_slot"].freeze
SLOT_KEYS = [:slot, :slot!, "slot", "slot!"].freeze
def initialize(**attrs)
@caller_slot = caller_slot?(attrs)
super
end
private
def caller_slot?(attrs)
return true if attrs.key?(:data!) || SLOT_ATTRS.any? { |key| attrs.key?(key) }
data = attrs[:data] || attrs["data"]
data.is_a?(Hash) && SLOT_KEYS.any? { |key| data.key?(key) }
end
# The part's own slot, or nil when the caller passed one.
def default_slot(name)
@caller_slot ? nil : name
end
end
end
Components
| Component | Built using | Source |
|---|---|---|
Table | Phlex | |
TableBody | Phlex | |
TableCaption | Phlex | |
TableCell | Phlex | |
TableDocs | Phlex | |
TableFooter | Phlex | |
TableHead | Phlex | |
TableHeader | Phlex | |
TableRow | Phlex | |
TableSlot | Phlex |