Aura Design System

Kanban

Columns of cards that move backward and forward.

Preview

Loading...
import { useState } from "react";
import { Kanban, type KanbanColumn } from "@/components/ui/Kanban";

export function KanbanDemo() {
  const [columns, setColumns] = useState(initial);
  return (
    <Kanban
      columns={columns}
      onMove={(cardId, columnId) => {
        setColumns((current) => {
          const card = current.flatMap((column) => column.cards).find((item) => item.id === cardId);
          if (!card) return current;
          return current.map((column) => ({
            ...column,
            cards:
              column.id === columnId
                ? [...column.cards.filter((item) => item.id !== cardId), card]
                : column.cards.filter((item) => item.id !== cardId),
          }));
        });
      }}
    />
  );
}

Installation

Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces

pnpm dlx shadcn@latest add @aura/kanban

Manual

Copy and paste the class names utility into your utils/class-names.ts file.

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Copy and paste the Kanban component into your components/ui/Kanban.tsx file.

"use client";

import * as React from "react";
import { cn } from "@/utils/class-names";

type KanbanCard = { id: string; title: string };
type KanbanColumn = { id: string; title: string; cards: KanbanCard[] };

function Kanban({
  className,
  columns,
  onMove,
  label = "Board",
}: {
  className?: string;
  columns: KanbanColumn[];
  onMove?: (cardId: string, columnId: string) => void;
  label?: string;
}) {
  return (
    <div role="list" aria-label={label} data-slot="kanban" className={cn("flex gap-1", className)}>
      {columns.map((column, columnIndex) => (
        <section key={column.id} aria-label={column.title} className="flex w-16 flex-col gap-0.5 rounded-sm bg-gray-2 p-0.5">
          <p className="m-0 px-0.5 font-medium text-gray-12">{column.title}</p>
          {column.cards.map((card) => (
            <article key={card.id} className="rounded-sm border border-gray-6 bg-gray-1 p-0.5">
              <p className="m-0">{card.title}</p>
              <div className="mt-0.5 flex gap-0.5">
                <button
                  type="button"
                  className="rounded-sm px-0.5 hover:bg-gray-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-8"
                  disabled={columnIndex === 0}
                  onClick={() => onMove?.(card.id, columns[columnIndex - 1].id)}
                >
                  Back
                </button>
                <button
                  type="button"
                  className="rounded-sm px-0.5 hover:bg-gray-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-8"
                  disabled={columnIndex === columns.length - 1}
                  onClick={() => onMove?.(card.id, columns[columnIndex + 1].id)}
                >
                  Next
                </button>
              </div>
            </article>
          ))}
        </section>
      ))}
    </div>
  );
}

export { Kanban };
export type { KanbanCard, KanbanColumn };