Kanban
A drag-and-drop board. Cards move between columns or reorder within one. Like DataGrid, Kanban is fully controlled and data-driven — you own the columns array and apply the next one in onChange (v-model in Vue). Uses native HTML5 drag-and-drop with zero dependencies.
Basic
Pass a controlled array of columns, each with its own cards. When a card is dropped, Kanban calls onChange with the next array — store it in state to complete the move. Card ids must be unique across the whole board.
Drag a card to another column — or reorder within one.
import { useState } from "react";
import { Kanban } from "@usevyre/react";
import type { KanbanColumn } from "@usevyre/react";
const [columns, setColumns] = useState<KanbanColumn[]>([
{ id: "todo", title: "To Do", cards: [
{ id: "c1", title: "Spec the API", description: "Define value/onChange shape" },
{ id: "c2", title: "Write docs page" },
]},
{ id: "doing", title: "In Progress", cards: [
{ id: "c3", title: "Drag-and-drop logic", description: "Native HTML5 DnD" },
]},
{ id: "done", title: "Done", cards: [
{ id: "c4", title: "Project kickoff" },
{ id: "c5", title: "Design tokens" },
]},
]);
<Kanban value={columns} onChange={setColumns} /><script setup>
import { ref } from "vue";
import { Kanban } from "@usevyre/vue";
const columns = ref([
{ id: "todo", title: "To Do", cards: [
{ id: "c1", title: "Spec the API", description: "Define value/onChange shape" },
{ id: "c2", title: "Write docs page" },
]},
{ id: "doing", title: "In Progress", cards: [
{ id: "c3", title: "Drag-and-drop logic", description: "Native HTML5 DnD" },
]},
{ id: "done", title: "Done", cards: [
{ id: "c4", title: "Project kickoff" },
{ id: "c5", title: "Design tokens" },
]},
]);
</script>
<template>
<Kanban v-model="columns" />
</template>Custom cards & click handler
Use renderCard (React render prop) or the #card scoped slot (Vue) for custom card content, and onCardClick /@card-click to open a detail view. Clicks are suppressed immediately after a drag so dropping never triggers a click. Compose the body from useVyre primitives — here Item with ItemMedia /ItemContent / ItemActions — so no extra CSS is needed.
Alex
Sam
Riya
Click a card — or drag it to another column.
import {
Kanban, Item, ItemMedia, ItemContent,
ItemTitle, ItemDescription, ItemActions, Avatar, Badge,
} from "@usevyre/react";
<Kanban
value={columns}
onChange={setColumns}
onCardClick={(card) => openDetail(card.id)}
renderCard={(card) => (
<Item variant="plain" size="sm" style={{ padding: 0 }}>
<ItemMedia>
<Avatar fallback={card.description?.[0] ?? "?"} size="sm" />
</ItemMedia>
<ItemContent>
<ItemTitle>{card.title}</ItemTitle>
<ItemDescription>{card.description}</ItemDescription>
</ItemContent>
<ItemActions><Badge variant="teal">{card.id}</Badge></ItemActions>
</Item>
)}
/><Kanban v-model="columns" @card-click="onCardClick">
<template #card="{ card }">
<Item variant="plain" size="sm" style="padding:0">
<ItemMedia>
<Avatar :fallback="card.description?.[0] ?? '?'" size="sm" />
</ItemMedia>
<ItemContent>
<ItemTitle>{{ card.title }}</ItemTitle>
<ItemDescription>{{ card.description }}</ItemDescription>
</ItemContent>
<ItemActions><Badge variant="teal">{{ card.id }}</Badge></ItemActions>
</Item>
</template>
</Kanban>Complex card content
A card only needs id and title — attach any extra fields (assignee, tags, progress, priority…) to the card object and read them back inside renderCard. Each card is automatically wrapped in aCard, so you only supply the body. This example composes ItemContent, TagGroup /Tag, Progress and Avatar — all useVyre components, no custom CSS — and stays fully draggable.
Cards composed from Item, TagGroup, Progress and Avatar — drag still works.
import {
Kanban, ItemContent, ItemTitle, ItemMedia, ItemActions,
Item, TagGroup, Tag, Badge, Progress, Avatar,
} from "@usevyre/react";
// A card only needs { id, title }. Attach any extra app fields
// and read them back in renderCard — no extra CSS needed, the
// content is composed from useVyre components.
interface TaskCard {
id: string; title: string;
assignee: string; initials: string;
tags: string[]; progress: number;
priority: "Low" | "Medium" | "High";
}
const priorityColor = {
High: "danger", Medium: "warning", Low: "default",
} as const;
const [columns, setColumns] = useState<KanbanColumn[]>([
{ id: "todo", title: "To Do", cards: [{
id: "t1", title: "Implement OAuth callback",
assignee: "Alex Kim", initials: "AK",
tags: ["backend", "security"], progress: 20, priority: "High",
} satisfies TaskCard] },
]);
<Kanban
value={columns}
onChange={setColumns}
onCardClick={(card) => openDetail(card.id)}
renderCard={(card) => {
const c = card as unknown as TaskCard;
return (
<ItemContent>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<ItemTitle style={{ flex: 1 }}>{c.title}</ItemTitle>
<Badge variant={priorityColor[c.priority]}>{c.priority}</Badge>
</div>
<TagGroup gap="sm">
{c.tags.map((t) => <Tag key={t}>{t}</Tag>)}
</TagGroup>
<Progress value={c.progress} size="sm" />
<Item variant="plain" size="sm" style={{ padding: 0 }}>
<ItemMedia><Avatar fallback={c.initials} size="sm" /></ItemMedia>
<ItemContent><ItemTitle>{c.assignee}</ItemTitle></ItemContent>
<ItemActions>{c.progress}%</ItemActions>
</Item>
</ItemContent>
);
}}
/><script setup>
import {
Kanban, ItemContent, ItemTitle, ItemMedia, ItemActions,
Item, TagGroup, Tag, Badge, Progress, Avatar,
} from "@usevyre/vue";
const priorityColor = {
High: "danger", Medium: "warning", Low: "default",
};
// columns: ref of KanbanColumn[]; cards carry extra fields
// (assignee, tags, progress, priority) read in the #card slot.
</script>
<template>
<Kanban v-model="columns" @card-click="openDetail">
<template #card="{ card }">
<ItemContent>
<div style="display:flex; align-items:center; gap:8px">
<ItemTitle style="flex:1">{{ card.title }}</ItemTitle>
<Badge :variant="priorityColor[card.priority]">
{{ card.priority }}
</Badge>
</div>
<TagGroup gap="sm">
<Tag v-for="t in card.tags" :key="t">{{ t }}</Tag>
</TagGroup>
<Progress :value="card.progress" size="sm" />
<Item variant="plain" size="sm" style="padding:0">
<ItemMedia><Avatar :fallback="card.initials" size="sm" /></ItemMedia>
<ItemContent><ItemTitle>{{ card.assignee }}</ItemTitle></ItemContent>
<ItemActions>{{ card.progress }}%</ItemActions>
</Item>
</ItemContent>
</template>
</Kanban>
</template>Custom colors
Set color on a column to tint the whole column, or on an individual card to tint just that card. Values are semantic and token-based, so they adapt to light/dark themes automatically. Available:accent, teal, success,warning, danger (and the implicit default).
Columns and individual cards carry a semantic color.
import { Kanban } from "@usevyre/react";
import type { KanbanColumn } from "@usevyre/react";
// color is a semantic tint: "accent" | "teal" | "success"
// | "warning" | "danger" | "default"
const [columns, setColumns] = useState<KanbanColumn[]>([
{ id: "active", title: "Active", color: "teal", cards: [
{ id: "g2", title: "API rate limiting", color: "warning" },
]},
{ id: "blocked", title: "Blocked", color: "danger", cards: [
{ id: "g4", title: "Vendor SDK upgrade", color: "danger" },
]},
{ id: "shipped", title: "Shipped", color: "success", cards: [
{ id: "g5", title: "Onboarding flow", color: "success" },
]},
]);
<Kanban value={columns} onChange={setColumns} /><script setup>
import { ref } from "vue";
import { Kanban } from "@usevyre/vue";
const columns = ref([
{ id: "active", title: "Active", color: "teal", cards: [
{ id: "g2", title: "API rate limiting", color: "warning" },
]},
{ id: "blocked", title: "Blocked", color: "danger", cards: [
{ id: "g4", title: "Vendor SDK upgrade", color: "danger" },
]},
{ id: "shipped", title: "Shipped", color: "success", cards: [
{ id: "g5", title: "Onboarding flow", color: "success" },
]},
]);
</script>
<template>
<Kanban v-model="columns" />
</template>Props
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value / v-model | KanbanColumn[] | — | Controlled board data. KanbanColumn = { id, title, cards, color? }. KanbanCard = { id, title, description?, color? }. Card ids must be unique across the whole board. |
onChange / @update:modelValue | (next: KanbanColumn[]) => void | — | Called with the next columns array after a drag move. Apply it back to your state — Kanban holds no internal data. |
renderCard | (card, column) => ReactNode | — | Custom card body, rendered inside the wrapping Card. React: render prop. Vue: #card scoped slot. Can return any components. |
onCardClick / @card-click | (card, column) => void | — | Fired on click / Enter / Space (suppressed right after a drag). |
className / class | string | — | Additional CSS class on the board root. |
Color values
Props
| Prop | Type | Default | Description |
|---|---|---|---|
color (column & card) | "default" | "accent" | "teal" | "success" | "warning" | "danger" | "default" | Semantic background tint. Set on a KanbanColumn to tint the whole column, or on a KanbanCard to tint that card. Token-based — adapts to theme. |
Common AI mistakes
- Kanban without onChange (or ignoring it)→ Store columns in state and setColumns in onChange (v-model in Vue)
- Duplicate card ids across columns→ Use globally-unique card ids across the entire board
- Mutating value in place then calling onChange→ Pass the new array Kanban gives you straight to setState / v-model
- color="blue" (or any non-semantic value)→ Use one of: "default" | "accent" | "teal" | "success" | "warning" | "danger"
Quick examples
const [columns, setColumns] = useState([
{ id: "todo", title: "To Do", cards: [{ id: "1", title: "Spec API" }] },
{ id: "doing", title: "In Progress", cards: [] },
{ id: "done", title: "Done", cards: [{ id: "2", title: "Kickoff" }] },
]);
<Kanban value={columns} onChange={setColumns} /><Kanban
value={columns}
onChange={setColumns}
onCardClick={(card) => openDetail(card.id)}
renderCard={(card) => (
<><strong>{card.title}</strong><Badge>{card.id}</Badge></>
)}
/>