All-in-one declarative table — define columns once, render everything.
Unlike the composable Table primitive (where you manually write thead/tbody
rows), DataTable generates the full table structure from a :rows list and
named :column slots. This makes common tables fast to build with minimal
boilerplate.
For advanced use cases (streams, row selection, pagination, column toggle)
use DataGrid instead.
Sub-components
| Function | Used as | Purpose |
|---|---|---|
data_table/1 | Component | Outer table + header + body |
:column slot | <:column> | Column definition with inner_block |
Example
<.data_table
rows={@users}
sort_key={@sort_key}
sort_dir={@sort_dir}
on_sort="sort"
striped
>
<:column key="name" label="Name" sortable :let={user}>
<span class="font-medium">{user.name}</span>
</:column>
<:column key="email" label="Email" sortable :let={user}>
{user.email}
</:column>
<:column key="role" label="Role" align={:center} :let={user}>
<.badge variant={:secondary}>{user.role}</.badge>
</:column>
<:action :let={user}>
<.button variant={:ghost} size={:sm} phx-click="edit" phx-value-id={user.id}>
Edit
</.button>
</:action>
</.data_table>Sort events
Sortable column headers fire the on_sort event with phx-value-key and
phx-value-dir (the next direction: "asc", "desc", or "none"):
def handle_event("sort", %{"key" => key, "dir" => dir}, socket) do
{:noreply, assign(socket, sort_key: key, sort_dir: String.to_existing_atom(dir))}
end
Summary
Functions
Renders a complete table from rows and :column slot definitions.
Functions
Renders a complete table from rows and :column slot definitions.
Column headers are generated from :column slot attrs (label, sortable,
align). Each body row is built by iterating rows and calling
render_slot(col, row) for each column — the slot's :let binding receives
the row map/struct.
Example
<.data_table rows={@products} empty_message="No products yet">
<:column key="name" label="Product" :let={p}>{p.name}</:column>
<:column key="price" label="Price" align={:right} :let={p}>
${p.price}
</:column>
</.data_table>Attributes
rows(:list) (required) - List of maps or structs to render — one row per element.row_id(:any) - Optional functionfn row -> id endto extract a stable row identifier. Not used directly in the render but available for LiveView stream integration.Defaults to
nil.sort_key(:string) - Key of the currently sorted column. Matches thekeyattr on:columnslots. Defaults tonil.sort_dir(:atom) - Current sort direction.:noneshows the unsorted double-chevron icon. Defaults to:none. Must be one of:none,:asc, or:desc.on_sort(:string) -phx-clickevent fired when a sortable column header is clicked. Receivesphx-value-key(column key) andphx-value-dir(next direction string).Defaults to
"sort".striped(:boolean) - Whentrue, alternating rows receive a subtlebg-muted/30background. Defaults tofalse.empty_message(:string) - Text displayed whenrowsis an empty list. Defaults to"No data".class(:string) - Additional CSS classes for the outer wrapper div. Defaults tonil.
Slots
column- Accepts attributes:key(:string) (required)label(:string) (required)sortable(:boolean)align(:atom) - Must be one of:left,:right, or:center.width(:string)
action- Optional per-row action cell appended as the last column. The slot receives each row via:let:<:action :let={user}> <.button phx-click="delete" phx-value-id={user.id}>Delete</.button> </:action>