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:

import { Kanban, getCardShape } from "@svar-ui/react-kanban";

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

<Kanban cards={cards} columns={columns} card={card} />

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 render function 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}>
{({ card, cardShape }) => (
<div className="my-card">
<strong>{card.label}</strong>

{card.owner && (
<span>{card.owner}</span>
)}
</div>
)}
</Kanban>

The render function 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:

import { Kanban, getMenuOptions } from "@svar-ui/react-kanban";

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

const onMenuClick = ({ action }) => {
if (action.id === "archive") {
// handle the custom action
}
};

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

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.

import { Kanban } from "@svar-ui/react-kanban";
import { useRef } from "react";

function App() {
const apiRef = useRef(null);

return (
<>
<Kanban ref={apiRef} cards={cards} columns={columns} />
<button onClick={() => apiRef.current.exec("add-card", {
card: { label: "New task", column: "todo" },
})}>
Add
</button>
</>
);
}

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.

import { Kanban, Editor } from "@svar-ui/react-kanban";
import { useState } from "react";

function App() {
const [api, setApi] = useState(null);

return (
<>
<Kanban init={setApi} cards={cards} columns={columns} />
{api && <Editor api={api} />}
</>
);
}

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.