Skip to main content

Cards

Cards are the work items on your board - tasks, tickets, leads, whatever moves through columns. This guide covers how cards carry data, how the built-in layout decides what to show, and how you create, update, move, and delete cards through the action API.

Card data shape​

A card is a plain object with an id and whatever fields your app needs. The widget reads certain well-known field names for its built-in sections, but you're free to attach any extra data for custom templates, filters, or sort comparators.

const cards = [
{
id: 1,
label: "Design homepage",
column: "todo",
description: "Create wireframes and mockups",
priority: 2,
progress: 0.3,
deadline: new Date("2026-08-01"),
tags: ["ui", "design"],
users: [1, 2],
attachments: 3,
comments: 5,
},
];

The column field (or whatever key columnAccessor points to) determines which column a card belongs to. Everything else is optional - the built-in card hides any section whose data is missing.

Fields the built-in card layout reads:

  • label - card title
  • description - short text under the title
  • priority - numeric id resolved against card.priority.data
  • progress - number from 0 to 1
  • deadline - Date, ISO string, or timestamp
  • tags - array of ids resolved against card.tags.data
  • users - array of ids resolved against card.users.data
  • attachments - numeric count
  • comments - numeric count
  • cover - image URL for the cover strip
  • css - static CSS class(es) on the card wrapper

Section visibility (CardShape)​

The card prop takes a CardShape object that controls which sections the built-in card renders. Each key is either a boolean toggle or an object with section-specific options.

<Kanban
:cards="cards"
:columns="columns"
:card="{
priority: true,
description: true,
progress: true,
deadline: true,
}"
/>

Card showing built-in sections: priority badge, description, progress bar, deadline, tags, and user avatars

Omit a key or set it to false to hide the section. A section also won't render if the card object doesn't have data for it - CardShape is an upper bound on visibility, not a guarantee.

The default shape (returned by getCardShape()) enables priority, progress, description, deadline, and tags. Start from that default and adjust:

<script setup>
import { Kanban, getCardShape } from "@svar-ui/vue-kanban";

const card = { ...getCardShape(), users: { data: users }, menu: true };
</script>

<template>
<Kanban :cards="cards" :columns="columns" :card="card" />
</template>

Per-section options​

Several sections take an options object instead of a plain boolean.

Priority​

Pass a data array to map numeric ids to labels and colors:

<Kanban
:cards="cards"
:columns="columns"
:card="{
priority: {
data: [
{ id: 1, label: 'Low', css: 'wx-card-priority-low' },
{ id: 2, label: 'Medium', css: 'wx-card-priority-medium' },
{ id: 3, label: 'High', css: 'wx-card-priority-high' },
],
},
}"
/>

Without data, the card renders the raw priority value. Use getPriorityOptions() to start from the built-in Low / Medium / High list.

Tags​

Pass a data array for label/color resolution and an optional max to cap visible tags:

:card="{
tags: {
max: 3,
data: [
{ id: 'docs', label: 'Docs', css: 'tag-docs' },
{ id: 'ui', label: 'UI', css: 'tag-ui' },
],
},
}"

Users​

Pass a data array with optional avatar images, and max to limit visible avatars:

:card="{
users: {
max: 3,
data: [
{ id: 1, label: 'Alice', img: '/avatars/alice.png' },
{ id: 2, label: 'Bob' },
],
},
}"

When img isn't provided, the avatar shows initials derived from label.

Progress​

Set showLabel: true to display a percentage label next to the progress bar:

:card="{
progress: { showLabel: true },
}"

Deadline​

Provide a format string to control date display. Supported tokens are YYYY, MM, DD, HH, mm:

:card="{
deadline: { format: 'YYYY-MM-DD' },
}"

With deadline: true, the date renders via toLocaleDateString().

Custom card body​

When the built-in sections aren't enough, pass a cardContent slot to replace the entire card body. The wrapper element (selection state, drag affordances, data-id attribute) stays intact - only the inner content is swapped.

<Kanban :cards="cards" :columns="columns">
<template #cardContent="{ card, cardShape }">
<div class="my-card">
<strong>{{ card.label }}</strong>

<span v-if="card.owner">{{ card.owner }}</span>
</div>
</template>
</Kanban>

