elements
Demos UI Feed Discuss Install
Style System
  • Style
  • Themes
  • Behaviors
Forms
  • Button
  • ButtonGroup
  • Checkbox
  • ColorPicker
  • Combobox
  • Field
  • FileUpload
  • Input
  • InputGroup
  • InputOTP
  • Label
  • NativeSelect
  • NumberInput
  • PasswordInput
  • RadioGroup
  • Select
  • Slider
  • Switch
  • TagsInput
  • Textarea
  • Toggle
  • ToggleGroup
Overlays
  • AlertDialog
  • Command
  • ContextMenu
  • Dialog
  • Drawer
  • DropdownMenu
  • HoverCard
  • Menubar
  • NavigationMenu
  • Popover
  • Sheet
  • Toast
  • Tooltip
Display
  • Accordion
  • Alert
  • AspectRatio
  • Avatar
  • Badge
  • Breadcrumb
  • Card
  • Collapsible
  • Empty
  • Item
  • Kbd
  • Pagination
  • Progress
  • Separator
  • Skeleton
  • Spinner
  • Table
  • Tabs
  • Tree
Layout
  • Carousel
  • Resizable
  • ScrollArea
  • Sidebar
Dates
  • Calendar
  • DatePicker
  • DateRangePicker
  • TimePicker
Data
  • AreaChart
  • BarChart
  • Chart
  • DataTable
  • Kanban
  • LineChart
  • PieChart
  • RadarChart
  • RadialChart
  • VirtualList
Chat
  • Attachment
  • Message
  • MessageList
  • PromptInput
Elements UI / Data / Kanban

Kanban

A board of columns of cards you reorder and move by pointer, touch or keyboard, with announcements and a one-column phone layout.

Board

Drag a card within a column or across columns, or focus it and use Space and the arrow keys. On a phone the board shows one column at a time, the nav scrolls between them and each card has move buttons.

To do

3
Write the release notes chore
Fix the login redirect loop bug
Add CSV export to reports feature

In progress

2
Invoice PDF layout feature
Timezone off by one in digests bug

Review

1
Rate limit the public API feature

Done

0

todo: t1,t2,t3 | doing: t4,t5 | review: t6 | done:

opened: 0

Code
<Kanban value={state.columns} label="Sprint board">
  <KanbanNav>
    <KanbanNavItem e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id}>{TITLES[id]}</KanbanNavItem>
  </KanbanNav>
  <KanbanBoard>
    <KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c}
                  value={id} title={TITLES[id]} count={state.columns[id].length}>
      <KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id} asHandle>
        {task.title}
        <KanbanItemMove />
      </KanbanItem>
    </KanbanColumn>
  </KanbanBoard>
</Kanban>

Usage

import { Kanban, KanbanBoard, KanbanColumn, KanbanItem, KanbanItemMove, KanbanNav, KanbanNavItem } from "@elements/ui/kanban";

<Kanban value={state.columns} label="Sprint board">
  <KanbanNav>
    <KanbanNavItem e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id}>{TITLES[id]}</KanbanNavItem>
  </KanbanNav>
  <KanbanBoard>
    <KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c}
                  value={id} title={TITLES[id]} count={state.columns[id].length}>
      <KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id}>
        {task.title}
        <KanbanItemMove />
      </KanbanItem>
    </KanbanColumn>
  </KanbanBoard>
</Kanban>

Examples

Handles and columns

With a handle only the grip drags, so the rest of the card stays clickable and selectable. A column handle reorders columns. A handle can be disabled on its own (the Retry failed webhooks grip and the Done column grip) while its card or column stays enabled. Columns scroll on their own past a height.

Backlog

6
Audit log retention chore
Bulk edit for tags feature
Slow search on large teams bug
Dark mode for emails feature
Upgrade Postgres chore
Retry failed webhooks feature

In progress

1
Billing settings page feature

Done

1
SSO for admins (shipped, locked) feature

