Expandable table rows with collapsible detail panels.
Pairs two components: expandable_table_row/1 adds a chevron toggle in the
first cell, and expandable_table_detail/1 renders the collapsible detail
row below it. All expand/collapse state lives in the LiveView.
Sub-components
| Function | HTML element | Purpose |
|---|---|---|
expandable_table_row/1 | <tr> | Row with expand/collapse chevron cell |
expandable_table_detail/1 | <tr> | Hidden detail row shown when expanded |
Example
defmodule MyAppWeb.OrdersLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, expanded_rows: MapSet.new(), orders: Orders.list())}
end
def handle_event("toggle_row", %{"id" => id}, socket) do
expanded =
if MapSet.member?(socket.assigns.expanded_rows, id),
do: MapSet.delete(socket.assigns.expanded_rows, id),
else: MapSet.put(socket.assigns.expanded_rows, id)
{:noreply, assign(socket, expanded_rows: expanded)}
end
def render(assigns) do
~H"""
<.table>
<.table_body>
<%= for order <- @orders do %>
<.expandable_table_row
row_id={to_string(order.id)}
expanded={order.id in @expanded_rows}
on_toggle="toggle_row"
>
<.table_cell>{order.number}</.table_cell>
<.table_cell>{order.customer}</.table_cell>
<.table_cell>{order.total}</.table_cell>
</.expandable_table_row>
<.expandable_table_detail
row_id={to_string(order.id)}
expanded={order.id in @expanded_rows}
>
<p class="text-sm text-muted-foreground">Order details for {order.number}</p>
</.expandable_table_detail>
<% end %>
</.table_body>
</.table>
"""
end
endAccessibility
- The chevron
<button>hasaria-expandedset to"true"or"false" aria-controlslinks the button to the detail row ID (row-detail-{row_id})- The detail
<tr>has a matchingidfor thearia-controlsreference
Summary
Functions
Renders a collapsible detail row below its paired expandable_table_row/1.
Renders a <tr> with an expand/collapse chevron prepended in the first cell.
Functions
Renders a collapsible detail row below its paired expandable_table_row/1.
When expanded={false} the hidden class hides the row entirely. When
expanded={true} the row is visible. The id matches the aria-controls
reference in the toggle button.
Example
<.expandable_table_detail
row_id={to_string(order.id)}
expanded={order.id in @expanded_rows}
>
<div class="rounded-md bg-muted/30 p-4 text-sm">
<p>Items: {length(order.items)}</p>
<p>Shipping: {order.shipping_address}</p>
</div>
</.expandable_table_detail>Attributes
row_id(:string) (required) - Must match therow_idof the pairedexpandable_table_row/1. Used to build theidattribute thataria-controlsreferences.expanded(:boolean) - Whenfalse(default) the row is hidden viahiddenclass. Controlled by LiveView. Defaults tofalse.col_span(:integer) - Number of columns this detail row spans. Default 100 spans all columns. Defaults to100.class(:string) - Additional CSS classes for the<tr>. Defaults tonil.
Slots
inner_block(required) - Detail content — can be a nested table, description list, JSON viewer, etc.
Renders a <tr> with an expand/collapse chevron prepended in the first cell.
The chevron rotates 90° when expanded={true} via a Tailwind transition.
All interaction state lives in the parent LiveView — this component is
stateless.
Pair with expandable_table_detail/1 using the same row_id.
Example
<.expandable_table_row
row_id={to_string(order.id)}
expanded={order.id in @expanded_rows}
on_toggle="toggle_row"
>
<.table_cell>{order.number}</.table_cell>
<.table_cell>{order.total}</.table_cell>
</.expandable_table_row>Attributes
expanded(:boolean) - Whether the detail row is currently visible. Controls chevron rotation. Defaults tofalse.on_toggle(:string) -phx-clickevent fired when the expand/collapse button is clicked. Receivesphx-value-idset torow_id. Toggle membership in yourexpanded_rowsset in the handler.Defaults to
"toggle_row".row_id(:string) (required) - Stable identifier for this row. Used forphx-value-idand to build thearia-controls/idpair with the matchingexpandable_table_detail/1.class(:string) - Additional CSS classes for the<tr>. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the
<tr>element (e.g.id).
Slots
inner_block(required) - Table data cells (table_cell/1or<td>) for this row, excluding the expand toggle cell — that is prepended automatically.