<!-- https://svgrid.com/docs/help/rows/kanban-board/ - SvGrid documentation as markdown. Index of every page: https://svgrid.com/llms.txt -->

# Kanban board mode

Set one `board` prop and the grid renders its rows as **cards in horizontal
lanes** instead of a table. It is the same `<SvGrid>`, the same `data` and
`columns` - only the presentation changes. Lanes are bucketed by a field;
dragging a card to another lane tells you to reassign that field on your own
data.

For a board built up option by option, see [Build a sprint board](https://svgrid.com/docs/help/kanban/sprint-board/); this page is the reference for everything the `board` prop can say.

![The same grid rows bucketed into lanes by a groupBy field, each row a card, with a drag reassigning the field.](https://svgrid.com/docs-media/grid-kanban.svg)

> Live demo: Kanban board mode - https://svgrid.com/demos/343-kanban-board/

> Video: Kanban board mode in SvGrid

Every example below runs against this setup. The board *renderer* ships in
`@svgrid/enterprise`, so `enableBoardView()` is what lights the view up - the
`board` prop itself is part of the free grid.

```svelte
<script lang="ts">
  import { SvGrid, SvAvatar, SvChip, type GridColumns } from '@svgrid/grid'
  import { enableBoardView } from '@svgrid/enterprise'

  enableBoardView()

  // Wider than the minimum on purpose: the later examples reach for swimlanes,
  // facets, flags and cover images, and one shared row type keeps every block
  // on this page runnable against the same data.
  type Task = {
    id: number
    title: string
    status: 'backlog' | 'in_progress' | 'review' | 'done'
    assignee: string
    priority: 'Low' | 'Medium' | 'High'
    points: number
    tags: string[]
    labels: string[]
    blocked: boolean
    comments: number
    attachments: number
    due: string
    created: string
    cover?: string
    stories?: Task[]
    assignees: string[]
  }

  const task = (
    id: number,
    title: string,
    status: Task['status'],
    assignee: string,
    priority: Task['priority'],
    points: number,
    extra: Partial<Task> = {},
  ): Task => ({
    id, title, status, assignee, priority, points,
    tags: [], labels: [], blocked: false, comments: 0, attachments: 0, assignees: [assignee],
    due: '2026-07-01', created: '2026-06-01',
    ...extra,
  })

  let data = $state<Task[]>([
    task(1, 'API auth flow',       'backlog',     'Sam', 'High',   5, { tags: ['api'] }),
    task(2, 'Board drag-and-drop', 'in_progress', 'Lee', 'High',   8, { comments: 3, blocked: true }),
    task(3, 'Empty states',        'in_progress', 'Sam', 'Low',    2, { tags: ['ui'] }),
    task(4, 'Dark theme audit',    'review',      'Ada', 'Medium', 3, { attachments: 2 }),
    task(5, 'Release 1.3.0',       'done',        'Ada', 'High',   5),
  ])

  // Referenced by the selection example.
  let count = $state(0)

  const columns: GridColumns<Task> = [
    { field: 'title',    header: 'Task',     width: 240 },
    { field: 'assignee', header: 'Assignee', width: 140 },
    { field: 'priority', header: 'Priority', width: 110 },
    { field: 'points',   header: 'Points',   width: 90 },
    { field: 'status',   header: 'Status',   width: 130 },
  ]

  let nextId = 100
  const uid = () => ++nextId
  const addRow = (laneId: Task["status"]) => {
    data = [...data, task(uid(), "New task", laneId, "Sam", "Low", 1)]
  }
  const remove = (row: Task) => {
    data = data.filter((r) => r !== row)
  }
  const clearLane = (laneId: Task["status"]) => {
    data = data.filter((r) => r.status !== laneId)
  }
  const save = (row: Task) => console.log("save", row.id)
  const row = data[0]!

  const lanes = [
    { id: 'backlog',     title: 'Backlog' },
    { id: 'in_progress', title: 'In progress', color: '#f59e0b', wipLimit: 3 },
    { id: 'review',      title: 'Review',      color: '#8b5cf6' },
    { id: 'done',        title: 'Done',        color: '#22c55e' },
  ]
</script>
```

## The minimum

Point `board.groupBy` at the field that decides the lane. With nothing else,
lanes are derived from the distinct values found in the data (first-seen
order), and each row renders as a default card built from your columns.

```svelte
<SvGrid {data} {columns} board={{ groupBy: 'status' }} />
```

## Ordered lanes, titles, colors, WIP limits

Pass an explicit `lanes` array to fix the order, give friendly titles, and add
an accent color or a work-in-progress limit. A lane id equals the `groupBy`
field value of the cards it holds; a lane with no cards shows a placeholder.

```svelte
<SvGrid {data} {columns}
  board={{
    groupBy: 'status',
    lanes: [
      { id: 'backlog',     title: 'Backlog' },
      { id: 'in_progress', title: 'In progress', color: '#f59e0b', wipLimit: 3 },
      { id: 'review',      title: 'Review',      color: '#8b5cf6' },
      { id: 'done',        title: 'Done',        color: '#22c55e' },
    ],
  }} />
```

The lane header always shows its card count; when the count exceeds `wipLimit`
it is flagged (shown as `count/limit`). Set **`enforceWip`** to make the limit
hard - a drag or keyboard move that would push a lane past its limit is
rejected and the card snaps back (announced to screen readers).

```svelte
<SvGrid {data} {columns}
  board={{ groupBy: 'status', enforceWip: true, lanes }} />
```

## Lane summaries (roll-ups)

Pass `laneSummary` to show a per-lane total in the header - story points, a
dollar sum, anything. It receives the lane's cards (the current swimlane's when
`swimlaneBy` is set) and returns a short string. `swimlaneSummary` does the same
for a whole swimlane band.

```svelte
<SvGrid {data} {columns}
  board={{
    groupBy: 'status',
    laneSummary: (rows) => `${rows.reduce((s, r) => s + r.points, 0)} pts`,
    swimlaneSummary: (rows) => `${rows.reduce((s, r) => s + r.points, 0)} pts`,
  }} />
```

## Search, filter and sort

The board renders the grid's **filtered and sorted** row model, not the raw
data - so column filters, the grid's sort, and the built-in search box all flow
through to the lanes. A search box shows above the board by default; set
`searchable: false` to hide it, or `searchPlaceholder` to change its text.

```svelte
<SvGrid {data} {columns} sortable
  board={{ groupBy: 'status', searchPlaceholder: 'Find a task...' }} />
```

## Swimlanes

Set `swimlaneBy` to split the board into horizontal **swimlane bands** by a
second field. Each band shows the full lane set with only its own cards.
Dragging a card into a lane under a different band reassigns the swimlane field
too - `onCardMove` then carries `fromSwimlane` / `toSwimlane`. Add
`collapsibleSwimlanes` to let users fold a whole band via its header chevron.

```svelte
<SvGrid {data} {columns}
  board={{
    groupBy: 'status',
    swimlaneBy: 'assignee',
    onCardMove: (e) => {
      e.row.status = e.toLane
      if (e.toSwimlane != null) e.row.assignee = e.toSwimlane
    },
  }} />
```

## Collapsible lanes

Set `collapsibleLanes` and each lane header gets a chevron that collapses the
lane to a slim strip - handy for wide boards.

```svelte
<SvGrid {data} {columns} board={{ groupBy: 'status', collapsibleLanes: true }} />
```

## Card context menu

Provide `cardMenu` and right-clicking (or long-pressing) a card opens a context
menu. Return the items for that card; the `ctx` argument gives board-native
helpers - `moveTo(laneId)` and `edit()` - plus the lane list, so a "Move to"
submenu is a one-liner. Items use the same `MenuItem` shape as `SvContextMenu`
(label / icon / shortcut / `children` submenus / `separator` / `onSelect`).

```svelte
<SvGrid {data} {columns}
  board={{
    groupBy: 'status',
    cardMenu: (row, ctx) => [
      { label: 'Edit', onSelect: ctx.edit },
      { label: 'Move to', children: ctx.lanes
          .filter((l) => l.id !== row.status)
          .map((l) => ({ label: l.title, onSelect: () => ctx.moveTo(l.id) })) },
      { separator: true },
      { label: 'Delete', onSelect: () => remove(row) },
    ],
  }} />
```

## Lane context menu

Alongside `cardMenu`, `laneMenu` puts a menu on a **lane header** right-click.
Its `ctx` gives lane-native helpers - `addCard(title?)`, `toggleCollapse()`,
plus the lane's `cards` and `collapsed` state.

```svelte
<SvGrid {data} {columns}
  board={{ groupBy: 'status',
    laneMenu: (laneId, ctx) => [
      { label: 'Add card', onSelect: () => ctx.addCard() },
      { label: `Clear (${ctx.cards.length})`, onSelect: () => clearLane(laneId) },
    ] }} />
```

## Facet filters, multi-select, flags & age

A cluster of "power" options, all on the built-in default card:

- **`facets`** - a filter bar of chips (one per distinct value) for the listed
  fields; selecting chips filters the visible cards. Works with array fields
  (tags) too.
- **`selectable`** - click to select a card, Ctrl/Cmd-click to toggle, and
  dragging a selected card moves the **whole selection**. `onSelectionChange`
  reports the selected rows.
- **`flagField`** - a truthy field paints a red **blocked** corner.
- **`ageField`** - a created/started date renders a relative **age** badge (`3d`).

```svelte
<SvGrid {data} {columns}
  board={{
    groupBy: 'status',
    facets: ['tags', 'assignee'],
    selectable: true,
    onSelectionChange: (rows) => (count = rows.length),
    flagField: 'blocked',
    ageField: 'created',
  }} />
```

## Hierarchical cards (epics -> stories)

Point `childrenField` at a row's child rows and each card gains a children
count that **expands to show its sub-cards** inline, each with its own title
and `groupBy` value - the epic -> stories shape.

```svelte
<SvGrid {data} {columns}
  board={{ groupBy: 'status', childrenField: 'stories' }} />
```

## Reorderable lanes & runtime group-by

Set `reorderableLanes` to let users **drag lane headers** to reorder the
columns; the order is tracked (and saved with `persistKey`), and
`onLaneReorder(orderedIds)` fires. Because `groupBy` is reactive, you can also
switch the **lane axis at runtime** just by changing it - the board re-buckets
and resets card positions to the new field automatically. Set `onLaneRename`
and users can **double-click a lane title** to rename it inline.

```svelte
<script>
  let groupBy = $state('status') // bind to a Status / Assignee / Priority selector
</script>

<SvGrid {data} {columns}
  board={{ groupBy, reorderableLanes: true, onCardMove: (e) => (row[groupBy] = e.toLane) }} />
```

## Saving the board layout

Pass `persistKey` and the board saves its layout - card positions, order,
per-card edits, and collapsed lanes - to `localStorage` under that key, and
restores it on load. It needs `getRowId` so the saved keys line up with the
right rows after a reload.

```svelte
<SvGrid {data} {columns}
  getRowId={(r) => String(r.id)}
  board={{ groupBy: 'status', persistKey: 'my-board' }} />
```

The layout is an overlay on top of your data, so if you also persist the data
itself (e.g. mirroring `onCardMove` to your store), the two stay in sync. To
persist somewhere other than `localStorage`, use `onLayoutChange(layout)` and
write it yourself; feed it back by seeding your data before mount. To reset,
clear the key and remount the board (e.g. Svelte's `{#key}`).

## Virtualized lanes (large boards)

For boards with thousands of cards in a lane, set `virtualized` so each lane
only keeps the cards in view in the DOM. Windowing is fixed-height, so set
`cardHeight` to match your cards (default `76`). Drag-and-drop, keyboard move,
and search all keep working - keyboard focus scrolls an off-screen card into
view before landing on it.

```svelte
<SvGrid {data} {columns}
  board={{ groupBy: 'status', virtualized: true, cardHeight: 72 }} />
```

Because windowing assumes a uniform card height, keep your cards a consistent
size (or close to it) when virtualizing; leave `virtualized` off for boards
with a few hundred cards, where it isn't needed.

## Moving cards

Moving cards is **built in** - you write no move code. Cards are draggable
between lanes and within a lane (with a live insertion marker), and the board
applies the move itself: it reassigns the lane and reorders, tracked as an
overlay keyed by row id, without mutating your data.

`onCardMove` is an optional **notification** - it fires after each move so you
can persist to a backend, or mirror the new lane onto your own row if another
view (a table, a count) reads the same data:

```svelte
<script>
  // Optional. The board already moved the card; this just syncs your store.
  function onCardMove(e) {
    // e = { row, fromLane, toLane, toIndex }
    e.row.status = e.toLane
  }
</script>

<SvGrid {data} {columns}
  getRowId={(r) => String(r.id)}
  board={{ groupBy: 'status', onCardMove }} />
```

Pass `getRowId` so the move overlay stays pinned to the right row across data
changes (adds/removes/re-sorts). Without it the board falls back to per-object
identity, which is fine until you replace the row objects wholesale.

## Keyboard drag-and-drop

The board is fully keyboard operable, so drag-and-drop is not mouse-only. Focus
a card and:

- **Space** / **Enter** - grab it (press again to drop).
- **Arrow Left / Right** - move it to the previous / next lane.
- **Arrow Up / Down** - reorder it within its lane.
- **Escape** - cancel and restore its original position.

Every step is announced through an ARIA live region for screen-reader users.

## Editing cards

Set `editable` and the board gets a **built-in card editor**: double-click a
card (or press **F2** while it is focused) to edit its fields inline. The
fields come from the columns that declare an `editorType` (or all fields if
none do), rendered as the matching input (text / number / date / checkbox).
Save writes to the board's edit overlay - the card updates immediately - and
fires `onCardCommit` with the changed fields, so you can persist:

```svelte
<script>
  function onCardCommit(e) {
    // e = { row, changes, values } - the board already applied it
    Object.assign(e.row, e.values) // mirror onto your data / persist
  }
</script>

<SvGrid {data} {columns}
  board={{ groupBy: 'status', editable: true, onCardCommit }} />
```

### Detail drawer

For a fuller editor, set `drawer` and opening a card shows a built-in
**`SvDrawer` + `SvForm`** rendered from your columns with the UI-kit editors
(text / date / select / number / colour / checkbox, chosen from each column's
`editorType`). `true` shows **all** fields; pass a config to pick a subset or
customize the drawer:

```svelte
<SvGrid {data} {columns}
  board={{ groupBy: 'status', drawer: true, onCardCommit: (e) => save(e) }} />

<!-- only some fields, custom title / side -->
<SvGrid {data} {columns}
  board={{ groupBy: 'status',
    drawer: { fields: ['title', 'due', 'assignee'], title: (row) => row.title, side: 'right' } }} />
```

Saving applies the edit and fires `onCardCommit`. The drawer does **not** close
on an outside click (so opening a select or picker inside it, or a stray click,
never loses your edits) - close it with Save, Escape, or the × button. If
`commentsField` is set, the drawer also shows the **comments thread** (read /
write / delete) below the form.

Precedence: `onCardEdit` > `drawer` > the inline `editable` editor - so pass
`onCardEdit` if you'd rather open your own editor or route to a detail screen.

## Custom cards

Omit `card` for the default card (title from the first column, then a few
meta rows). Pass a `card` snippet to render whatever you like - it receives the
row, so you can drop in avatars, chips, progress, or actions from the UI kit.

```svelte
<SvGrid {data} {columns}
  board={{ groupBy: 'status', card: taskCard }} />

{#snippet taskCard(task)}
  <div class="font-semibold">{task.title}</div>
  <div class="row">
    <SvAvatar name={task.assignee} size={20} />
    <SvChip variant={task.priority === 'High' ? 'danger' : 'neutral'}>
      {task.priority}
    </SvChip>
  </div>
{/snippet}
```

You can also set `titleField` and `cardFields` to steer the default card
without writing a snippet.

## Adding cards

Provide `onCardAdd` and each lane header gains a `+` button; it fires with the
lane id so you can append a new row in that lane. Add `composer` for a nicer
**type-a-title** box at the bottom of each lane - Enter calls
`onCardAdd(laneId, title)`.

```svelte
<SvGrid {data} {columns}
  board={{ groupBy: 'status', composer: true, onCardAdd: (lane, title) => addRow(lane, title) }} />
```

## Card badges, tags, comments & cover

The default card can show a rich badge row without a custom snippet - point the
board at your fields:

```svelte
<SvGrid {data} {columns}
  board={{
    groupBy: 'status',
    labelsField: 'labels',           // tags: string[] or { text, color }[] (coloured chips)
    dueField: 'due',                 // Date | ISO string; red when overdue
    assigneesField: 'assignees',     // string | string[] -> avatar initials
    commentsField: 'comments',       // array (length) or number -> comment-count badge
    attachmentsField: 'attachments', // array or number -> attachment-count badge
    coverField: 'cover',             // a CSS colour -> strip across the card top
  }} />
```

`labelsField` is your **tags** - pass `{ text, color }[]` for coloured chips.
These badges render on the default card **and** below a custom `card` snippet.

**Comments** are interactive: the comment badge expands an inline thread, and
the same thread appears in the **detail drawer** (a more discoverable place to
manage them). Pass `commentsField` (a `BoardComment[]` = `{ id, text, author?,
at? }`) plus `onCommentAdd` / `onCommentDelete` to let users **write and delete**
comments - update your data in the callbacks.

```svelte
<SvGrid {data} {columns}
  board={{ groupBy: 'status', commentsField: 'comments',
    onCommentAdd: (row, text) => row.comments.push({ id: uid(), text, author: 'You' }),
    onCommentDelete: (row, id) => (row.comments = row.comments.filter((c) => c.id !== id)) }} />
```

A **blocked** card (one that can't move forward - waiting on a dependency) is
marked with `flagField`: a truthy value shows a red corner + a "Blocked" badge,
and a string value is used as the reason (badge tooltip). A blocked card is
**locked from moving** (drag + keyboard) until it is unblocked - so blocking
actually does something. Set `flagBlocksMoves: false` if you want a purely
visual flag that still allows moves.

## Sub-tasks (checklists)

Point `subtasks` at a field holding `BoardSubtask[]` (`{ id, title, done }`) and
each card gets a progress chip + bar; expand it to tick items or add more. The
board tracks toggles/adds in its own overlay (your data is never mutated) and
fires `onSubtaskToggle` / `onSubtaskAdd` as notifications. Sub-task state is
included in `persistKey` so it survives a reload.

```svelte
<SvGrid {data} {columns}
  getRowId={(r) => String(r.id)}
  board={{ groupBy: 'status', subtasks: 'items',
           onSubtaskToggle: (row, id, done) => …, onSubtaskAdd: (row, title) => … }} />
```

For a full-detail editor, wire `onCardEdit` to open an `SvDrawer` with the rest
of a card's fields (see demo 349).

## `BoardConfig`

| Field | Type | Notes |
| --- | --- | --- |
| `groupBy` | `string` | **Required.** Field whose value buckets each row into a lane. |
| `lanes` | `BoardLane[]` | Explicit, ordered lanes. Omit to derive from distinct values. |
| `card` | `Snippet<[row]>` | Custom card body. Omit for the default card. |
| `onCardMove` | `(e) => void` | Optional notification after a built-in move: `{ row, fromLane, toLane, toIndex }`. Persist / mirror here. |
| `onCardAdd` | `(laneId, title?) => void` | Add-card affordance (header `+`, or composer title). |
| `composer` | `boolean` | Type-a-title quick-add box at the bottom of each lane. |
| `subtasks` | `string` | Field of `BoardSubtask[]`; renders a checklist + progress on the card. |
| `onSubtaskToggle` | `(row, id, done) => void` | Notified when a sub-task is toggled. |
| `onSubtaskAdd` | `(row, title) => void` | Notified when a sub-task is added on the card. |
| `labelsField` | `string` | Field of label strings / `{text,color}` -> chips on the card. |
| `dueField` | `string` | Field of a due date -> badge (red when overdue). |
| `assigneesField` | `string` | Field of assignee name(s) -> avatar(s) on the card. |
| `editable` | `boolean` | Enables the built-in inline card editor (double-click / F2). |
| `onCardEdit` | `(row) => void` | Open your own editor instead of the built-in one. |
| `drawer` | `boolean \| BoardDrawerConfig` | Built-in `SvDrawer` + `SvForm` detail editor; `true` = all fields, or `{ fields, title, side, size }`. |
| `cardMenu` | `(row, ctx) => MenuItem[]` | Card right-click menu; `ctx` = `{ lanes, moveTo, edit }`. |
| `laneMenu` | `(laneId, ctx) => MenuItem[]` | Lane-header right-click menu; `ctx` = `{ cards, collapsed, toggleCollapse, addCard }`. |
| `facets` | `string[]` | Fields shown as a facet filter-chip bar above the board. |
| `selectable` | `boolean` | Card multi-select; dragging a selected card moves the whole selection. |
| `onSelectionChange` | `(rows) => void` | Fires with the selected rows. |
| `flagField` | `string` | Truthy -> "Blocked" badge + red corner (string = reason); locks the card from moving. |
| `flagBlocksMoves` | `boolean` | Whether a blocked card is locked from moving. Defaults to `true`. |
| `ageField` | `string` | A date -> a relative age badge (`3d`). |
| `childrenField` | `string` | Child rows -> an expandable count of nested mini-cards. |
| `onCardCommit` | `(e) => void` | Fires when the built-in editor saves: `{ row, changes, values }`. |
| `laneSummary` | `(rows, laneId) => string` | Per-lane header roll-up (points / $ total / count). |
| `swimlaneSummary` | `(rows, swimId) => string` | Per-swimlane band roll-up. |
| `onLaneRename` | `(laneId, title) => void` | Enable double-click inline lane rename. |
| `swimlaneBy` | `string` | Second axis: split the board into swimlane bands by this field. |
| `collapsibleSwimlanes` | `boolean` | Let users collapse a swimlane band via its header chevron. |
| `commentsField` | `string` | `BoardComment[]` -> a comment badge that expands to a thread. |
| `onCommentAdd` | `(row, text) => void` | Let users write comments (append to your data). |
| `onCommentDelete` | `(row, commentId) => void` | Let users delete comments. |
| `attachmentsField` | `string` | Array or number -> attachment-count badge. |
| `coverField` | `string` | A CSS colour -> a strip across the card top. |
| `enforceWip` | `boolean` | Reject moves that would push a lane past its `wipLimit`. |
| `collapsibleLanes` | `boolean` | Let users collapse a lane to a strip via its header chevron. |
| `reorderableLanes` | `boolean` | Let users drag lane headers to reorder columns (persisted). |
| `onLaneReorder` | `(orderedIds) => void` | Fires after a lane drag-reorder. |
| `searchable` | `boolean` | Show the built-in search box (default `true`). |
| `searchPlaceholder` | `string` | Placeholder for the search box. |
| `virtualized` | `boolean` | Window each lane (only in-view cards in the DOM) for very large boards. |
| `cardHeight` | `number` | Estimated card height for `virtualized` windowing (default `76`). |
| `persistKey` | `string` | Save/restore the board layout to `localStorage` (needs `getRowId`). |
| `onLayoutChange` | `(layout) => void` | Fires with the serializable layout on any change. |
| `titleField` | `string` | Title on the default card (defaults to first column). |
| `cardFields` | `string[]` | Meta rows on the default card (max 4). |

`BoardLane` = `{ id, title?, color?, wipLimit? }`.

## Notes

- The board themes purely from the grid's `--sg-*` tokens - pick any preset and
  it follows. Set `--sg-board-lane-w` to change lane width.
- Board mode replaces the table render, so table-only chrome (column headers,
  the pagination footer, the filter row) does not apply while `board` is set.
- Moving a card is tracked as an overlay on top of your data - the board never
  mutates your rows. Mirror via `onCardMove` if other views must agree.

## More examples

### Sprint board (swimlanes + WIP)

An agile sprint board: status lanes crossed with a swimlane per assignee, an enforced WIP limit on In progress, and a story-point roll-up in every lane header via board.laneSummary. Drag across a band to reassign the owner; double-click to edit. Collapsible lanes.

> Live demo: Sprint board (swimlanes + WIP) - https://svgrid.com/demos/344-kanban-sprint/

### Sales pipeline (deal board)

A CRM deal board: stages as lanes with a live total-value roll-up per stage (board.laneSummary). Rich deal cards; drag to advance a deal, double-click to edit. Moves and edits persist to localStorage via onCardMove / onCardCommit, so a reload restores the board.

> Live demo: Sales pipeline (deal board) - https://svgrid.com/demos/345-kanban-pipeline/

### Large board (virtualized)

Thousands of cards per lane kept smooth with board.virtualized - only the cards in view are in the DOM. A slider scales the board from 1k to 50k cards; drag-and-drop and keyboard move still work across the whole window.

> Live demo: Large board (virtualized) - https://svgrid.com/demos/346-kanban-virtualized/

### Support triage board

A helpdesk triage board: status lanes crossed with a priority swimlane, a search box front-and-center, and an SLA colour bar on each card driven by ticket age. Drag across a band to re-triage a ticket priority.

> Live demo: Support triage board - https://svgrid.com/demos/347-kanban-support/

### Content calendar

An editorial pipeline: stage lanes crossed with a swimlane per channel. Rich cards carry a cover glyph, author and target date. Drag a piece down the pipeline, or across a band to move it to another channel; double-click to edit.

> Live demo: Content calendar - https://svgrid.com/demos/348-kanban-content/

### Cards with sub-tasks & badges

The default board card made rich by config alone: label / due / assignee badges, a sub-task checklist with a progress bar (expand to tick items or add more), and a quick-add composer at the bottom of each lane. Double-click a card for the BUILT-IN detail drawer (board.drawer -> SvDrawer + SvForm with the UI-kit editors); toggle all fields vs a customized subset. Board / Table switcher.

> Live demo: Cards with sub-tasks & badges - https://svgrid.com/demos/349-kanban-subtasks/

### Group-by switch + reorderable lanes

Board structure, live: switch the lane axis at runtime (Status / Assignee / Priority) and the board re-buckets, and drag a lane header to reorder the columns. Both built-in - the group-by switch resets card positions to the new field; lane order is tracked and persists with persistKey. Board / Table switcher.

> Live demo: Group-by switch + reorderable lanes - https://svgrid.com/demos/350-kanban-structure/

### Board power tools (filters, multi-select, menus)

A facet filter bar (by tag + assignee), card multi-select with bulk move (click / Ctrl-click then drag the group), a blocked flag, a card-age badge, and both card + lane right-click menus - all from board config on the built-in default card. Board / Table switcher.

> Live demo: Board power tools (filters, multi-select, menus) - https://svgrid.com/demos/351-kanban-power/

### Epics -> stories (hierarchical cards)

Point board.childrenField at a row child rows and each card gains a children count that expands to show its sub-cards inline (Jira epic -> stories). Each child shows its own title + status. Board / Table switcher.

> Live demo: Epics -> stories (hierarchical cards) - https://svgrid.com/demos/352-kanban-epics/

## See also

- [Row dragging](https://svgrid.com/docs/help/rows/row-dragging/) - reorder rows and move them between grids.
- [Mobile card view](https://svgrid.com/docs/help/mobile-card-view/) - the responsive grid/card pivot.
- [Grouping & aggregation](https://svgrid.com/docs/help/grouping-aggregation/) - the row-bucketing model board lanes reuse.
- [Scheduler / calendar mode](https://svgrid.com/docs/help/rows/scheduler/) - the same rows as bookings on a calendar.
- [Gantt chart mode](https://svgrid.com/docs/help/rows/gantt/) - the same rows as a project plan: in progress, due 1 November 2026.

---

SvGrid is the Svelte 5 data grid (`npm install @svgrid/grid`, MIT). This page: https://svgrid.com/docs/help/rows/kanban-board/ . All docs: https://svgrid.com/llms.txt