backlog: h1,h2,h3,h4,h5,h6 | doing: h7 | done: h8

Code
<Kanban value={state.columns}>
  <KanbanBoard>
    <KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]}>
      <KanbanColumnHandle slot="actions" disabled={id === "done"} />
      <KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id}>
        <KanbanItemHandle disabled={task.pinned} />
        {task.title}
      </KanbanItem>
      <Button slot="footer" variant="ghost" size="sm" onclick={() => add(id)}>Add card</Button>
    </KanbanColumn>
  </KanbanBoard>
</Kanban>

Rules

onMove can refuse a move (In progress holds two cards). A disabled card stays put, a disabled column takes no cards and its handle does not drag, and a disabled board does nothing.

To do

3
Draft the onboarding email
Locked: legal review
Empty state for projects

In progress

2
Fix avatar upload
Team invites

Archived

1
Old pricing page

todo: r1,r2,r3 | doing: r4,r5 | archived: r6

Code
<Kanban value={state.columns} disabled={state.frozen} onMove={(e) => limit(e)}>
  <KanbanBoard>
    <KanbanColumn value="todo" title="To do">
      <KanbanItem value="r2" asHandle disabled>Locked: legal review</KanbanItem>
    </KanbanColumn>
    <KanbanColumn value="archived" title="Archived" disabled>
      <KanbanColumnHandle slot="actions" />   <!-- data-disabled, aria-disabled: it does not drag -->
      ...
    </KanbanColumn>
  </KanbanBoard>
</Kanban>

function limit(e: KanbanMoveEvent) {
  if (e.to.column === "doing" && state.columns.doing.length >= 2) {
    e.preventDefault();
  }
}

Plain values and a custom overlay

Strings are their own ids. Bind dragging and fill the overlay slot to draw your own drag preview; dragging.kind tells a column drag from a card drag.

Ideas

Search
Exports
Webhooks

Now

Billing

Shipped

ideas: Search,Exports,Webhooks | now: Billing | shipped: dragging: none moves: 0

Code
<Kanban value={state.columns} dragging={state.dragging}>
  <KanbanBoard>
    <KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]}>
      <KanbanColumnHandle slot="actions" />
      <KanbanItem e:for={name of state.columns[id]} e:key={(n) => n} value={name} asHandle>{name}</KanbanItem>
    </KanbanColumn>
  </KanbanBoard>
  <div slot="overlay" class={["drag-preview", state.dragging?.kind === "column" && "is-column"]} e:if={state.dragging}>
    Moving {state.dragging.id}
  </div>
</Kanban>

Vertical

orientation=vertical stacks the columns. ArrowDown past the last card moves into the next column, and a column handle reorders columns with ArrowUp and ArrowDown.

Now

Payments
Refunds

Next

Disputes

Later

Payouts
Tax

now: Payments,Refunds | next: Disputes | later: Payouts,Tax

Code
<Kanban value={state.columns} orientation="vertical">
  <KanbanBoard>
    <KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]}>
      <KanbanColumnHandle slot="actions" />
      <KanbanItem e:for={name of state.columns[id]} e:key={(n) => n} value={name} asHandle>{name}</KanbanItem>
    </KanbanColumn>
  </KanbanBoard>
</Kanban>

Restrict to an axis

restrictTo=vertical keeps the dragged card on its column's axis, so a pointer only reorders.

Now

One
Two
Three

Next

Four

now: One,Two,Three | next: Four

Code
<Kanban value={state.columns} restrictTo="vertical">...</Kanban>

Right to left

Columns run from the right; ArrowLeft moves a card to the next column.

To do

Alpha
Beta

In progress

Gamma

Done

todo: Alpha,Beta | doing: Gamma | done:

Code
<div dir="rtl">
  <Kanban value={state.columns}>...</Kanban>
</div>

Whole card and column as handle

asHandle on a card makes the whole card drag even with a grip inside. asHandle on a column drags it from its header and background; presses on its cards still move cards.

Column A

Whole card drags
So does its grip