The slot receives the source card object and the resolved cardShape, so you can still consult the shape flags inside custom markup.

Card styling​

Two mechanisms apply CSS classes to cards.

Static class on the card object. Set a css field on individual cards:

const cards = [
{ id: 1, label: "Urgent fix", column: "doing", css: "card--urgent" },
];

Dynamic class via cardCss callback. Return a class string based on card data:

<Kanban
:cards="cards"
:columns="columns"
:cardCss="(card) =>
card.deadline && new Date(card.deadline) < new Date()
? 'card--overdue'
: ''"
/>

Both stack - card.css and the cardCss return value are both appended to the card wrapper. Column-level styling works the same way through columnCss.

Card menu​

Enable the per-card menu button by setting menu in the card shape. The simplest form uses the built-in options from getMenuOptions() (Edit, Duplicate, Delete):

<Kanban :cards="cards" :columns="columns" :card="{ menu: true }" />

Card with three-dot menu button open showing Edit, Duplicate, and Delete options

For custom menu entries, pass an options array and a click handler:

<script setup>
import { Kanban, getMenuOptions } from "@svar-ui/vue-kanban";

const menuOptions = [
...getMenuOptions(),
{ id: "archive", text: "Archive", icon: "wxi-archive" },
];

function onMenuClick({ action }) {
if (action.id === "archive") {
// handle the custom action
}
}
</script>

<template>
<Kanban
:cards="cards"
:columns="columns"
:card="{ menu: { options: menuOptions, onclick: onMenuClick } }"
/>
</template>

Built-in item ids (edit-card, duplicate-card, delete-card) keep their default store-action wiring regardless of whether they come from the default list or a custom array.

Adding, updating, moving, deleting cards​

All card mutations go through actions dispatched via api.exec(). The widget doesn't modify the source cards array directly - the internal store handles immutable updates and triggers re-rendering.

<script setup>
import { ref } from "vue";
import { Kanban } from "@svar-ui/vue-kanban";

const api = ref(null);
</script>

<template>
<Kanban ref="api" :cards="cards" :columns="columns" />

<button :onclick="() => api.exec('add-card', {
card: { label: 'New task', column: 'todo' },
})">
Add
</button>
</template>

Key actions:

  • add-card - creates a card. Pass edit: true to open the editor on it immediately. Use after to control insertion position.
  • update-card - patches a card by id: api.exec("update-card", { id: 1, card: { progress: 0.5 } }).
  • move-card - moves a card to a different column or reorders within the same column: api.exec("move-card", { id: 1, column: "done", before: null }). Setting before: null places the card at the end.
  • duplicate-card - clones a card with optional overrides: api.exec("duplicate-card", { id: 1, card: { label: "Copy" } }).
  • delete-card - removes a card: api.exec("delete-card", { id: 1 }).

To react to mutations, use event handler props (onaddcard, onupdatecard, onmovecard, etc.) or the api.on() / api.intercept() methods. Interceptors can cancel an action by returning false.

Selection and editor opening​

Clicking a card dispatches select-card, which populates the store's editorData field. If an Editor component is mounted, it opens automatically for the selected card.

<script setup>
import { ref } from "vue";
import { Kanban, Editor } from "@svar-ui/vue-kanban";

const api = ref(null);
</script>

<template>
<Kanban ref="api" :cards="cards" :columns="columns" />
<Editor v-if="api" :api="api" />
</template>

Open or close the editor programmatically:

// Open editor for card 5
api.exec("select-card", { id: 5 });

// Close the editor
api.exec("select-card", { id: null });

Alternatively, pass a cardPopup component to replace the editor with a popup that appears on click:

<Kanban :cards="cards" :columns="columns" :cardPopup="MyPopup" />

The popup component receives { card, close } as props.

Performance​

For boards with hundreds or thousands of cards, enable virtualization through the render prop to keep scroll performance smooth:

<Kanban
:cards="cards"
:columns="columns"
:render="{
virtualizeCards: true,
estimatedCardHeight: 96,
cardOverscan: 5,
}"
/>

This renders only the cards visible in each column's scroll viewport, plus a configurable overscan buffer. For boards with many columns, add virtualizeColumns: true as well.