Kanban board component for PhiaUI.
Renders a horizontal workflow board composed of named columns and draggable
task cards. The layout is CSS-only; drag-and-drop reordering is provided by
the optional PhiaKanban vanilla-JS hook.
When to use
Use KanbanBoard whenever you need a multi-stage workflow visualisation with
discrete status columns — project management, recruitment pipelines, support
ticket queues, content-approval workflows, etc.
Anatomy
| Component | Element | Purpose |
|---|---|---|
kanban_board/1 | div | Horizontal scroll container for all columns |
kanban_column/1 | div | A single workflow stage (label + optional count) |
kanban_card/1 | li | An individual task card with slots |
Priority levels
| Value | Left-border colour |
|---|---|
critical | border-l-destructive (red) |
high | border-l-orange-500 (orange) |
medium | border-l-yellow-500 (yellow) |
low | border-l-muted-foreground/40 |
Minimal example
<.kanban_board id="project-board">
<.kanban_column id="col-todo" label="To Do" count={length(@todo_cards)}>
<.kanban_card
:for={card <- @todo_cards}
id={"card-#{card.id}"}
title={card.title}
priority={card.priority}
/>
</.kanban_column>
</.kanban_board>Full project-management board with LiveView streams
LiveView streams keep DOM diffing minimal — only changed cards are patched, not the entire column.
<.kanban_board id="pm-board" phx-hook="PhiaKanban" data-on-move="card_moved">
<.kanban_column id="col-backlog" label="Backlog" count={@backlog_count}>
<.kanban_card
:for={{dom_id, card} <- @streams.backlog}
id={dom_id}
title={card.title}
priority={card.priority}
phx-click="open_card"
phx-value-id={card.id}
>
<:avatar>
<.avatar size="xs">
<.avatar_image src={card.assignee_avatar} />
<.avatar_fallback name={card.assignee_name} />
</.avatar>
</:avatar>
<:tags>
<.badge :for={label <- card.labels} variant="secondary" size="sm">
{label}
</.badge>
</:tags>
<:footer>
<.icon name="calendar" size={:xs} />
<span>{card.due_date}</span>
<.icon name="message-square" size={:xs} class="ml-auto" />
<span>{card.comment_count}</span>
</:footer>
</.kanban_card>
</.kanban_column>
<.kanban_column id="col-in-progress" label="In Progress" count={@in_progress_count}>
<%!-- ... --%>
</.kanban_column>
<.kanban_column id="col-done" label="Done" count={@done_count}>
<%!-- ... --%>
</.kanban_column>
</.kanban_board>Drag-and-drop setup (optional)
The board works without the hook — cards are static server-rendered items.
Add phx-hook="PhiaKanban" to kanban_board/1 to enable drag-and-drop
reordering. The hook emits a push_event("phx:card_moved", %{id, from, to, index})
on every successful drop.
# app.js
import PhiaKanban from "./hooks/kanban"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { PhiaKanban }
})
# LiveView handler
def handle_event("card_moved", %{"id" => id, "from" => from, "to" => to, "index" => idx}, socket) do
# Update the card's column and position in your database
{:noreply, socket}
end
Summary
Functions
Renders the kanban board root container.
Renders a single kanban task card.
Renders a single kanban workflow column.
Functions
Renders the kanban board root container.
All kanban_column/1 components go inside. The board overflows horizontally
on narrow screens so every column remains accessible without horizontal
viewport scrolling that would break the page layout.
Example
<.kanban_board id="sales-pipeline">
<.kanban_column id="col-lead" label="Lead" count={12}>
...
</.kanban_column>
<.kanban_column id="col-qualified" label="Qualified" count={5}>
...
</.kanban_column>
</.kanban_board>Attributes
id(:string) (required) - DOM id for the board element. Required for LiveView streams and the PhiaKanban hook.class(:string) - Additional CSS classes for the board container. Defaults tonil.- Global attributes are accepted. HTML attributes forwarded to the root element.
Use
phx-hook="PhiaKanban"to enable drag-and-drop reordering. Usedata-on-moveto specify the LiveView event name for card moves. Supports all globals plus:["phx-hook", "data-on-move"].
Slots
inner_block(required) -kanban_column/1children.
Renders a single kanban task card.
Cards are <li> elements inside the column's <ol>, which preserves
correct document semantics for ordered task lists.
The priority-coloured left border gives a quick visual signal without requiring team members to read each card individually.
Example — full card with all slots
<.kanban_card
id={"card-#{task.id}"}
title={task.title}
priority={task.priority}
phx-click="open_task"
phx-value-id={task.id}
>
<:avatar>
<.avatar size="xs">
<.avatar_image src={task.assignee.avatar_url} />
<.avatar_fallback name={task.assignee.name} />
</.avatar>
</:avatar>
<:tags>
<.badge :for={label <- task.labels} variant="secondary" size="sm">
{label.name}
</.badge>
</:tags>
<:footer>
<.icon name="calendar" size={:xs} />
<span>Due {task.due_date}</span>
</:footer>
</.kanban_card>Attributes
id(:string) (required) - DOM id for the card element. Required for LiveView streams (dom_idfrom stream).title(:string) (required) - Card title — the task or item name.priority(:string) - Priority level. Controls the left-border accent colour:critical— red (destructive)high— orangemedium— yellowlow— muted
Defaults to
"medium". Must be one of"critical","high","medium", or"low".class(:string) - Additional CSS classes for the card. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the
<li>card element. Addphx-click="open_card" phx-value-id={card.id}to make cards clickable. The PhiaKanban hook setsdraggable="true"automatically when enabled. Supports all globals plus:["phx-click", "phx-value-id", "draggable"].
Slots
avatar- Optional avatar oravatar_group/1for assignees. Rendered in the top-right of the card header.tags- Optional tag / badge row rendered below the title. Usebadge/1components withvariant="secondary".footer- Optional footer row for metadata such as due dates, comment counts, or story-point estimates. Content is rendered attext-xs text-muted-foreground.
Renders a single kanban workflow column.
Each column has a fixed width of w-64 (256 px) and does not shrink, which
ensures consistent column widths regardless of card content. The count badge
helps teams see at a glance whether a stage has become a bottleneck.
Example
<.kanban_column id="col-review" label="In Review" count={@review_count}>
<.kanban_card
:for={{dom_id, card} <- @streams.review}
id={dom_id}
title={card.title}
priority={card.priority}
/>
</.kanban_column>Attributes
id(:string) (required) - DOM id for the column. Required for the PhiaKanban hook to identify drop zones.label(:string) (required) - Column heading for the workflow stage, e.g. "To Do", "In Progress", "Done".count(:integer) - Optional item count displayed as a badge beside the label. Useful for at-a-glance capacity monitoring. Passnilto omit the badge.Defaults to
nil.class(:string) - Additional CSS classes for the column wrapper. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the column div (e.g.
data-column-idfor the hook).
Slots
inner_block(required) -kanban_card/1children.