Feature-rich data table with server-side sorting, pagination, toolbar, column visibility toggling, and row selection — all stateless.
Every sub-component is a pure render function. All state (sort direction,
current page, selected row IDs, visible columns) lives in your LiveView
assigns. Events are sent via phx-click / phx-change and handled with
standard handle_event/3 callbacks.
The data_grid_body/1 element is LiveView Streams compatible: add
phx-update="stream" to enable incremental DOM patching for large datasets.
Sub-components
| Component | HTML element | Purpose |
|---|---|---|
data_grid/1 | div > table | Scrollable outer container |
data_grid_head/1 | th | Column header; optional sort button |
data_grid_body/1 | tbody | Data rows; accepts phx-update="stream" |
data_grid_row/1 | tr | Row with hover state and optional styling |
data_grid_cell/1 | td | Data cell with consistent px-4 py-3 padding |
data_grid_toolbar/1 | div | Flex toolbar for search, filters, actions |
data_grid_pagination/1 | nav | First/prev/next/last + rows-per-page selector |
data_grid_column_toggle/1 | div | Dropdown checklist to show/hide columns |
data_grid_row_checkbox/1 | td | Row selection checkbox cell |
data_grid_select_all/1 | th | Select-all checkbox header cell |
Sort direction lifecycle
Sort directions are represented as atoms in Elixir (:none, :asc, :desc)
but arrive as strings from phx-value ("asc", "desc", "none").
The next_dir/1 helper cycles the direction on each click:
:none → :asc → :desc → :noneIn your handle_event always convert with String.to_existing_atom/1:
def handle_event("sort", %{"key" => key, "dir" => dir}, socket) do
{:noreply, assign(socket, sort_key: key, sort_dir: String.to_existing_atom(dir))}
endThe use of String.to_existing_atom/1 (not String.to_atom/1) is safe
because the only possible values are :asc, :desc, and :none, which
are already atoms defined in this module.
Complete example with streams and sorting
defmodule MyAppWeb.UsersLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
socket =
socket
|> assign(sort_key: "name", sort_dir: :asc)
|> assign(page: 1, total_pages: 10, page_size: 20)
|> assign(selected_ids: MapSet.new())
|> assign(columns: [
%{key: "name", label: "Name", visible: true},
%{key: "email", label: "Email", visible: true},
%{key: "role", label: "Role", visible: true},
%{key: "plan", label: "Plan", visible: false}
])
|> stream(:users, Accounts.list_users(sort: "name", dir: :asc))
{:ok, socket}
end
def handle_event("sort", %{"key" => key, "dir" => dir}, socket) do
dir_atom = String.to_existing_atom(dir)
users = Accounts.list_users(sort: key, dir: dir_atom)
socket =
socket
|> assign(sort_key: key, sort_dir: dir_atom)
|> stream(:users, users, reset: true)
{:noreply, socket}
end
def handle_event("paginate", %{"page" => page}, socket) do
page_int = String.to_integer(page)
users = Accounts.list_users(page: page_int, page_size: socket.assigns.page_size)
socket =
socket
|> assign(page: page_int)
|> stream(:users, users, reset: true)
{:noreply, socket}
end
def handle_event("page_size", %{"value" => size}, socket) do
size_int = String.to_integer(size)
users = Accounts.list_users(page: 1, page_size: size_int)
socket =
socket
|> assign(page: 1, page_size: size_int)
|> stream(:users, users, reset: true)
{:noreply, socket}
end
def handle_event("select_row", %{"id" => id}, socket) do
id_int = String.to_integer(id)
selected =
if MapSet.member?(socket.assigns.selected_ids, id_int),
do: MapSet.delete(socket.assigns.selected_ids, id_int),
else: MapSet.put(socket.assigns.selected_ids, id_int)
{:noreply, assign(socket, selected_ids: selected)}
end
def handle_event("select_all", _params, socket) do
# Toggle: if any selected, deselect all; otherwise select all visible IDs
selected =
if MapSet.size(socket.assigns.selected_ids) > 0,
do: MapSet.new(),
else: MapSet.new(Enum.map(socket.assigns.streams.users, fn {_, u} -> u.id end))
{:noreply, assign(socket, selected_ids: selected)}
end
def handle_event("toggle_column", %{"column" => key}, socket) do
columns =
Enum.map(socket.assigns.columns, fn col ->
if col.key == key, do: %{col | visible: !col.visible}, else: col
end)
{:noreply, assign(socket, columns: columns)}
end
def render(assigns) do
~H"""
<.data_grid_toolbar>
<input
type="text"
placeholder="Search users…"
phx-change="search"
phx-debounce="300"
class="h-8 rounded-md border border-input bg-background px-3 text-sm w-64"
/>
<div class="ml-auto flex items-center gap-2">
<.data_grid_column_toggle
id="user-col-toggle"
columns={@columns}
on_toggle="toggle_column"
/>
</div>
</.data_grid_toolbar>
<.data_grid>
<thead>
<tr>
<.data_grid_select_all
checked={MapSet.size(@selected_ids) == length(@streams.users)}
indeterminate={MapSet.size(@selected_ids) > 0 and MapSet.size(@selected_ids) < length(@streams.users)}
on_check="select_all"
/>
<.data_grid_head sort_key="name" sort_dir={if @sort_key == "name", do: @sort_dir, else: :none} on_sort="sort">Name</.data_grid_head>
<.data_grid_head sort_key="email" sort_dir={if @sort_key == "email", do: @sort_dir, else: :none} on_sort="sort">Email</.data_grid_head>
<.data_grid_head sort_key="role" sort_dir={if @sort_key == "role", do: @sort_dir, else: :none} on_sort="sort">Role</.data_grid_head>
<.data_grid_head>Actions</.data_grid_head>
</tr>
</thead>
<.data_grid_body id="users-body" phx-update="stream">
<.data_grid_row
:for={{dom_id, user} <- @streams.users}
id={dom_id}
class={if user.id in @selected_ids, do: "bg-muted/70"}
>
<.data_grid_row_checkbox
row_id={to_string(user.id)}
checked={user.id in @selected_ids}
on_check="select_row"
/>
<.data_grid_cell><%= user.name %></.data_grid_cell>
<.data_grid_cell><%= user.email %></.data_grid_cell>
<.data_grid_cell><%= user.role %></.data_grid_cell>
<.data_grid_cell>
<.button variant={:ghost} size={:sm} phx-click="edit_user" phx-value-id={user.id}>
Edit
</.button>
</.data_grid_cell>
</.data_grid_row>
</.data_grid_body>
</.data_grid>
<.data_grid_pagination
current_page={@page}
total_pages={@total_pages}
page_size={@page_size}
on_page="paginate"
on_page_size="page_size"
/>
"""
end
endAccessibility
- Sortable headers emit
aria-sort("ascending","descending","none") - Row checkboxes use
role="checkbox"andaria-checked - Select-all uses
aria-checked="mixed"for indeterminate state - Pagination uses
<nav aria-label="Table pagination">andaria-labelon each button
Summary
Functions
Renders the scrollable <table> container.
Flex row of active filter chips with a "Clear all" button.
Renders a visually distinct aggregation row for totals, averages, or sums.
Renders the <tbody> element.
Renders a <td> data cell with consistent px-4 py-3 padding.
Renders a <th> spanning multiple columns for two-row grouped headers.
Renders a column-visibility toggle button with a dropdown checklist.
Three-button toggle to switch between compact / normal / comfortable row density.
Renders a full-width detail panel row for expandable row content.
Renders a client-side CSV export button.
Badge-style chip showing an active filter with a remove button.
Full group header row wrapper for two-level column grouping.
Renders a collapsible row-group header with a chevron and optional count badge.
Renders a <th> column header with optional sort button.
Renders a full-featured pagination navigation bar.
Renders a sticky pinned row for summary totals or frozen header rows.
Renders a <tr> table row with hover state.
Renders a <td> selection checkbox for an individual data grid row.
Renders a <th> select-all checkbox for the data grid header row.
Renders a flex toolbar row positioned above the data grid.
Functions
Renders the scrollable <table> container.
The outer <div> has overflow-auto to handle wide tables on small screens
without breaking the page layout. The table itself is w-full caption-bottom text-sm so it fills its container and uses bottom-positioned captions.
Example
<.data_grid>
<thead>...</thead>
<.data_grid_body id="rows" phx-update="stream">
...
</.data_grid_body>
</.data_grid>Attributes
id(:string) - Optional ID for the outer wrapper div; used to build the aria-live status region ID. Defaults tonil.status_message(:string) - Screen reader announcement for sort/filter/page changes. E.g.: 'Sorted by Name, ascending'. Defaults to"".responsive_mode(:atom) - Controls responsive behavior on small screens.:scroll— default horizontal scroll on overflow (viaoverflow-x-auto):stack— belowmd:, each row becomes a vertical card layout:hide_columns— no change to the grid itself; columns with apriorityattr can be hidden viahidden md:table-cellin the consuming template
Defaults to
:scroll. Must be one of:scroll,:stack, or:hide_columns.sticky_header(:boolean) - Whentrue, the<thead>is sticky at the top of the scrollable container. Addssticky top-0 z-10 bg-backgroundto the thead via a child selector.Defaults to
false.loading(:boolean) - Whentrue, renders a semi-transparent overlay with a spinner icon over the table to indicate an in-flight data fetch. The table content remains visible but non-interactive beneath the overlay.Defaults to
false.class(:string) - Additional CSS classes for the outer overflow wrapper. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the
<table>element (e.g.aria-label,data-*).
Slots
inner_block(required) - Table structure: thead, data_grid_body/1, tfoot.
Flex row of active filter chips with a "Clear all" button.
Renders nothing when filters is empty. Each chip fires on_remove with
phx-value-key set to the filter's :value. A "Clear all" button fires
on_clear_all.
Example
<.data_grid_active_filters
:if={@filters != []}
filters={@filters}
on_remove="remove_filter"
on_clear_all="clear_filters"
/>Where @filters is e.g.:
[%{label: "Role: Admin", value: "role"}, %{label: "Status: Active", value: "status"}]Attributes
filters(:list) (required) - List of active filter maps. Each must have:label— display string (e.g. "Role: Admin")value— key sent toon_removehandler (e.g. "role")
on_remove(:string) - phx-click event fired by each chip. Defaults to"remove_filter".clear_all_label(:string) - Label for the clear-all button. Defaults to"Clear all".on_clear_all(:string) - phx-click event for clear all. Defaults to"clear_all_filters".class(:string) - Defaults tonil.
Renders a visually distinct aggregation row for totals, averages, or sums.
Use at the bottom of data_grid_body/1 (or after a group's rows).
Applies a top double-border and font-medium bg-muted/30 styling.
Example
<.data_grid_aggregation_row>
<.data_grid_cell class="font-semibold">Total</.data_grid_cell>
<.data_grid_cell class="tabular-nums">{@sum}</.data_grid_cell>
</.data_grid_aggregation_row>Attributes
class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required) - data_grid_cell/1 children with aggregate values.
Renders the <tbody> element.
The [&_tr:last-child]:border-0 rule removes the bottom border from the
last row so the table does not appear to have a double border with the
pagination row below it.
Supports phx-update="stream" for incremental LiveView Streams updates.
Always provide an id attribute when using streams so LiveView can track
the element across patches.
Example
<.data_grid_body id="user-rows" phx-update="stream">
<.data_grid_row :for={{dom_id, user} <- @streams.users} id={dom_id}>
<.data_grid_cell><%= user.name %></.data_grid_cell>
</.data_grid_row>
</.data_grid_body>Attributes
class(:string) - Additional CSS classes. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the
<tbody>element. The most important use isphx-update="stream"for LiveView Streams:<.data_grid_body id="users" phx-update="stream"> <.data_grid_row :for={{dom_id, user} <- @streams.users} id={dom_id}> ... </.data_grid_row> </.data_grid_body>When using streams,
idis also required on the<tbody>itself.
Slots
inner_block(required) - data_grid_row/1 children.
Renders a <td> data cell with consistent px-4 py-3 padding.
The [&:has([role=checkbox])]:pr-0 rule removes right padding from cells
containing a checkbox (data_grid_row_checkbox/1), keeping checkboxes
visually aligned with the select-all header.
Example
<.data_grid_cell>John Smith</.data_grid_cell>
<.data_grid_cell class="text-right font-mono">$1,234.56</.data_grid_cell>
<.data_grid_cell>
<.badge variant={:secondary}><%= user.role %></.badge>
</.data_grid_cell>Attributes
density(:atom) - Padding density for the data cell::compact—px-3 py-1.5:normal—px-4 py-3(default):comfortable—px-4 py-4
Defaults to
:normal. Must be one of:compact,:normal, or:comfortable.class(:string) - Additional CSS classes for the cell. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the
<td>element (e.g.colspan,rowspan).
Slots
inner_block(required) - Cell content — text, badges, buttons, avatars, etc.
Renders a <th> spanning multiple columns for two-row grouped headers.
Use inside a top <tr> in <thead> above the individual column headers.
Example
<thead>
<tr>
<.data_grid_column_group colspan={2} label="Personal Info" />
<.data_grid_column_group colspan={3} label="Work Details" />
</tr>
<tr>
<.data_grid_head>Name</.data_grid_head>
<.data_grid_head>Email</.data_grid_head>
...
</tr>
</thead>Attributes
colspan(:integer) (required) - Number of columns this group spans.label(:string) - Group header text. Defaults to"".class(:string) - Defaults tonil.- Global attributes are accepted.
Renders a column-visibility toggle button with a dropdown checklist.
Clicking the "Columns" button calls JS.toggle/1 to show/hide a dropdown
menu listing all columns with checkboxes. Checking/unchecking a box fires
on_toggle with phx-value-column set to the column key. All visibility
state is managed in the LiveView — this component is stateless.
The dropdown is positioned absolute right-0 top-10 so it opens below
and to the right of the button. Ensure the parent has position: relative
(the wrapper div adds this automatically).
Example
<.data_grid_column_toggle
id="user-columns"
columns={@columns}
on_toggle="toggle_column"
/>Attributes
id(:string) (required) - Unique element ID. Used to build the dropdown menu ID (e.g."my-grid-menu") and as the toggle target forJS.toggle/1. Must be unique on the page.columns(:list) (required) - List of column definition maps. Each map must have:key— String used asphx-value-columnwhen the checkbox is clickedlabel— Human-readable column name shown in the dropdownvisible— Boolean controlling the checked state of the checkbox
Example:
[%{key: "name", label: "Name", visible: true}, ...]on_toggle(:string) -phx-clickevent fired when a column checkbox is toggled. Receivesphx-value-columnset to the columnkey. Handle with:def handle_event("toggle_column", %{"column" => key}, socket) do columns = Enum.map(socket.assigns.columns, fn col -> if col.key == key, do: %{col | visible: !col.visible}, else: col end) {:noreply, assign(socket, columns: columns)} endDefaults to
"toggle_column".class(:string) - Additional CSS classes for the wrapper div. Defaults tonil.
Three-button toggle to switch between compact / normal / comfortable row density.
Fires on_change with phx-value-density set to the selected value string.
Pass the resulting density atom to data_grid_head/1 and data_grid_cell/1.
Example
<.data_grid_toolbar>
<div class="ml-auto flex items-center gap-2">
<.data_grid_density_toggle density={@density} on_change="set_density" />
</div>
</.data_grid_toolbar>Handler
def handle_event("set_density", %{"density" => d}, socket) do
{:noreply, assign(socket, density: String.to_existing_atom(d))}
endAttributes
density(:atom) - Currently active density. Defaults to:normal. Must be one of:compact,:normal, or:comfortable.on_change(:string) - phx-click event name. Defaults to"change_density".class(:string) - Defaults tonil.
Renders a full-width detail panel row for expandable row content.
Place immediately after the expanded data_grid_row/1. Toggle visibility
in your LiveView by conditionally rendering this row.
Example
<.data_grid_row id={dom_id}>
...
<.data_grid_cell>
<button phx-click="toggle_row" phx-value-id={row.id}>Expand</button>
</.data_grid_cell>
</.data_grid_row>
<.data_grid_detail_row :if={row.id in @expanded} colspan={5}>
<div class="p-4 bg-muted/20">Detailed view for {row.name}</div>
</.data_grid_detail_row>Attributes
colspan(:integer) (required) - Number of columns to span (equals total column count).class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Renders a client-side CSV export button.
The PhiaGridExport hook reads visible <th> columns (skipping those
with data-hidden) and all <tr> in the <tbody> to build a CSV blob
and trigger a browser download. Entirely client-side — no server call.
Example
<.data_grid_toolbar>
<div class="ml-auto flex items-center gap-2">
<.data_grid_export_button
id="users-export"
target_id="users-grid"
filename="users.csv"
/>
</div>
</.data_grid_toolbar>
<.data_grid id="users-grid">...</.data_grid>Attributes
id(:string) (required) - Unique ID (required for phx-hook).target_id(:string) (required) - ID of thedata_gridtable to export.filename(:string) - Downloaded filename. Defaults to"export.csv".label(:string) - Defaults to"Export CSV".class(:string) - Defaults tonil.- Global attributes are accepted.
Badge-style chip showing an active filter with a remove button.
Fires on_remove with phx-value-key={value} when the × is clicked.
Example
<.data_grid_filter_chip label="Role: Admin" value="role" on_remove="clear_filter" />Attributes
label(:string) (required) - Display text for the filter chip.value(:string) (required) - phx-value-key sent when chip is removed.on_remove(:string) - phx-click event name. Defaults to"remove_filter".class(:string) - Defaults tonil.
Full group header row wrapper for two-level column grouping.
Renders a <tr> that should be placed as the first row in <thead>.
Fill it with data_grid_column_group/1 cells.
Example
<.data_grid_group_head>
<.data_grid_column_group colspan={2} label="Identity" />
<.data_grid_column_group colspan={2} label="Status" />
</.data_grid_group_head>Attributes
class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Renders a collapsible row-group header with a chevron and optional count badge.
Fires on_toggle with phx-value-value={value} when clicked.
Your LiveView maintains the expanded/collapsed state per group key.
Example
<.data_grid_group_row
label="Active Users"
count={length(@active_users)}
expanded={@groups_expanded["active"]}
on_toggle="toggle_group"
value="active"
colspan={5}
/>Attributes
label(:string) (required) - Group name displayed in the header row.count(:any) - Item count shown as a badge (nil to hide). Defaults tonil.expanded(:boolean) - Controls chevron direction. Defaults totrue.on_toggle(:string) - phx-click event name. Defaults to"toggle_group".value(:string) (required) - phx-value-value identifying this group.colspan(:integer) - Colspan for the group label cell. Defaults to1.class(:string) - Defaults tonil.- Global attributes are accepted.
Renders a <th> column header with optional sort button.
When :sort_key is set, the header content is wrapped in a <button> that:
- Fires
:on_sortviaphx-click - Sends
phx-value-key={sort_key}andphx-value-dir={next_direction} - Renders a sort direction icon (
chevrons-up-down,chevron-up, orchevron-down) - Sets
aria-sorton the<th>for screen reader announcements
Without :sort_key, renders a plain, non-interactive header cell suitable
for action columns or columns where sorting is not meaningful.
Example
<%!-- Sortable column --%>
<.data_grid_head sort_key="name" sort_dir={@sort[:name]} on_sort="sort">
Name
</.data_grid_head>
<%!-- Non-sortable actions column --%>
<.data_grid_head>Actions</.data_grid_head>Attributes
sort_key(:string) - Column key sent asphx-value-keywhen the sort button is clicked. Whennil, the header renders as plain non-interactive text (no sort button). Use the same string your LiveView will receive inhandle_event("sort", ...).Defaults to
nil.sort_dir(:atom) - Current sort direction for this column. Pass:nonefor all columns except the currently sorted one. Controls thearia-sortattribute and which chevron icon is displayed. The next direction (sent on click) cycles::none → :asc → :desc → :none.Defaults to
:none. Must be one of:none,:asc, or:desc.on_sort(:string) -phx-clickevent name fired when the sort button is clicked. Receives twophx-valueparams:key(the column key) anddir(the next direction as a string:"asc","desc", or"none").Defaults to
"sort".density(:atom) - Padding density for the header cell::compact—px-3 py-1.5 h-9:normal—px-4 h-11(default, matches original):comfortable—px-4 h-12
Defaults to
:normal. Must be one of:compact,:normal, or:comfortable.class(:string) - Additional CSS classes. Defaults tonil.Global attributes are accepted.
Slots
inner_block(required) - Column header label text.
Renders a full-featured pagination navigation bar.
Includes:
- Rows per page selector (left side) — fires
on_page_sizeviaphx-change - Page counter display: "Page X of Y"
- First / Prev / Next / Last buttons — fire
on_pageviaphx-click
Navigation buttons are automatically disabled (pointer-events-none + opacity-50) when they would navigate out of range:
- First and Prev are disabled on page 1
- Next and Last are disabled on the last page
Example
<.data_grid_pagination
current_page={@page}
total_pages={@total_pages}
page_size={@page_size}
page_size_options={[10, 25, 50]}
on_page="paginate"
on_page_size="change_page_size"
/>LiveView handlers
def handle_event("paginate", %{"page" => page}, socket) do
{:noreply, assign(socket, page: String.to_integer(page))}
end
def handle_event("change_page_size", %{"value" => size}, socket) do
{:noreply, assign(socket, page: 1, page_size: String.to_integer(size))}
endAttributes
current_page(:integer) (required) - Current page number (1-indexed). Controls disabled state of prev/first buttons.total_pages(:integer) (required) - Total number of pages. Controls disabled state of next/last buttons.page_size(:integer) - Currently active page size — used to pre-select the correct option in the size selector. Defaults to10.page_size_options(:list) - Available rows-per-page options for the<select>element. Each option renders as<option value={n}>{n}</option>. Default:[10, 20, 50, 100].Defaults to
[10, 20, 50, 100].on_page(:string) -phx-clickevent fired by all navigation buttons (first, prev, next, last). Receivesphx-value-pageset to the target page number as a string. Handle with:String.to_integer(page).Defaults to
"paginate".on_page_size(:string) -phx-changeevent fired when the rows-per-page selector changes. Receives%{"value" => "20"}. Handle with:String.to_integer(size).Defaults to
"page_size".class(:string) - Additional CSS classes for the nav element. Defaults tonil.Global attributes are accepted.
Renders a sticky pinned row for summary totals or frozen header rows.
Use :top for pinned column headers within the body, :bottom for totals.
Place inside data_grid_body/1.
Example
<.data_grid_body id="rows">
<.data_grid_row :for={{id, row} <- @streams.rows} id={id}>
<.data_grid_cell>{row.name}</.data_grid_cell>
</.data_grid_row>
<.data_grid_pinned_row position={:bottom}>
<.data_grid_cell class="font-semibold">Total</.data_grid_cell>
<.data_grid_cell class="font-semibold tabular-nums">{@total}</.data_grid_cell>
</.data_grid_pinned_row>
</.data_grid_body>Attributes
position(:atom) -:topsticks to top of table body;:bottomsticks to bottom. Defaults to:bottom. Must be one of:top, or:bottom.class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Renders a <tr> table row with hover state.
Rows have hover:bg-muted/50 for a subtle highlight on pointer-over and
transition-colors so the background change is smooth. The base
border-b border-border creates row dividers.
Example
<.data_grid_row id={dom_id} class={if user.suspended, do: "opacity-50"}>
<.data_grid_cell><%= user.name %></.data_grid_cell>
</.data_grid_row>Attributes
class(:string) - Additional CSS classes for conditional row styling (e.g."bg-amber-50"to highlight rows with warnings). Applied after the base hover styles.Defaults to
nil.Global attributes are accepted. HTML attributes forwarded to the
<tr>element. When using LiveView Streams, passid={dom_id}here so LiveView can identify the row.
Slots
inner_block(required) - data_grid_cell/1 and data_grid_row_checkbox/1 children.
Renders a <td> selection checkbox for an individual data grid row.
The checkbox cell has a fixed width (w-10) and minimal padding (p-2)
to stay compact. The role="checkbox" attribute enables the CSS rule
[&:has([role=checkbox])]:pr-0 on parent cells to trim padding.
Pass checked={row_id in @selected_ids} and maintain the selected set in
your LiveView. There is no internal state here.
Example
<.data_grid_row_checkbox
row_id={to_string(user.id)}
checked={user.id in @selected_ids}
on_check="select_row"
/>Attributes
row_id(:string) (required) - Row identifier sent asphx-value-idwhen the checkbox is clicked. Typically the record's database ID as a string:to_string(user.id). Yourhandle_eventreceives it as a string and should convert back withString.to_integer/1or pattern-match directly.checked(:boolean) - Whether this row is currently selected. Derive from your LiveView state:checked={user.id in @selected_ids}(forMapSet) orchecked={user.id in @selected_ids}(for a plain list).Defaults to
false.on_check(:string) -phx-clickevent fired when the checkbox is toggled. Receivesphx-value-idset torow_id. Toggle the ID in/out of your selection set in the handler.Defaults to
"select_row".class(:string) - Additional CSS classes for the<td>wrapper. Defaults tonil.
Renders a <th> select-all checkbox for the data grid header row.
When indeterminate is true, aria-checked is set to "mixed" to
communicate partial selection to screen readers. This is the WAI-ARIA
pattern for tri-state checkboxes. Note that the visual indeterminate
state (the dash inside the checkbox) requires JavaScript on the client
side — the PhiaDataGrid hook (if installed) handles this automatically.
Example
<.data_grid_select_all
checked={@all_selected}
indeterminate={@some_selected}
on_check="select_all"
/>Attributes
checked(:boolean) - Whether all visible rows are selected. Set totruewhen every row ID is in@selected_ids.Defaults to
false.indeterminate(:boolean) - Whentrue, rendersaria-checked="mixed"to signal partial selection to assistive technology. Set totruewhen some (but not all) rows are selected. Takes precedence over:checkedfor thearia-checkedvalue.Defaults to
false.on_check(:string) -phx-clickevent fired when the checkbox is clicked. Receives nophx-valueparams — the handler should toggle between selecting all and deselecting all based on current state.Defaults to
"select_all".class(:string) - Additional CSS classes for the<th>cell. Defaults tonil.
Renders a flex toolbar row positioned above the data grid.
The toolbar uses flex items-center justify-between with gap-2 so that
left-side controls (search, filters) and right-side controls (column toggle,
export) sit at opposite ends naturally.
Example
<.data_grid_toolbar>
<input type="text" placeholder="Search…" phx-change="search" phx-debounce="300"
class="h-8 w-64 rounded-md border border-input bg-background px-3 text-sm" />
<div class="ml-auto flex items-center gap-2">
<.data_grid_column_toggle id="col-toggle" columns={@columns} on_toggle="toggle_column" />
<.button variant={:outline} size={:sm} phx-click="export">Export CSV</.button>
</div>
</.data_grid_toolbar>Attributes
class(:string) - Additional CSS classes for the toolbar wrapper. Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required) - Toolbar content — typically a search input on the left and action buttons (column toggle, filters, export) on the right. Useml-autoon the right group to push it to the trailing edge.