Table
NewOverview
A responsive, stream-powered table with semantic headers, body rows, captions, and footers.
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit card | 25000 |
| INV002 | Pending | PayPal | 15000 |
| INV003 | Unpaid | Bank transfer | 35000 |
| INV004 | Paid | Credit card | 45000 |
| Total | $1,200.00 | ||
<Table.table container_class="rounded-xl border border-border">
<caption>A list of recent invoices.</caption>
<Table.table_head>
<:col class="w-28">Invoice</:col>
<:col>Status</:col>
<:col>Method</:col>
<:col class="text-right">Amount</:col>
</Table.table_head>
<Table.table_body id="invoices">
<Table.table_row :for={{dom_id, invoice} <- @streams.invoices} id={dom_id}>
<:cell class="font-medium">{invoice.number}</:cell>
<:cell>{invoice.status}</:cell>
<:cell>{invoice.method}</:cell>
<:cell class="text-right">{invoice.amount}</:cell>
</Table.table_row>
</Table.table_body>
<tfoot>
<tr>
<td colspan="3">Total</td>
<td class="text-right">$1,200.00</td>
</tr>
</tfoot>
</Table.table>
Sortable headers
Put a keyboard-accessible button inside a sortable column header and set
aria-sort
on the currently sorted column. The parent LiveView handles the event and supplies the
reordered stream.
| Method | |||
|---|---|---|---|
| INV001 | Paid | Credit card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank transfer | $350.00 |
| INV004 | Paid | Credit card | $450.00 |
<Table.table container_class="rounded-xl border border-border">
<Table.table_head>
<:col aria-sort={if @table_sort == {:number, :asc}, do: "ascending", else: if(@table_sort == {:number, :desc}, do: "descending", else: "none")}>
<Button.button kind={:ghost} size={:sm} class="-mx-2" phx-click="sort-table" phx-value-column="number">
Invoice <Icon.icon name={if @table_sort == {:number, :asc}, do: "hero-chevron-up", else: if(@table_sort == {:number, :desc}, do: "hero-chevron-down", else: "hero-chevron-up-down")} class="size-4" />
</Button.button>
</:col>
<:col aria-sort={if @table_sort == {:status, :asc}, do: "ascending", else: if(@table_sort == {:status, :desc}, do: "descending", else: "none")}>
<Button.button kind={:ghost} size={:sm} class="-mx-2" phx-click="sort-table" phx-value-column="status">
Status <Icon.icon name={if @table_sort == {:status, :asc}, do: "hero-chevron-up", else: if(@table_sort == {:status, :desc}, do: "hero-chevron-down", else: "hero-chevron-up-down")} class="size-4" />
</Button.button>
</:col>
<:col>Method</:col>
<:col class="text-right" aria-sort={if @table_sort == {:amount, :asc}, do: "ascending", else: if(@table_sort == {:amount, :desc}, do: "descending", else: "none")}>
<Button.button kind={:ghost} size={:sm} class="-mr-2 ml-auto" phx-click="sort-table" phx-value-column="amount">
Amount <Icon.icon name={if @table_sort == {:amount, :asc}, do: "hero-chevron-up", else: if(@table_sort == {:amount, :desc}, do: "hero-chevron-down", else: "hero-chevron-up-down")} class="size-4" />
</Button.button>
</:col>
</Table.table_head>
<Table.table_body id="sortable-invoices">
<Table.table_row :for={{dom_id, invoice} <- @streams.invoices} id={dom_id}>
<:cell class="font-medium">{invoice.number}</:cell>
<:cell>{invoice.status}</:cell>
<:cell>{invoice.method}</:cell>
<:cell class="text-right">{invoice.amount}</:cell>
</Table.table_row>
</Table.table_body>
</Table.table>
Stream containers
Supply a stable id
to Table.table_body. It always renders phx-update="stream", and every row requires
a stable ID. Use the DOM ID from each stream tuple for streamed rows.
| Invoice | Status | Amount |
|---|---|---|
| INV001 | Paid | $250.00 |
| INV002 | Pending | $150.00 |
| INV003 | Unpaid | $350.00 |
| INV004 | Paid | $450.00 |
<Table.table container_class="rounded-xl border border-border">
<Table.table_head>
<:col>Invoice</:col>
<:col>Status</:col>
<:col class="text-right">Amount</:col>
</Table.table_head>
<Table.table_body id="invoices">
<Table.table_row id="invoices-empty" class="hidden only:table-row">
<:cell colspan="3" class="py-10 text-center text-muted-foreground">
No invoices found.
</:cell>
</Table.table_row>
<Table.table_row :for={{dom_id, invoice} <- @streams.invoices} id={dom_id}>
<:cell class="font-medium">{invoice.number}</:cell>
<:cell>{invoice.status}</:cell>
<:cell class="text-right">{invoice.amount}</:cell>
</Table.table_row>
</Table.table_body>
</Table.table>
Rich rows
Cell slots accept arbitrary HEEx, native cell attributes, and per-column utility classes. Use them for stacked text, status badges, and row actions without changing the table API.
| Product | Status | Price | Actions |
|---|---|---|---|
|
Wireless mouse
ELC-001
| In stock | $29.99 | |
|
Mechanical keyboard
ELC-014
| Low stock | $129.99 | |
|
USB-C hub
ACC-072
| Out of stock | $49.99 |
<Table.table container_class="rounded-xl border border-border">
<Table.table_head>
<:col>Product</:col>
<:col>Status</:col>
<:col class="text-right">Price</:col>
<:col><span class="sr-only">Actions</span></:col>
</Table.table_head>
<Table.table_body id="products">
<Table.table_row :for={{dom_id, product} <- @streams.products} id={dom_id}>
<:cell>
<div class="font-medium">{product.name}</div>
<div class="text-sm text-muted-foreground">{product.sku}</div>
</:cell>
<:cell><Badge.badge kind={product.badge}>{product.status}</Badge.badge></:cell>
<:cell class="text-right font-mono">{product.price}</:cell>
<:cell class="text-right">
<Button.button kind={:ghost} size={:icon_sm} aria-label={"Actions for #{product.name}"}>
<Icon.icon name="hero-ellipsis-horizontal" />
</Button.button>
</:cell>
</Table.table_row>
</Table.table_body>
</Table.table>
Accessibility
Keep column headers descriptive and add a
caption
when surrounding content does not already identify the table. Dizzy sets
scope="col"
on header cells. Sort controls remain native buttons, and the active header communicates
its direction with aria-sort.
References
Add this module
mix dizzy.add table
Component API
Table.table
Attributes
| Name | Type | Default | Values |
|---|---|---|---|
class |
:any
| nil | โ |
container_class |
:any
| nil | โ |
rest |
:global
| nil | โ |
Slots
| Name | Required |
|---|---|
inner_block | Required |
Table.table_head
Attributes
| Name | Type | Default | Values |
|---|---|---|---|
class |
:any
| nil | โ |
rest |
:global
| nil | โ |
Slots
| Name | Required |
|---|---|
col | Required |
Table.table_body
Attributes
| Name | Type | Default | Values |
|---|---|---|---|
class |
:any
| nil | โ |
id |
:string
| Required | โ |
rest |
:global
| nil | โ |
Slots
| Name | Required |
|---|---|
inner_block | Required |
Table.table_row
Attributes
| Name | Type | Default | Values |
|---|---|---|---|
class |
:any
| nil | โ |
id |
:string
| Required | โ |
rest |
:global
| nil | โ |
Slots
| Name | Required |
|---|---|
cell | Required |