A basic table component with daisyUI styling.
Supports an optional card/table view toggle for responsive layouts. When items
is provided or toggleable is true, renders both a table view (desktop default)
and a card view (mobile default) with an optional toggle button.
Examples
Basic table (unchanged API)
<.table_default>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>Name</.table_default_header_cell>
<.table_default_header_cell>Email</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<.table_default_row>
<.table_default_cell>John Doe</.table_default_cell>
<.table_default_cell>john@example.com</.table_default_cell>
</.table_default_row>
</.table_default_body>
</.table_default>With card/table toggle
<.table_default
id="users-table"
toggleable
items={@users}
card_title={fn user -> user.name end}
card_fields={fn user -> [
%{label: "Email", value: user.email},
%{label: "Role", value: user.role}
] end}
>
<.table_default_header>...</.table_default_header>
<.table_default_body>...</.table_default_body>
<:card_actions :let={user}>
<.button size="sm" navigate={~p"/users/#{user.id}"}>View</.button>
</:card_actions>
<:toolbar_title>
<span class="text-sm text-base-content/60">{length(@users)} users</span>
</:toolbar_title>
<:toolbar_actions>
<.button size="sm" navigate={~p"/users/new"}>Add User</.button>
</:toolbar_actions>
</.table_default>
Summary
Functions
Drag-handle cell for table-view DnD reorder.
Empty <th> sized to match the drag-handle column. Sits in the
header row alongside <.bulk_select_header_cell> and the regular
<.table_default_header_cell>s.
Renders a search input with a magnifying glass icon and debounce.
Renders a sortable table header cell.
Renders a table with daisyUI styling.
Renders a table body section.
Renders a table data cell.
Renders a table footer section.
Renders a table header section.
Renders a table header cell.
Renders a table row.
Functions
Drag-handle cell for table-view DnD reorder.
Renders a <td> containing the hero-bars-3 grip icon, with the
classes the SortableGrid hook needs to find it (.pk-drag-handle).
The icon is always visible — a muted tone that strengthens on
hover. It used to hide until row hover (opacity-0 group-hover/row:opacity-100), but an affordance you cannot see is
one nobody discovers: users had no idea rows were draggable until
they happened to mouse over the right cell. Deliberate product call
(2026-08-14) — do not quietly restore the hover reveal.
Pair this with a <tbody phx-hook="SortableGrid"> setup (see the
<.table_default> moduledoc / phoenix_kit_projects reference call
sites). The cell only renders the affordance — the hook owns the
drag mechanics.
Attributes
class(:any) - Defaults to"w-8".title(:string) - Defaults tonil.- Global attributes are accepted.
Empty <th> sized to match the drag-handle column. Sits in the
header row alongside <.bulk_select_header_cell> and the regular
<.table_default_header_cell>s.
Separate component (rather than a bare <.table_default_header_cell class="w-8" />)
so the intent is obvious at the call site, and the width default
matches <.drag_handle_cell>'s without consumers having to keep
them in sync.
Attributes
class(:any) - Defaults to"w-8".
Renders a search input with a magnifying glass icon and debounce.
Emits phx-change="search" (override via on_change) with a 300ms
debounce. The input is ALWAYS wrapped in a <form> — LiveView's client
refuses phx-change on an input outside a form (it throws in the
browser console and the event never reaches the server, which reads as
"search silently does nothing"; the component's original no-on_submit
branch rendered exactly that trap and its first real caller hit it).
Pressing Enter fires on_submit when given, otherwise the same
on_change event — so Enter always means "search now".
Attributes
value(:string) (required)on_change(:string) - Defaults to"search".on_submit(:string) - Defaults tonil.placeholder(:string) - Defaults tonil.debounce(:integer) - Defaults to300.name(:string) - Defaults to"search".target(:any) - Defaults tonil.id(:string) - Id for the wrapping<form>. Without one, LiveView form recovery is silently disabled and host suites warnmissing_form_id. Defaults to a value derived from the change event, which is unique per page in practice; pass an explicit id when two search toolbars share a page. Defaults tonil.loading_indicator(:boolean) - Show a spinner while the change event is in flight (LiveView'sphx-change-loadingon the input). Pass false when results appear instantly anyway — e.g. a client-side filter (TableLocalSearch) — where a spinner would be noise. Defaults totrue.class(:any) - Defaults to"".
Renders a sortable table header cell.
When sort is nil, renders an inert <th> label. When sort is a map,
renders a clickable button emitting toggle_sort (or a custom event) with
phx-value-by set to the field key.
States rendered
- Inactive column (sortable, not the current sort) — faint
hero-chevron-up-down-minihint, strengthens on hover viagroup-hover. - Active asc / Active desc — solid chevron-up / chevron-down.
- Loading (during in-flight click) — all chevrons hide and a
loading-spinnershows;pointer-events-noneblocks double-clicks; button dims viaopacity-60. Driven by Phoenix's auto-applied.phx-click-loadingclass — no consumer wiring needed. - Active column with an unrecognised
sort.dir(defensive) — falls back to the inactive up-down hint rather than rendering no icon.
Atom or string sort.dir values are accepted (:asc/:desc/"asc"/"desc").
Attributes
field(:atom) (required)sort(:map) - Current sort: %{by: atom, dir: :asc | :desc}. When nil, renders inert label. Defaults tonil.event(:string) - Defaults to"toggle_sort".target(:any) - Defaults tonil.align(:atom) - Defaults to:left. Must be one of:left,:right, or:center.class(:any) - Defaults to"".- Global attributes are accepted. Supports all globals plus:
["colspan", "rowspan"].
Slots
inner_block(required)
Renders a table with daisyUI styling.
When items is provided or toggleable is true, renders a responsive wrapper
with both table and card views, plus an optional desktop toggle button.
Otherwise renders the classic table-only layout (fully backward compatible).
Attributes
id- Element ID, required when using card/table toggle (optional)class- Additional CSS classes (optional)variant- Table variant: "default", "zebra", "pin-rows", "pin-cols" (optional, default: "default")size- Table size: "xs", "sm", "md", "lg" (optional, default: "md")toggleable- Show card/table toggle buttons on desktop (optional, default: false)items- List of items for card view rendering (optional, default: [])card_title- Function that returns the card title for an item (optional)card_fields- Function that returns a list of%{label: string, value: any}for an item (optional)card_class- Per-card wrapper class. String OR 1-arity function(item) -> string. Default"card card-sm bg-base-200 shadow-sm". Override when the consumer needs a different look (e.g. larger padding, conditional opacity for disabled rows).storage_key- localStorage key for persisting view preference, falls back toidin JS (optional)rest- Additional HTML attributes (optional)
Slots
inner_block- Table content (thead, tbody, etc.)card_body- Fully-custom card content (receives item via:let). When provided, REPLACES the prescribedcard_header+card_title+card_fieldsrendering — the consumer owns the inside of<div class="card-body">.card_actionsfooter still renders if provided. Use for rich cards with badges, icon-prefixed rows, custom layouts.card_media- Media region (image/thumbnail/video preview) rendered ABOVE the card body, receives item via:let. Use for thumbnails, cover images, document previews. The slot owns its own padding/background — wrap content in a styled container.card_actions- Action buttons rendered in each card footer (receives item via :let)toolbar_title- Title/content rendered at the start of the toolbar rowtoolbar_actions- Buttons rendered in the toolbar before the view toggle
Controlled view mode
By default the card / comfortable / compact toggle is driven entirely
client-side (JS hook + localStorage; comfortable is the default when
nothing is stored). Pass view_mode="card", view_mode="comfy", or
view_mode="table" to take control from the assigns side — the
component then renders ONLY that view (no JS toggle) and the toolbar
buttons emit phx-click={view_event} with
phx-value-mode="card"|"comfy"|"table" so the consumer can drive
state via push_patch (URL-backed) or assign. Use this when the
view choice must survive across LV navigation or be part of the URL.
Attributes
id(:string) - Defaults tonil.class(:any) - Defaults to"".variant(:string) - Defaults to"default". Must be one of"default","zebra","pin-rows", or"pin-cols".size(:string) - Defaults to"md". Must be one of"xs","sm","md", or"lg".toggleable(:boolean) - Defaults tofalse.show_toggle(:boolean) - Defaults totrue.items(:list) - Defaults to[].card_title(:any) - Defaults tonil.card_fields(:any) - Defaults tonil.card_class(:any) - Per-card wrapper class. String or 1-arity fn(item) -> string. When fn, called per item. Defaults to"card card-sm bg-base-200 shadow-sm".storage_key(:string) - Defaults tonil.wrapper_class(:string) - Defaults to"rounded-lg shadow-md overflow-x-auto overflow-y-clip".card_grid_class(:string) - Layout classes for the card-view grid (column density, gaps). Must NOT include adisplayutility (grid/hidden) — the component setsdisplayper view-mode branch so controlled table mode can emithiddencleanly. Override to change column count, e.g. a densergap-4 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5. Must be a literal in a Tailwind-scanned source so the classes are compiled — a dynamically-built string won't be in the generated CSS. Defaults to"gap-4 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4".view_mode(:string) - Controlled view selector. When set, renders ONLY that view and disables the JS toggle; toggle buttons emitview_eventwithphx-value-mode. When nil, falls back to the JS hook + localStorage default. Defaults tonil. Must be one ofnil,"card","table", or"comfy".view_event(:string) - Event name emitted by the toggle buttons in controlled mode. Defaults to"switch_view".on_reorder(:string) - When set, the card-view container becomes a SortableGrid hook target. The table-view's tbody is owned by the inner_block — wire that side separately so desktop users get the same DnD as mobile. Defaults tonil.reorder_scope(:map) - Map of scope values exposed on the card-view container as data-sortable-scope-* attrs. Keys are lowercased and dasherized for the DOM attr; the JS hook sends them back to LV as camelCase, so an Elixir key:category_uuidarrives in the LV handler payload as"categoryUuid". Defaults to%{}.reorder_group(:string) - SortableJS group name for cross-container drag (must match the table-view side). Defaults tonil.item_id(:any) - 1-arity function returning the data-id for a card. Defaults to& &1.uuid. Required when on_reorder is set. Defaults tonil.card_media_class(:string) - Classes for the:card_mediawrapper.Defaults to no class at all, which is exactly what the wrapper was before this attr existed — an existing consumer renders unchanged. Pass
"relative"for a positioning context if the slot puts a corner overlay (a checkbox, a drag handle, a spinner) over the media.For a card GRID, pass a fixed band instead —
"relative h-24 bg-base-200 overflow-hidden"is the shape the catalogue module uses everywhere: a card with a photo and one without are then the same height and the grid stays even. Cards drift apart when each page invents its own, so pick one per module and pass it consistently.Defaults to
nil.Global attributes are accepted.
Slots
inner_block(required)card_header- Custom header for each card (receives item via :let); replaces card_title.card_body- Fully-custom card body (receives item via :let). When present, replaces card_header + card_title + card_fields rendering.card_media- Media region (image/thumbnail/video) rendered above the card body. Receives item via :let. The slot owns its own padding/background; passcard_media_classto frame the wrapper so cards look alike across pages.card_actions- Action buttons in card footer.above_cards- Content rendered inside the card-view container, above the card grid. Hidden automatically when the JS hook switches to table mode (the wrapper hasmd:hiddentoggled on).sort_bar- Always-visible sort UI rendered in its own row above the table/cards (e.g. a<.sort_selector>). Unlike:above_cards, this slot renders in both views.toolbar_title- Title or arbitrary content rendered at the start of the toolbar row.toolbar_actions- Action buttons rendered in the toolbar, before the view toggle.
Renders a table body section.
Accepts arbitrary HTML attrs via :rest so consumers can wire the
SortableGrid hook directly onto the <tbody>:
<.table_default_body
id="endpoints-list-body"
phx-hook="SortableGrid"
data-sortable="true"
data-sortable-event="reorder_endpoints"
data-sortable-items=".sortable-item"
data-sortable-handle=".pk-drag-handle"
>
...
</.table_default_body>Attributes
- Global attributes are accepted.
Slots
inner_block(required)
Renders a table data cell.
Attributes
class- Additional CSS classes (optional)colspan- Number of columns to span (optional)rowspan- Number of rows to span (optional)rest- Additional HTML attributes (optional)
Attributes
class(:any) - Defaults to"".colspan(:integer) - Defaults tonil.rowspan(:integer) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Renders a table header section.
Attributes
class- Overrides the default header styling. Default is"bg-base-300"— a calm, theme-neutral header that reads as a subtle separator from<tbody>instead of the loud daisyUI primary. Pass"bg-primary text-primary-content"to restore the legacy look, orclass=""for a fully bare header. The string fully replaces the default; concatenate manually if you want to add classes on top.
Attributes
class(:any) - Defaults to"bg-base-300".
Slots
inner_block(required)
Renders a table header cell.
:inner_block is optional — a self-closing call (<.table_default_header_cell />)
renders an empty <th>, useful for drag-handle / row-selection columns
that don't need a label.
Attributes
class- Additional CSS classes (optional)rest- Additional HTML attributes (optional)
Attributes
class(:any) - Defaults to"".- Global attributes are accepted.
Slots
inner_block
Renders a table row.
Attributes
class- Additional CSS classes (optional)hover- Enable hover effect: true/false (optional, default: true)rest- Additional HTML attributes (optional)
Attributes
class(:any) - Defaults to"".hover(:boolean) - Defaults totrue.- Global attributes are accepted.
Slots
inner_block(required)