Menu

Change language

GitHub repo

Kanban

kanbanui

Composable compound Kanban primitive for building board, pipeline, and agile workflows with tactile card drag-and-drop mechanics.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/kanban.json
Manifest:https://uipkge.dev/r/svelte/kanban.jsonInstalls to:src/lib/components/ui/kanban/

Variants

Loading interactive previews…

Examples in Use

Full Block

Multi-column agile board composing Kanban columns, drag-and-drop task cards, priority indicators, and member avatar stacks.

Explore full block

Props

NameType / ValuesDefaultRequired
isOver
'true''false'
—optional
childrenSnippet—optional
oncardmove

Fired when a card moves within or across columns (pointer drop or keyboard move).

(event: KanbanMoveEvent) => void—optional
refHTMLDivElement | null—optional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

KanbanMoveEvent
interface KanbanMoveEvent {
  cardId: string
  fromColumnId: string
  toColumnId: string
  toIndex?: number
}
KanbanContext
interface KanbanContext {
  readonly draggingCardId: string | null
  readonly draggingColumnId: string | null
  readonly overColumnId: string | null
  /** Set while a card is held by keyboard (Space), not by pointer drag. */
  readonly grabbedCardId: string | null
  setDraggingCard: (cardId: string | null, columnId: string | null) => void
  setOverColumn: (columnId: string | null) => void
  setGrabbedCard: (cardId: string | null) => void
  emitMove: (event: KanbanMoveEvent) => void
  /** Speak a message through the board's polite live region. */
  announce: (message: string) => void
}
KanbanColumnContext
interface KanbanColumnContext {
  columnId: string
  /** True while a dragged (pointer or keyboard-grabbed) card hovers this column. React twin: `isOver`. */
  readonly isOver: boolean
  /** Column label, used to announce keyboard moves. Falls back to the id. */
  readonly label: string | undefined
}

Files installed (18)

  • src/lib/components/ui/kanban/Kanban.svelte2 kB
    <script lang="ts" module>
      import type { Snippet } from 'svelte'
      import type { HTMLAttributes } from 'svelte/elements'
      import type { KanbanMoveEvent } from './context'
    
      export interface KanbanProps extends HTMLAttributes<HTMLDivElement> {
        children?: Snippet
        /** Fired when a card moves within or across columns (pointer drop or keyboard move). */
        oncardmove?: (event: KanbanMoveEvent) => void
        ref?: HTMLDivElement | null
      }
    </script>
    
    <script lang="ts">
      import { cn } from '$lib/utils'
      import { setKanbanContext } from './context'
    
      let { class: className, children, oncardmove, ref = $bindable(null), ...restProps }: KanbanProps = $props()
    
      let draggingCardId = $state<string | null>(null)
      let draggingColumnId = $state<string | null>(null)
      let overColumnId = $state<string | null>(null)
      let grabbedCardId = $state<string | null>(null)
      // Keyboard moves are silent to a screen reader — the card just appears
      // somewhere else. This region narrates pick up / move / drop / cancel.
      let announcement = $state('')
    
      setKanbanContext({
        get draggingCardId() {
          return draggingCardId
        },
        get draggingColumnId() {
          return draggingColumnId
        },
        get overColumnId() {
          return overColumnId
        },
        get grabbedCardId() {
          return grabbedCardId
        },
        setDraggingCard(cardId, columnId) {
          draggingCardId = cardId
          draggingColumnId = columnId
        },
        setOverColumn(columnId) {
          overColumnId = columnId
        },
        setGrabbedCard(cardId) {
          grabbedCardId = cardId
        },
        emitMove(event) {
          oncardmove?.(event)
        },
        announce(message) {
          // Re-assigning the same string would not re-trigger the live region.
          announcement = announcement === message ? `${message} ` : message
        },
      })
    </script>
    
    <div bind:this={ref} data-uipkge="" data-slot="kanban" class={cn('w-full', className)} {...restProps}>
      {@render children?.()}
      <div data-slot="kanban-live-region" class="sr-only" role="status" aria-live="polite" aria-atomic="true">
        {announcement}
      </div>
    </div>
    
  • src/lib/components/ui/kanban/KanbanBoard.svelte0.6 kB
  • src/lib/components/ui/kanban/KanbanColumn.svelte2.4 kB
  • src/lib/components/ui/kanban/KanbanColumnHeader.svelte0.6 kB
  • src/lib/components/ui/kanban/KanbanColumnDot.svelte0.5 kB
  • src/lib/components/ui/kanban/KanbanColumnTitle.svelte0.6 kB
  • src/lib/components/ui/kanban/KanbanColumnCount.svelte0.7 kB
  • src/lib/components/ui/kanban/KanbanColumnAdd.svelte0.9 kB
  • src/lib/components/ui/kanban/KanbanColumnBody.svelte0.6 kB
  • src/lib/components/ui/kanban/KanbanColumnEmpty.svelte0.8 kB
  • src/lib/components/ui/kanban/KanbanCard.svelte6.1 kB
  • src/lib/components/ui/kanban/KanbanCardHeader.svelte0.6 kB
  • src/lib/components/ui/kanban/KanbanCardTitle.svelte0.6 kB
  • src/lib/components/ui/kanban/KanbanCardDescription.svelte0.6 kB
  • src/lib/components/ui/kanban/KanbanCardFooter.svelte0.6 kB
  • src/lib/components/ui/kanban/context.ts1.7 kB
  • src/lib/components/ui/kanban/kanban.variants.ts1.4 kB
  • src/lib/components/ui/kanban/index.ts1.5 kB

Raw manifest:https://uipkge.dev/r/svelte/kanban.json