Table

A responsive, accessible table. Wraps native <table> semantics with shadcn styling.

Installation

bin/rails g shadcnrb:component table

Usage

Compose sui.table do |t| with t.header, t.body, t.row, t.head, t.cell. Wrap in a card for the bordered look, or leave plain.

Invoice Status Method Amount
INV001 Paid Credit Card $250.00
INV002 Pending PayPal $150.00
INV003 Unpaid Bank Transfer $350.00
<%= sui.table class: "rounded-lg border" do |t| %>
  <%= t.header do %>
    <%= t.row do %>
      <%= t.head "Invoice" %>
      <%= t.head "Status" %>
      <%= t.head "Method" %>
      <%= t.head "Amount", class: "text-right" %>
    <% end %>
  <% end %>
  <%= t.body do %>
    <% [
      {id: "INV001", status: "Paid",    method: "Credit Card",    amount: "$250.00"},
      {id: "INV002", status: "Pending", method: "PayPal",         amount: "$150.00"},
      {id: "INV003", status: "Unpaid",  method: "Bank Transfer",  amount: "$350.00"}
    ].each do |inv| %>
      <%= t.row do %>
        <%= t.cell inv[:id], class: "font-medium" %>
        <%= t.cell do %>
          <%= sui.badge inv[:status], variant: (inv[:status] == "Paid" ? :default : :secondary) %>
        <% end %>
        <%= t.cell inv[:method] %>
        <%= t.cell inv[:amount], class: "text-right" %>
      <% end %>
    <% end %>
  <% end %>
<% end %>

With caption and footer

Invoice Amount
INV001$250.00
INV002$150.00
Total $400.00

A list of your recent invoices.

<%= sui.table class: "rounded-lg border" do |t| %>
  <%= t.caption "A list of your recent invoices.", class: "text-center" %>
  <%= t.header do %>
    <%= t.row do %>
      <%= t.head "Invoice" %>
      <%= t.head "Amount", class: "text-right" %>
    <% end %>
  <% end %>
  <%= t.body do %>
    <%= t.row { t.cell("INV001") + t.cell("$250.00", class: "text-right") } %>
    <%= t.row { t.cell("INV002") + t.cell("$150.00", class: "text-right") } %>
  <% end %>
  <%= t.footer do %>
    <%= t.row do %>
      <%= t.cell "Total" %>
      <%= t.cell "$400.00", class: "text-right" %>
    <% end %>
  <% end %>
<% end %>

Lazy-loaded rows

Rows that arrive after the page: the body starts with a single colspan placeholder row, and a Turbo Frame beside the table fetches a stream that replaces the tbody. A turbo-frame can't sit inside a table, which is why the frame stays outside and carries a turbo-stream. The fixed header keeps its natural layout while the placeholder shows — widths are only measured against a row with one cell per heading — and realigns when the rows land. The partial reaches t.body and friends through sui.table_proxy.

Invoice Status Method Amount
Loading…
<%= sui.table fixed_header: true, class: "h-64 rounded-lg border" do |t| %>
  <%= t.header do %>
    <%= t.row do %>
      <%= t.head "Invoice" %>
      <%= t.head "Status" %>
      <%= t.head "Method" %>
      <%= t.head "Amount", class: "text-right" %>
    <% end %>
  <% end %>
  <%= t.body id: "lazy-invoices" do %>
    <%= t.row do %>
      <%= t.cell "Loading…", colspan: 4, class: "text-center text-muted-foreground animate-pulse" %>
    <% end %>
  <% end %>
<% end %>
<turbo-frame id="lazy-invoices-frame" src="<%= lazy_table_rows_path %>"></turbo-frame>

Fixed header

fixed_header: true keeps the header in place while the rows scroll inside a bounded box. The header and body render as two tables — the header outside the scroll container — and a small Stimulus controller keeps their column widths and horizontal scroll in step, so the scrollbar sits beside the rows only. Give the box a height.

Invoice Status Method Amount
INV001 Paid Credit Card $50.00
INV002 Pending Credit Card $100.00
INV003 Paid Bank Transfer $150.00
INV004 Pending Credit Card $200.00
INV005 Paid Credit Card $250.00
INV006 Pending Bank Transfer $300.00
INV007 Paid Credit Card $350.00
INV008 Pending Credit Card $400.00
INV009 Paid Bank Transfer $450.00
INV010 Pending Credit Card $500.00
INV011 Paid Credit Card $550.00
INV012 Pending Bank Transfer $600.00
INV013 Paid Credit Card $650.00
INV014 Pending Credit Card $700.00
INV015 Paid Bank Transfer $750.00
INV016 Pending Credit Card $800.00
INV017 Paid Credit Card $850.00
INV018 Pending Bank Transfer $900.00
INV019 Paid Credit Card $950.00
INV020 Pending Credit Card $1000.00
<%= sui.table fixed_header: true, class: "h-64 rounded-lg border" do |t| %>
  <%= t.header do %>
    <%= t.row do %>
      <%= t.head "Invoice" %>
      <%= t.head "Status" %>
      <%= t.head "Method" %>
      <%= t.head "Amount", class: "text-right" %>
    <% end %>
  <% end %>
  <%= t.body do %>
    <% 1.upto(20) do |i| %>
      <%= t.row do %>
        <%= t.cell "INV#{i.to_s.rjust(3, '0')}", class: "font-medium" %>
        <%= t.cell i.odd? ? "Paid" : "Pending" %>
        <%= t.cell i % 3 == 0 ? "Bank Transfer" : "Credit Card" %>
        <%= t.cell "$#{i * 50}.00", class: "text-right" %>
      <% end %>
    <% end %>
  <% end %>
<% end %>

Sizes

size: :lg relaxes row height and padding for index-style lists; data grids keep the dense default. The size is set as data-size on the table and applied through descendant selectors, so t.head and t.cell stay size-agnostic.

Name Role
Ada LovelaceEngineer
Grace HopperAdmiral
<%= sui.table size: :lg, class: "rounded-lg border" do |t| %>
  <%= t.header do %>
    <%= t.row do %>
      <%= t.head "Name" %>
      <%= t.head "Role" %>
    <% end %>
  <% end %>
  <%= t.body do %>
    <%= t.row { t.cell("Ada Lovelace", class: "font-medium") + t.cell("Engineer") } %>
    <%= t.row { t.cell("Grace Hopper", class: "font-medium") + t.cell("Admiral") } %>
  <% end %>
<% end %>

API

MethodRendersNotes
sui.table<table>Wrapped in an overflow-x-auto div that class: lands on: give it a border, or a height for a fixed header; yields a t proxy. fixed_header: true splits header and body into two tables kept in step by shadcnrb--table--component; size: is :default or :lg.
t.header / body / footer<thead> / <tbody> / <tfoot>Footer gets a top border + muted bg. The header part (thead, or the header container in fixed mode) is data-slot="table-header" and draws the header rule; the body part is table-body.
t.row<tr>Hover highlight, supports data-state="selected".
t.head<th>Muted foreground, left-aligned by default.
t.cell<td>First arg or block for content.
t.caption<p>Renders below the wrapper, outside any border, linked to the table by aria-describedby.