Skip to main content

Card Menu

The kanban board supports context menus for card-level actions - editing, duplicating, deleting, and anything custom you add. You can trigger the menu through right-click or through a visible button on each card, and control which items appear on a per-card basis.

How the menu works

The card menu has two entry points that share the same options and handlers:

  • Right-click context menu - wrap <Kanban> in a <ContextMenu> component. Right-clicking any card opens the menu at the pointer.
  • Per-card menu button - enable the menu section in the card prop. A three-dot button appears on each card and opens the same menu on left-click.

Both entry points use the same default options from getMenuOptions(): Edit card, Duplicate card, and Delete card. Each default item maps to a store action (select-card, duplicate-card, delete-card) that fires automatically when clicked. Custom items you add are surfaced through the onclick callback instead.

When you enable both entry points, they share one menu instance and one configuration. Users get a consistent experience regardless of how they open the menu.

Right-click context menu

Wrap <Kanban> in <ContextMenu> and pass the kanban API reference. The wrapper listens for contextmenu events, resolves the clicked card from the DOM, and opens the menu.

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

const api = ref();
</script>

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

Board with right-click context menu open on a card showing Edit, Duplicate, and Delete options

The Editor is optional but makes the built-in "Edit card" action useful - it opens the editor for the clicked card.

Per-card menu trigger

Instead of (or alongside) right-click, you can show a three-dot button directly on each card. Set menu to true in the card prop:

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

The button appears in the card header and opens the default menu on click. No <ContextMenu> wrapper is needed - the kanban handles it internally.

To use both entry points together, enable card.menu and wrap in <ContextMenu>:

<template>
<ContextMenu :api="api">
<Kanban
ref="api"
:cards="cards"
:columns="columns"
:card="{ menu: true }"
/>
</ContextMenu>
</template>

Default options

When no custom options are provided, the menu shows three items from getMenuOptions():

ItemAction dispatched
Edit cardselect-card (opens the editor)
Duplicate cardduplicate-card
Delete carddelete-card

These built-in items dispatch their store actions automatically. You don't need an onclick handler for them.

Custom items

Extend the menu by spreading getMenuOptions() and adding your own entries. Handle custom item IDs in the onclick callback - only built-in IDs dispatch store actions automatically.

For the right-click wrapper:

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

const api = ref();

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

function onclick({ action }) {
if (action.id === "archive-card") {
api.value.exec("update-card", {
id: action.context.id,
card: { archived: true },
});
}
}
</script>

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

For the per-card button, pass the same configuration through card.menu:

<template>
<Kanban
:cards="cards"
:columns="columns"
:card="{
menu: {
options: [
...getMenuOptions(),
{ id: 'archive-card', text: 'Archive', icon: 'wxi-empty' },
],
onclick: ({ action }) => { /* handle custom items */ },
},
}"
/>
</template>

When both entry points are active, pass the same options and onclick to both to keep the menu consistent:

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

const api = ref();

const options = [
...getMenuOptions(),
{ id: "my-action", text: "My action", icon: "wxi-empty" },
];

function onclick({ action }) {
if (action.id === "my-action") {
// handle custom action
}
}
</script>

<template>
<ContextMenu :api="api" :options="options" :onclick="onclick">
<Kanban
ref="api"
:cards="cards"
:columns="columns"
:card="{ menu: { options, onclick } }"
/>
</ContextMenu>
<Editor v-if="api" :api="api" />
</template>

Filtering items per card

Use resolver to block the menu entirely for certain cards, and filter to hide individual items based on card data.

resolver receives the resolved card and the mouse event. Return a falsy value to suppress the menu for that card:

<script setup>
function resolver(card) {
return !card.readOnly;
}

function filter(item, card) {
if (card.locked && item.id === "delete-card") return false;
return true;
}
</script>

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

In this example, read-only cards show no menu at all, while locked cards see the menu but without the "Delete card" option.

The per-card button entry point supports filter and onclick but not resolver. To conditionally hide the button itself, toggle card.menu based on your data or hide all items through filter.