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.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Kanban). See the SvGrid documentation for the full API.
What this example shows
Change the lane axis at runtime (Status / Assignee / Priority) and the board re-buckets live; drag a lane header to reorder the columns. Both are built-in - the group-by switch resets card positions to the new field, and lane order is tracked (and persists with `persistKey`).
Imports, features and API used
Imports: @svgrid/enterprise, @svgrid/grid
Table features registered: rowSortingFeature, columnFilteringFeature
Columns: title (Task), assignee (Assignee), priority (Priority), status (Status)
Source code (350-kanban-structure.svelte)
<script lang="ts">
// Kanban board rendering is an Enterprise feature - register the renderer.
import { enableBoardView } from "@svgrid/enterprise";
enableBoardView();
/**
* 350. Board structure: group-by switch + reorderable lanes
* ---------------------------------------------------------
* Change the lane axis at runtime (Status / Assignee / Priority) and the
* board re-buckets live; drag a lane header to reorder the columns. Both are
* built-in - the group-by switch resets card positions to the new field, and
* lane order is tracked (and persists with `persistKey`).
*/
import { SvGrid, tableFeatures, rowSortingFeature, columnFilteringFeature, type ColumnDef, type BoardCardMoveEvent } from '@svgrid/grid'
type Task = { id: number; title: string; status: string; assignee: string; priority: string }
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
let view = $state<'board' | 'table'>('board')
let groupField = $state<'status' | 'assignee' | 'priority'>('status')
let rows = $state<Task[]>([
{ id: 1, title: 'Auth refresh tokens', status: 'todo', assignee: 'Sam Rivera', priority: 'High' },
{ id: 2, title: 'Board DnD', status: 'in_progress', assignee: 'Lee Park', priority: 'High' },
{ id: 3, title: 'Default card', status: 'in_progress', assignee: 'Sam Rivera', priority: 'Medium' },
{ id: 4, title: 'WIP styling', status: 'review', assignee: 'Ada Cole', priority: 'Low' },
{ id: 5, title: 'Docs pass', status: 'todo', assignee: 'Lee Park', priority: 'Medium' },
{ id: 6, title: 'Release 1.4', status: 'done', assignee: 'Ada Cole', priority: 'High' },
{ id: 7, title: 'Keyboard a11y', status: 'todo', assignee: 'Sam Rivera', priority: 'High' },
])
const columns: ColumnDef<any, Task>[] = [
{ field: 'title', header: 'Task', editorType: 'text' },
{ field: 'assignee', header: 'Assignee', editorType: 'text' },
{ field: 'priority', header: 'Priority', editorType: 'text' },
{ field: 'status', header: 'Status', editorType: 'text' },
]
const statusLanes = [
{ id: 'todo', title: 'To do' },
{ id: 'in_progress', title: 'In progress', color: '#f59e0b' },
{ id: 'review', title: 'Review', color: '#8b5cf6' },
{ id: 'done', title: 'Done', color: '#22c55e' },
]
function onCardMove(e: BoardCardMoveEvent<Task>) {
;(e.row as Record<string, unknown>)[groupField] = e.toLane
}
const GROUPS: Array<typeof groupField> = ['status', 'assignee', 'priority']
</script>
<section class="flex flex-col flex-1 min-h-0 gap-3">
<div class="flex items-center justify-between gap-3 shrink-0 flex-wrap">
<div class="kb-note text-sm">
The same <strong><SvGrid></strong>, two views. Switch the lane axis live, and
<strong>drag a lane header</strong> to reorder columns.
</div>
<div class="flex items-center gap-3 shrink-0">
{#if view === 'board'}
<label class="flex items-center gap-1.5 text-sm">
Group by:
<select
class="kb-select px-2 py-1 rounded border bg-transparent capitalize"
bind:value={groupField}>
{#each GROUPS as g (g)}<option value={g}>{g}</option>{/each}
</select>
</label>
{/if}
<div class="kb-switch inline-flex rounded-md border overflow-hidden text-sm">
<button class="kb-seg px-3 py-1 {view === 'board' ? 'kb-seg-on' : ''}"
onclick={() => (view = 'board')}>Board</button>
<button class="kb-seg px-3 py-1 {view === 'table' ? 'kb-seg-on' : ''}"
onclick={() => (view = 'table')}>Table</button>
</div>
</div>
</div>
<div class="flex-1 min-h-0">
{#if view === 'board'}
<SvGrid responsive={true}
columnResize
data={rows}
columns={columns}
getRowId={(r) => String(r.id)}
containerHeight="100%"
board={{
groupBy: groupField,
lanes: groupField === 'status' ? statusLanes : undefined,
reorderableLanes: true,
editable: true,
laneSummary: (list) => `${list.length}`,
onCardMove,
}}
/>
{:else}
<SvGrid responsive={true}
columnResize
data={rows}
columns={columns}
features={features}
getRowId={(r) => String(r.id)}
sortable
filterable
enableInlineEditing
rowHeight={36}
containerHeight="100%"
fitColumns
/>
{/if}
</div>
<footer class="kb-foot text-sm shrink-0">
Grouped by <strong class="capitalize">{groupField}</strong> · {rows.length} tasks · drag a header to reorder.
</footer>
</section>
<style>
.kb-note { color: var(--sg-muted, #475569); }
.kb-foot { color: var(--sg-muted, #64748b); }
.kb-switch { border-color: var(--sg-border, #cbd5e1); }
.kb-select {
border-color: var(--sg-border, #cbd5e1);
color: var(--sg-fg, #0f172a);
}
.kb-seg {
background: transparent;
color: var(--sg-fg, #0f172a);
cursor: pointer;
}
.kb-seg-on {
background: var(--sg-accent, #1e293b);
color: var(--sg-on-accent, #fff);
}
</style>Related documentation
Related articles
- Building a Kanban Board in Svelte 5 (Without a Board Library) - How to turn a Svelte data grid into a drag-and-drop Kanban board with one prop - lanes, WIP limits, swimlanes, keyboard moves, and virtualized lanes for large boards.
More Kanban examples
- Kanban board mode - The same <SvGrid>, same data + columns, rendered as a Kanban board by setting one `board` prop: rows become cards in horizontal lanes bucketed by a `status` field. Built-in drag-and-drop AND keyboard move (Space + arrows) reassign the lane and reorder; double-click / F2 opens a built-in card editor; WIP limits, add-card, and a rich card snippet (assignee avatar + priority chip). Toggle Board / Table to see it is one grid.
- 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.
- 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.
- 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.
- 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.