Column B

Press the column background to move it

a: 1,2 | b: 3

Code
<Kanban value={state.columns}>
  <KanbanBoard>
    <KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]} asHandle>
      <KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id} asHandle>
        <KanbanItemHandle />
        {task.title}
      </KanbanItem>
    </KanbanColumn>
  </KanbanBoard>
</Kanban>

Translated, with custom ids

instructions, announcements, roleDescription and the move button labels replace every screen reader text, so all of it can be translated. getItemValue reads an id from any field. attrs sets the board's and each column's id, and class is appended to the KanbanBoard.

Pendiente

Traducir la ayuda
Revisar los textos

Haciendo

Nueva portada

Hecho

pendiente: i18n-1,i18n-2 | haciendo: i18n-3 | hecho:

Code
<Kanban value={state.columns}
        getItemValue={(t) => t.key}
        instructions="Pulsa espacio para mover."
        roleDescription="ordenable"
        attrs={{ id: "tablero" }}
        announcements={{
          start: (a) => `Recogido ${a.label}. Posición ${a.position} de ${a.total}.`,
          over: (a) => ...,
          end: (a) => ...,
          cancel: (a) => ...,
        }}>
  <KanbanBoard class="kanban-demo-board">
    <KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITULOS[id]}
                  attrs={{ id: `columna-${id}` }}>
      <KanbanItem e:for={t of state.columns[id]} e:key={(t) => t.key} value={t.key} asHandle>
        {t.titulo}
        <KanbanItemMove labels={{
          to: "Mover a {column}",
          previous: "Mover a la columna anterior",
          next: "Mover a la columna siguiente",
          noPrevious: "No hay columna anterior",
          noNext: "No hay columna siguiente",
        }} />
      </KanbanItem>
    </KanbanColumn>
  </KanbanBoard>
</Kanban>

Fixed cards and custom keys

A card drags when it has asHandle or a handle; without either it stays where it is and only moves aside for others. keyboardCodes changes the keys that pick up, drop and cancel; pair it with instructions that name them.

This week

Pinned: weekly goals
Ship the changelog
Review open PRs

Later

Plan the offsite

week: f1,f2,f3 | later: f4

Code
<Kanban value={state.columns}
        keyboardCodes={{ start: ["KeyM"], end: ["KeyM", "Enter"], cancel: ["Escape"] }}
        instructions="To pick up a card, press M. Use the arrow keys to move it, M again to drop it, or Escape to cancel.">
  <KanbanBoard>
    <KanbanColumn e:for={id of Object.keys(state.columns)} e:key={(c) => c} value={id} title={TITLES[id]}>
      <!-- Without asHandle or a handle a card does not drag: it only moves aside. -->
      <KanbanItem e:for={task of state.columns[id]} e:key={(t) => t.id} value={task.id} asHandle={!task.pinned}>
        {task.title}
      </KanbanItem>
    </KanbanColumn>
  </KanbanBoard>
</Kanban>

Options

flatCursor keeps the default cursor, autoScroll off leaves scrolling to the user, and restrictTo keeps the dragged copy on a row, inside the board or inside the window. KanbanNav always shows at every width. A column with only a label is named without a title.

restrictTo

Now

Alpha
Beta

Next

Gamma

Someday (label, no title)

Delta

now: Alpha,Beta | next: Gamma | later: Delta

Code
<Kanban value={state.columns} flatCursor autoScroll={false} restrictTo="board">
  <KanbanNav always>...</KanbanNav>
  <KanbanBoard>
    <KanbanColumn value="later" label="Someday">
      <p slot="header">Someday, no title</p>
      ...
    </KanbanColumn>
  </KanbanBoard>
</Kanban>
Reference Kanban in the manual Every part, attribute, slot, key and data attribute.
Prev DataTableNext LineChart
Product
Manual Pricing Compare
Company
History Contact
Legal
Privacy Policy Acceptable Use Terms License Agreement
© Elements Dev Corporation 2026