Rapid Rails UI Pro

Upgrade to unlock this component

Get Pro →

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.

To Do

Fix login bug

Priority: High

Update user docs

Priority: Low

In Progress

Design new dashboard

Priority: Medium

Done

Deploy v2.0

Completed

Basic Kanban Board
<%= 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

Backlog

Inherits board color

Active

Also inherits info

Board Default Color
<%= 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

To Do

Default zinc

In Progress

Warning accent

Done

Success accent

Column Color Override
<%= 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.

Tasks

Critical bug

Red accent

Performance issue

Yellow accent

Feature request

Green accent

Documentation

Blue accent

Card Accent Colors
<%= 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

Compact

Small card

Another card

Base (Default)

Default

Base card

Another card

Large

Spacious

Large card

Another card

Sizes
<%= 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

Rounded corners

Square

Square

Square corners

Shapes
<%= 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.

Backlog 5

Task one

In Progress 2

Active task

Badge Count
<%= 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.

Custom Header 3

Card with custom header

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.

Backlog
Column Footer
<%= 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.

Rich Cards
Bug #1234

Login fails on Safari

Reported 2 hours ago

Feature #1235

Add dark mode toggle

Requested by 12 users

Rich Card Content
<%= 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.

Scrollable

Card 1

Card 2

Card 3

Card 4

Card 5

Card 6

Card 7

Card 8

Scrollable Columns
<%= 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.

In Progress 2

Active task 1

Active task 2

Backlog

Waiting

WIP Limits
<%= 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.

Status Overview

Read-only card

Cannot be dragged

Static Board
<%= 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.

Auto-PATCH on Move
<%= 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

Rails 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 Request
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
Event Listener
// 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" with aria-label="Kanban board"
  • Card lists: role="list" with aria-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.

Related Components