Table
A semantic table for rows and columns of data.
Installation
Included in poetry-ui — available as
poetry_table
the moment you've installed Poetry,
with no per-component step. To own the source and edit it, copy it into your app:
bin/rails g poetry:add table
Default
Open Default standalone
| Invoice | Status | Amount |
|---|---|---|
| INV001 | Paid | $250.00 |
| INV002 | Pending | $150.00 |
| INV003 | Unpaid | $350.00 |
| INV004 | Paid | $500.00 |
| Total | $1,250.00 | |
<% invoices = [
{ number: "INV001", status: "Paid", amount: "$250.00", selected: true },
{ number: "INV002", status: "Pending", amount: "$150.00" },
{ number: "INV003", status: "Unpaid", amount: "$350.00" },
{ number: "INV004", status: "Paid", amount: "$500.00" }
] %>
<div class="w-full max-w-2xl">
<%= poetry_table do %>
<%= poetry_table_caption { "A list of your recent invoices." } %>
<%= poetry_table_header do %>
<%= poetry_table_row do %>
<%= poetry_table_head { "Invoice" } %>
<%= poetry_table_head { "Status" } %>
<%= poetry_table_head(class: "text-right") { "Amount" } %>
<% end %>
<% end %>
<%= poetry_table_body do %>
<% invoices.each do |invoice| %>
<%= poetry_table_row(data: invoice[:selected] ? { selected: "" } : {}) do %>
<%= poetry_table_cell(class: "font-medium") { invoice[:number] } %>
<%= poetry_table_cell { invoice[:status] } %>
<%= poetry_table_cell(class: "text-right") { invoice[:amount] } %>
<% end %>
<% end %>
<% end %>
<%= poetry_table_footer do %>
<%= poetry_table_row do %>
<%= poetry_table_cell(colspan: 2) { "Total" } %>
<%= poetry_table_cell(class: "text-right") { "$1,250.00" } %>
<% end %>
<% end %>
<% end %>
</div>
Actions
Open Actions standalone
| Project | Environment | Status | Actions |
|---|---|---|---|
| Marketing site | Production | Live |
|
| Billing API | Staging | Building |
|
| Docs portal | Preview | Queued |
|
| Mobile gateway | Production | Live |
|
<%# A trailing actions column: each row's controls live in a dropdown menu
behind a compact ellipsis trigger. The last header is labelled for
screen readers only so the column still has an accessible name. %>
<% deployments = [
{ project: "Marketing site", environment: "Production", status: "Live" },
{ project: "Billing API", environment: "Staging", status: "Building" },
{ project: "Docs portal", environment: "Preview", status: "Queued" },
{ project: "Mobile gateway", environment: "Production", status: "Live" }
] %>
<div class="w-full max-w-2xl">
<%= poetry_table do %>
<%= poetry_table_caption { "Recent deployments across your environments." } %>
<%= poetry_table_header do %>
<%= poetry_table_row do %>
<%= poetry_table_head { "Project" } %>
<%= poetry_table_head { "Environment" } %>
<%= poetry_table_head { "Status" } %>
<%= poetry_table_head(class: "w-12") do %>
<span class="sr-only">Actions</span>
<% end %>
<% end %>
<% end %>
<%= poetry_table_body do %>
<% deployments.each do |deployment| %>
<%= poetry_table_row do %>
<%= poetry_table_cell(class: "font-medium") { deployment[:project] } %>
<%= poetry_table_cell { deployment[:environment] } %>
<%= poetry_table_cell { deployment[:status] } %>
<%= poetry_table_cell(class: "text-right") do %>
<%= poetry_dropdown_menu(align: :end) do |menu| %>
<% menu.with_trigger(variant: :ghost, size: :icon, label: "Open actions for #{deployment[:project]}") do %>
<%= poetry_icon(name: :ellipsis) %>
<% end %>
<% menu.with_label { "Actions" } %>
<% menu.with_group do |group| %>
<% group.with_item do %><%= poetry_icon(name: :user) %>View owner<% end %>
<% group.with_item(shortcut: "⌘C") do %><%= poetry_icon(name: :copy) %>Copy deploy ID<% end %>
<% end %>
<% menu.with_separator %>
<% menu.with_item(variant: :destructive) do %><%= poetry_icon(name: :"trash-2") %>Roll back<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
Footer
Open Footer standalone
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit Card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank Transfer | $350.00 |
| Total | $750.00 | ||
<%# Upstream's table-footer demo: the tfoot anatomy in isolation - a
colspan Total row closing the ledger. Upstream shows $2,500.00 (its
full seven-invoice total) over three sliced rows; the figure here sums
the rows actually shown. %>
<% invoices = [
{ number: "INV001", status: "Paid", method: "Credit Card", amount: "$250.00" },
{ number: "INV002", status: "Pending", method: "PayPal", amount: "$150.00" },
{ number: "INV003", status: "Unpaid", method: "Bank Transfer", amount: "$350.00" }
] %>
<div class="w-full max-w-2xl">
<%= poetry_table do %>
<%= poetry_table_caption { "A list of your recent invoices." } %>
<%= poetry_table_header do %>
<%= poetry_table_row do %>
<%= poetry_table_head(class: "w-[100px]") { "Invoice" } %>
<%= poetry_table_head { "Status" } %>
<%= poetry_table_head { "Method" } %>
<%= poetry_table_head(class: "text-right") { "Amount" } %>
<% end %>
<% end %>
<%= poetry_table_body do %>
<% invoices.each do |invoice| %>
<%= poetry_table_row do %>
<%= poetry_table_cell(class: "font-medium") { invoice[:number] } %>
<%= poetry_table_cell { invoice[:status] } %>
<%= poetry_table_cell { invoice[:method] } %>
<%= poetry_table_cell(class: "text-right") { invoice[:amount] } %>
<% end %>
<% end %>
<% end %>
<%= poetry_table_footer do %>
<%= poetry_table_row do %>
<%= poetry_table_cell(colspan: 3) { "Total" } %>
<%= poetry_table_cell(class: "text-right") { "$750.00" } %>
<% end %>
<% end %>
<% end %>
</div>
Minimal
Open Minimal standalone
| Name | Role | |
|---|---|---|
| Ada Lovelace | ada@example.com | Owner |
| Grace Hopper | grace@example.com | Admin |
| Alan Turing | alan@example.com | Member |
<% members = [
["Ada Lovelace", "ada@example.com", "Owner"],
["Grace Hopper", "grace@example.com", "Admin"],
["Alan Turing", "alan@example.com", "Member"]
] %>
<div class="w-full max-w-2xl">
<%= poetry_table do %>
<%= poetry_table_header do %>
<%= poetry_table_row do %>
<%= poetry_table_head { "Name" } %>
<%= poetry_table_head { "Email" } %>
<%= poetry_table_head { "Role" } %>
<% end %>
<% end %>
<%= poetry_table_body do %>
<% members.each do |name, email, role| %>
<%= poetry_table_row do %>
<%= poetry_table_cell(class: "font-medium") { name } %>
<%= poetry_table_cell { email } %>
<%= poetry_table_cell { role } %>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
Sticky
Open Sticky standalone
| Invoice | Status | Amount |
|---|---|---|
| INV001 | Pending | $150.00 |
| INV002 | Unpaid | $250.00 |
| INV003 | Paid | $350.00 |
| INV004 | Pending | $450.00 |
| INV005 | Unpaid | $550.00 |
| INV006 | Paid | $650.00 |
| INV007 | Pending | $750.00 |
| INV008 | Unpaid | $850.00 |
| INV009 | Paid | $950.00 |
| INV010 | Pending | $1050.00 |
| INV011 | Unpaid | $1150.00 |
| INV012 | Paid | $1250.00 |
<%= poetry_table(sticky_header: true, container_class: "max-h-64", scroll_label: "Recent invoices") do %>
<%= poetry_table_header do %>
<%= poetry_table_row do %>
<%= poetry_table_head { "Invoice" } %>
<%= poetry_table_head { "Status" } %>
<%= poetry_table_head(class: "text-right") { "Amount" } %>
<% end %>
<% end %>
<%= poetry_table_body do %>
<% 1.upto(12) do |i| %>
<%= poetry_table_row do %>
<%= poetry_table_cell(class: "font-medium") { format("INV%03d", i) } %>
<%= poetry_table_cell { %w[Paid Pending Unpaid][i % 3] } %>
<%= poetry_table_cell(class: "text-right") { format("$%d50.00", i) } %>
<% end %>
<% end %>
<% end %>
<% end %>
API
Poetry::Ui::Table::Component — options are
constructor keywords (the poetry_* helper forwards them);
slots are composed inside the block. Generated from the gem's source documentation.
| Option | Type | Details | Description |
|---|---|---|---|
| container_class: | String | Extra classes for the scroll container - e.g. \"max-h-96\" to cap its height. | |
| scroll_label: | String | The scroll region's accessible name, required with sticky_header: a scrollable region a keyboard can't reach fails WCAG (axe scrollable-region-focusable), and a focusable region needs a name. | |
| sticky_header: | Boolean | defaults to false |
Pins the <thead> while the container scrolls; needs a height cap (container_class:) to take effect, and requires scroll_label:. |
Styling
Every part carries a stable data-slot attribute — target
[data-slot=…] from your own CSS to restyle it. State rides
data attributes on the parts below. This contract is verified against rendered DOM in CI.
| Part | Description |
|---|---|
| [data-slot=table] | The semantic <table> element itself - the root the part helpers compose into |
| [data-slot=table-caption] | The <caption> (poetry_table_caption) - the table's accessible purpose |
| [data-slot=table-header] | The <thead> (poetry_table_header) holding the column-header row |
| [data-slot=table-body] | The <tbody> (poetry_table_body) holding the data rows |
| [data-slot=table-footer] | The <tfoot> (poetry_table_footer) - totals/summary rows |
| [data-slot=table-row] | A <tr> (poetry_table_row) in any section |
| [data-slot=table-head] | A column header <th> (poetry_table_head) |
| [data-slot=table-cell] | A data <td> (poetry_table_cell) |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| table-row | data-selected | the row is marked selected (presence attribute, no value) - the theme tints it | — |