Kanban Board
Drag-and-drop kanban board for organizing content into columns. Columns hold arbitrary cards — tickets, deals, profiles, contacts, anything.
Key Features
- HTML5 Native Drag & Drop — No external JS libraries, uses the native Drag and Drop API
- Stimulus.js Powered — Declarative behavior via data attributes
- Arbitrary Card Content — Cards hold any ERB: partials, components, raw HTML
- Column Accents — Color-coded top borders per column
- Card Accents — Color-coded left borders per card
- WIP Limits — Max cards per column, drops rejected when full
- Custom Headers & Footers — Override column header or add footer content
- Server Persistence — Auto-PATCH on card move with Turbo Stream support
- 3 Sizes — sm, base, lg
- 2 Shapes — Rounded or square corners
- Scrollable Columns — Set max height for card area overflow
- Static Mode — Disable drag-and-drop for read-only boards
- Full Keyboard Navigation — Space, Arrows, Escape for grab/move/drop/cancel
- WCAG 2.2 Accessible — ARIA roles, live region for screen reader announcements
Basic Usage
Create a board with rui_kanban, add columns with board.with_column, and cards with col.with_card. Card content is purely block-based.
Fix login bug
Priority: High
Update user docs
Priority: Low
Design new dashboard
Priority: Medium
Deploy v2.0
Completed
<%= rui_kanban do |board| %>
<% board.with_column(title: "To Do", id: "todo") do |col| %>
<% col.with_card(id: "card-1") do %>
<p class="font-medium">Fix login bug</p>
<p class="text-xs text-zinc-500">Priority: High</p>
<% end %>
<% end %>
<% board.with_column(title: "In Progress", id: "wip") do |col| %>
<% col.with_card(id: "card-2") do %>
<p class="font-medium">Design new dashboard</p>
<% end %>
<% end %>
<% board.with_column(title: "Done", id: "done") do |col| %>
<% col.with_card(id: "card-3") do %>
<p class="font-medium">Deploy v2.0</p>
<% end %>
<% end %>
<% end %>
Colors
Colors cascade: board default → column override → card accent. Column color controls the top border, card color controls the left border.
Board Default Color
Inherits board color
Also inherits info
<%= rui_kanban(color: :info) do |board| %>
<% board.with_column(title: "Backlog", id: "backlog") do |col| %>
<% col.with_card(id: "c1") { "Inherits board color" } %>
<% end %>
<% end %>
Column Color Override
Default zinc
Warning accent
Success accent
<%= rui_kanban do |board| %>
<% board.with_column(title: "To Do", id: "todo", color: :zinc) do |col| %>
<% col.with_card(id: "c1") { "Default" } %>
<% end %>
<% board.with_column(title: "In Progress", id: "wip", color: :warning) do |col| %>
<% col.with_card(id: "c2") { "Warning" } %>
<% end %>
<% board.with_column(title: "Done", id: "done", color: :success) do |col| %>
<% col.with_card(id: "c3") { "Success" } %>
<% end %>
<% end %>
Card Accent Colors
Cards can have their own left-border accent color, independent of the column color.
Critical bug
Red accent
Performance issue
Yellow accent
Feature request
Green accent
Documentation
Blue accent
<%= rui_kanban do |board| %>
<% board.with_column(title: "Tasks", id: "tasks") do |col| %>
<% col.with_card(id: "c1", color: :danger) do %>
<p class="font-medium">Critical bug</p>
<% end %>
<% col.with_card(id: "c2", color: :warning) do %>
<p class="font-medium">Performance issue</p>
<% end %>
<% col.with_card(id: "c3", color: :success) do %>
<p class="font-medium">Feature request</p>
<% end %>
<% end %>
<% end %>
Sizes
Three size variants control padding and text sizing across the entire board.
Small
Small card
Another card
Base (Default)
Base card
Another card
Large
Large card
Another card
<%= rui_kanban(size: :sm) do |board| %>
<% end %>
<%= rui_kanban(size: :base) do |board| %>
<% end %>
<%= rui_kanban(size: :lg) do |board| %>
<% end %>
Shapes
Choose between rounded or square corners for columns and cards.
Rounded (Default)
Rounded corners
Square
Square corners
<%= rui_kanban(shape: :rounded) do |board| %>
<% end %>
<%= rui_kanban(shape: :square) do |board| %>
<% end %>
Column Features
Badge Count
Show a card count badge in the column header.
Task one
Active task
<%= rui_kanban do |board| %>
<% board.with_column(title: "Backlog", id: "backlog", badge_count: 5) do |col| %>
<% col.with_card(id: "c1") { "Task" } %>
<% end %>
<% end %>
Custom Header
Override the default title + badge header with any content via col.with_header.
Card with custom header
<%= rui_kanban do |board| %>
<% board.with_column(title: "ignored", id: "col") do |col| %>
<% col.with_header do %>
<div class="flex items-center gap-2">
<span class="font-bold">Custom</span>
<span class="text-xs bg-red-100 text-red-600 px-1 rounded">3</span>
</div>
<% end %>
<% end %>
<% end %>
Column Footer
Add footer content via col.with_footer — great for "Add card" buttons.
<%= rui_kanban do |board| %>
<% board.with_column(title: "Backlog", id: "backlog") do |col| %>
<% col.with_card(id: "c1") { "Task" } %>
<% col.with_footer do %>
<button class="text-sm text-zinc-500 hover:text-zinc-700">+ Add Card</button>
<% end %>
<% end %>
<% end %>
Card Features
Cards accept any block content — partials, other RUI components, raw HTML. This is the kanban's primary strength: no fixed card schema.
Login fails on Safari
Reported 2 hours ago
Add dark mode toggle
Requested by 12 users
<%= rui_kanban do |board| %>
<% board.with_column(title: "Tasks", id: "tasks") do |col| %>
<% col.with_card(id: "c1", color: :danger) do %>
<div class="flex items-center gap-2 mb-2">
<%= rui_badge(text: "Bug", color: :danger, size: :sm) %>
<span class="text-xs text-zinc-500">#1234</span>
</div>
<p class="font-medium">Login fails on Safari</p>
<p class="text-sm text-zinc-500 mt-1">Reported 2 hours ago</p>
<% end %>
<% end %>
<% end %>
Scrollable Columns
Set max_height to limit the card area height. Cards scroll within the column when they exceed the limit.
Card 1
Card 2
Card 3
Card 4
Card 5
Card 6
Card 7
Card 8
<%= rui_kanban(max_height: "200px") do |board| %>
<% board.with_column(title: "Scrollable", id: "col") do |col| %>
<% end %>
<% end %>
WIP Limits
Set max_cards on a column to enforce work-in-progress limits. Card drops are rejected when the column is full.
Active task 1
Active task 2
Waiting
<%= rui_kanban do |board| %>
<% board.with_column(title: "In Progress", id: "wip", max_cards: 3, badge_count: 2) do |col| %>
<% end %>
<% end %>
Static Board (No Drag)
Set draggable: false for a read-only view. Cards cannot be grabbed or moved.
Read-only card
Cannot be dragged
<%= rui_kanban(draggable: false) do |board| %>
<% end %>
Server Persistence
Set move_url to auto-PATCH when a card is dropped. The controller receives JSON with card_id, column_id, and position. Accepts Turbo Stream responses for server-driven UI updates.
<%= rui_kanban(move_url: kanban_move_path) do |board| %>
<% board.with_column(title: "To Do", id: "todo") do |col| %>
<% col.with_card(id: "card-1") { "Task" } %>
<% end %>
<% end %>
Server-Side Controller
# app/controllers/kanban_controller.rb
class KanbanController < ApplicationController
def move
card = Card.find(params[:card_id])
card.update!(
column: params[:column_id],
position: params[:position]
)
# Option 1: Respond with Turbo Stream
respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace(card) }
format.json { head :ok }
end
end
end
Request Payload
PATCH /kanban/move
Content-Type: application/json
{
"card_id": "card-1",
"column_id": "done",
"position": 0
}
Events
Listen for custom DOM events to handle card moves without auto-PATCH, or to react to keyboard interactions.
| Event | Detail | When |
|---|---|---|
| kanban:move | { cardId, fromColumnId, toColumnId, position } |
Card dropped in new position |
| kanban:move-failed | { cardId, error } |
Server persist failed |
| kanban:grab | { cardId } |
Keyboard grab activated |
| kanban:cancel | { cardId } |
Grab cancelled |
// Listen for card moves without auto-PATCH
document.addEventListener("kanban:move", (event) => {
const { cardId, fromColumnId, toColumnId, position } = event.detail
console.log(`Card ${cardId} moved from ${fromColumnId} to ${toColumnId} at position ${position}`)
})
Keyboard Navigation
Full keyboard support for grab, move, drop, and cancel operations — meeting WCAG 2.2 SC 2.5.7.
| Key | Action |
|---|---|
| Space | Grab / drop card |
| Arrow Up / Arrow Down | Move within column |
| Arrow Left / Arrow Right | Move to adjacent column |
| Escape | Cancel move, restore original position |
Accessibility
Built-in Accessibility
- Board:
role="region"witharia-label="Kanban board" - Card lists:
role="list"witharia-label="{column title} cards" - Cards:
role="listitem",aria-roledescription="draggable card" - Live region:
aria-live="assertive"announces all moves to screen readers - Keyboard: Full grab/move/drop/cancel via Space, Arrows, Escape
- Focus management: Draggable cards are focusable (
tabindex="0")
API Reference
rui_kanban
Drag-and-drop kanban board for organizing content into columns
| Parameter | Type | Default | Description |
|---|---|---|---|
| id | String |
auto-generated
|
Board HTML id |
| color | Symbol |
:zinc
|
Default column accent color |
| size | Symbol |
:base
|
Board size — :sm, :base, :lg |
| shape | Symbol |
:rounded
|
Corner shape — :rounded, :square |
| column_width | String |
"320px"
|
CSS width per column |
| max_height | String | — | Max height for card area (enables scrolling) |
| draggable | Boolean |
true
|
Enable drag-and-drop |
| move_url | String | — | URL for auto-PATCH on card move |
Column (board.with_column)
A column in the kanban board
| Parameter | Type | Default | Description |
|---|---|---|---|
| id | String |
—
|
Column identifier (required) |
| title | String |
—
|
Header text (required) |
| color | Symbol |
inherits
|
Column accent color (top border) |
| badge_count | Integer | — | Card count shown in header |
| max_cards | Integer | — | Maximum cards allowed (disables drops when full) |
| droppable | Boolean |
true
|
Whether this column accepts card drops |
Card (col.with_card)
A card inside a kanban column
| Parameter | Type | Default | Description |
|---|---|---|---|
| id | String |
—
|
Card identifier (required) |
| color | Symbol | — | Left-border accent color |
| draggable | Boolean |
inherits
|
Whether this card can be dragged |
Slots
Content slots for customizing component parts
| Slot | Description |
|---|---|
| column | Add a column to the board via board.with_column(id:, title:). Yields the column for adding cards. |
| card | Add a card to a column via col.with_card(id:). Content is block-based — put anything inside. |
| header | Override the default column header via col.with_header. Replaces title + badge. |
| footer | Add footer content to a column via col.with_footer. Great for 'Add card' buttons. |