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. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Kanban). See the SvGrid documentation for the full API.
About this example
A helpdesk triage board on the Svelte 5 data grid: status lanes crossed with a swimlane per priority, a search box front and centre through board.searchable, and an SLA colour bar on each card driven by ticket age. Drag a ticket to a new status, or across a band to re-triage its priority, and double-click to edit.
A helpdesk triage board: status lanes crossed with a swimlane per priority, a search box front-and-center, and an SLA colour bar on each card driven by ticket age. Drag a ticket to a new status, or across a band to re-triage its priority.
Imports, features and API used
Imports: @svgrid/enterprise, @svgrid/grid
Table features registered: rowSortingFeature, columnFilteringFeature
Columns: subject (Subject), customer (Customer), priority (Priority), status (Status)
Frequently asked questions
How does the search box filter the board?
searchable adds a search input above the lanes with searchPlaceholder as its hint; typing filters the cards across every lane and band by their fields while the lane structure stays in place.
How is the SLA bar coloured?
The card snippet reads the ticket's age in hours and paints the bar red past 24 hours, amber past 8 and green below, and the header counts the tickets breaching the SLA.
How is priority changed by drag?
Priority is the swimlaneBy field, so dropping a card in another horizontal band writes the new priority through onCardMove alongside any status change.
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.
Source code (347-kanban-support.svelte)
<script lang="ts">
// Kanban board rendering is an Enterprise feature - register the renderer.
import { enableBoardView } from "@svgrid/enterprise";
enableBoardView();
/**
* 347. Support triage (priority swimlanes + search + SLA)
* ------------------------------------------------------
* A helpdesk triage board: status lanes crossed with a swimlane per priority,
* a search box front-and-center, and an SLA colour bar on each card driven by
* ticket age. Drag a ticket to a new status, or across a band to re-triage
* its priority.
*/
import { SvGrid, tableFeatures, rowSortingFeature, columnFilteringFeature, type ColumnDef, type BoardCardMoveEvent, type BoardCardCommitEvent } from '@svgrid/grid'
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
let view = $state<'board' | 'table'>('board')
type Ticket = {
id: number
subject: string
status: string
priority: string
customer: string
ageH: number
}
let rows = $state<Ticket[]>([
{ id: 101, subject: 'Login loop after SSO', status: 'new', priority: 'Urgent', customer: 'Contoso', ageH: 26 },
{ id: 102, subject: 'Export missing columns', status: 'open', priority: 'High', customer: 'Fabrikam', ageH: 9 },
{ id: 103, subject: 'Typo on billing page', status: 'new', priority: 'Low', customer: 'Wingtip', ageH: 3 },
{ id: 104, subject: 'API 500 on bulk update', status: 'open', priority: 'Urgent', customer: 'Northwind', ageH: 40 },
{ id: 105, subject: 'Dark mode contrast', status: 'pending', priority: 'Normal', customer: 'Litware', ageH: 14 },
{ id: 106, subject: 'Slow board with 10k cards', status: 'open', priority: 'High', customer: 'Proseware', ageH: 5 },
{ id: 107, subject: 'Refund request', status: 'pending', priority: 'Normal', customer: 'Tailspin', ageH: 20 },
{ id: 108, subject: 'Feature: swimlanes', status: 'resolved', priority: 'Low', customer: 'Adventure', ageH: 2 },
{ id: 109, subject: 'Webhook retries', status: 'new', priority: 'High', customer: 'Contoso', ageH: 33 },
])
const columns: ColumnDef<any, Ticket>[] = [
{ field: 'subject', header: 'Subject', editorType: 'text' },
{ field: 'customer', header: 'Customer', editorType: 'text' },
{ field: 'priority', header: 'Priority' },
{ field: 'status', header: 'Status' },
]
const lanes = [
{ id: 'new', title: 'New' },
{ id: 'open', title: 'Open', color: '#3b82f6' },
{ id: 'pending', title: 'Pending', color: '#f59e0b' },
{ id: 'resolved', title: 'Resolved', color: '#22c55e' },
]
// SLA: red past 24h, amber past 8h, else green.
const sla = (h: number) => (h >= 24 ? '#ef4444' : h >= 8 ? '#f59e0b' : '#22c55e')
function onCardMove(e: BoardCardMoveEvent<Ticket>) {
e.row.status = e.toLane
if (e.toSwimlane != null) e.row.priority = e.toSwimlane
}
function onCardCommit(e: BoardCardCommitEvent<Ticket>) {
Object.assign(e.row, e.values)
}
</script>
<section class="flex flex-col flex-1 min-h-0 gap-3">
<div class="flex items-center justify-between gap-3 shrink-0">
<div class="kb-lead text-sm">
The same <strong><SvGrid></strong>, two views. Status lanes x a priority
swimlane; search filters every field; the bar is the SLA (red > 24h, amber > 8h).
</div>
<div class="kb-seg inline-flex rounded-md overflow-hidden text-sm shrink-0">
<button class="kb-seg-btn px-3 py-1 {view === 'board' ? 'kb-on' : ''}"
onclick={() => (view = 'board')}>Board</button>
<button class="kb-seg-btn px-3 py-1 {view === 'table' ? 'kb-on' : ''}"
onclick={() => (view = 'table')}>Table</button>
</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: 'status',
lanes,
swimlaneBy: 'priority',
collapsibleSwimlanes: true,
editable: true,
searchPlaceholder: 'Search tickets...',
laneSummary: (list) => `${list.length}`,
onCardMove,
onCardCommit,
card: ticketCard,
}}
/>
{: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">
{rows.length} open tickets · {rows.filter((t) => t.ageH >= 24).length} breaching SLA
</footer>
</section>
{#snippet ticketCard(t: Ticket)}
<div class="flex gap-2">
<span class="w-1 rounded-full shrink-0" style="background:{sla(t.ageH)}"></span>
<div class="flex flex-col gap-1 min-w-0">
<div class="font-semibold text-[0.8rem] leading-snug truncate">{t.subject}</div>
<div class="kb-card-meta flex items-center justify-between gap-2 text-[0.72rem]">
<span class="truncate">{t.customer}</span>
<span class="tabular-nums shrink-0">#{t.id} · {t.ageH}h</span>
</div>
</div>
</div>
{/snippet}
<style>
.kb-lead { color: var(--sg-fg, #475569); }
.kb-foot { color: var(--sg-muted, #64748b); }
.kb-seg { border: 1px solid var(--sg-border, #cbd5e1); }
.kb-seg-btn { background: transparent; color: var(--sg-fg, #0f172a); }
.kb-seg-btn:hover { background: var(--sg-row-hover-bg, #f1f5f9); }
.kb-seg-btn.kb-on { background: var(--sg-accent, #1e293b); color: var(--sg-on-accent, #fff); }
.kb-card-meta { color: var(--sg-muted, #64748b); }
</style>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.
- 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.