BulkActionBar component for PhiaUI.
A contextual action toolbar that appears when one or more rows are selected in a table or list. Displays the selection count, a clear-selection button, and a slot for action buttons.
The toolbar is hidden automatically when count is zero — implemented as two
function heads on bulk_action_bar/1: the count: 0 head renders an empty
fragment so the DOM element is completely absent, while any non-zero count
renders the full toolbar. This avoids CSS display:none tricks and keeps
the DOM clean.
When to use
Use BulkActionBar in data table or list views wherever you want to let users
select multiple rows and act on them collectively — for example:
- Delete multiple records at once
- Archive or restore a batch of items
- Export selected rows to CSV
- Assign multiple tasks to a team member
- Approve / reject a group of requests
Anatomy
| Component | Element | Purpose |
|---|---|---|
bulk_action_bar/1 | div | Root toolbar (role="toolbar") — hidden when count=0 |
bulk_action/1 | button | Individual action button with optional icon and variant |
Example — table with bulk actions
<%!-- Toolbar sits above (or below) the table and shows when rows are selected --%>
<.bulk_action_bar
count={@selected_count}
label="contacts selected"
on_clear="clear_selection"
>
<.bulk_action label="Send email" on_click="bulk_email" icon="mail" />
<.bulk_action label="Export CSV" on_click="bulk_export" icon="download" />
<.bulk_action label="Archive" on_click="bulk_archive" icon="archive" />
<.bulk_action label="Delete" on_click="bulk_delete" icon="trash" variant="destructive" />
</.bulk_action_bar>
<.data_grid rows={@contacts} columns={@columns} />LiveView handlers
def handle_event("clear_selection", _params, socket) do
{:noreply, assign(socket, selected_ids: MapSet.new(), selected_count: 0)}
end
def handle_event("bulk_delete", _params, socket) do
ids = MapSet.to_list(socket.assigns.selected_ids)
Repo.delete_all(from c in Contact, where: c.id in ^ids)
{:noreply,
socket
|> assign(selected_ids: MapSet.new(), selected_count: 0)
|> stream(:contacts, Repo.all(Contact))}
end
def handle_event("bulk_export", _params, socket) do
ids = MapSet.to_list(socket.assigns.selected_ids)
csv = Contacts.export_csv(ids)
{:noreply, push_event(socket, "download", %{filename: "contacts.csv", content: csv})}
endWhy two function heads instead of CSS display:none?
When count is 0, bulk_action_bar/1 renders ~H"" — an empty HEEx
fragment. This means the DOM element does not exist at all, which:
- Prevents screen readers from announcing the toolbar when it is inactive
- Avoids accidental tab focus landing on hidden buttons
- Keeps the DOM tree small for large data tables
- Makes
role="toolbar"semantics correct (toolbars should not be present in the accessibility tree when they have no applicable context)
Summary
Functions
Renders a single bulk action button.
Place inside a bulk_action_bar/1 slot. Use variant="destructive" for
irreversible operations (delete, purge) so users visually distinguish them
from safe operations (export, archive).
Example
<.bulk_action label="Delete selected" on_click="bulk_delete" icon="trash" variant="destructive" />
<.bulk_action label="Export" on_click="bulk_export" icon="download" />Attributes
label(:string) (required) - Button label text (e.g. "Delete", "Archive").on_click(:string) (required) -phx-clickevent name fired when the button is pressed. The handler should act on all currently selected IDs and then clear the selection.variant(:string) - Visual variant:default— muted text with hover background (for safe operations)destructive— redtext-destructivewith red hover (for delete/irreversible actions)
Defaults to
"default". Must be one of"default", or"destructive".icon(:string) - Optional Lucide icon name displayed before the label. Recommended for quick recognition:"trash","archive","download","mail".Defaults to
nil.class(:string) - Additional CSS classes for the button. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the
<button>element. Usedisabled={@processing}to prevent double-submits during async operations. Supports all globals plus:["phx-value-id", "phx-value-type", "disabled"].
Renders the bulk action toolbar.
When count is 0, renders an empty HEEx fragment (~H""), meaning no
DOM element is present. This is preferable to CSS visibility toggling for
accessibility and DOM cleanliness.
When count > 0, renders a glass-morphism toolbar with:
- Selection count + label on the left
- A clear (×) button to deselect all rows
- A vertical divider
- Action buttons from the
:inner_blockslot on the right
Uses role="toolbar" for correct ARIA semantics and includes a dynamic
aria-label that reads the count aloud to screen readers.
Attributes
count(:integer) (required) - Number of currently selected items. When0, the entire toolbar is removed from the DOM. When positive, the toolbar renders withcountdisplayed prominently.label(:string) - Label appended after the count, e.g."selected"→ "3 selected", or"contacts selected"→ "5 contacts selected".Defaults to
"selected".on_clear(:string) (required) -phx-clickevent name for the clear-selection button. The handler should reset selected IDs and count to zero.class(:string) - Additional CSS classes for the toolbar container. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the root div.
Slots
inner_block(required) -bulk_action/1children — the action buttons for this toolbar.