From PD rockets by derekr, where it is pd-kanban-board, with its names in Starbase's sb- prefix and the patches in patches/pd-rockets (0030 to 0035). For this board they keep Alt and the arrows from leaving the page at the first and last lane, keep a card's row when it changes lanes, add Alt + Home and Alt + End, let the arrows reach empty lanes, send sb-kanban-select on a click too, and list the events in the component's manifest.
A board of lanes whose cards the server renders and places. Drag a card within its lane or into another one, or focus it and hold Alt while you press the arrow keys. The component shows where the card would land, then emits sb-kanban-move with the card, the lane and the card it goes before. It moves nothing itself: the server applies the move and sends the board back, and the morph moves each card to its place with a short animation (none when the reader's system asks for reduced motion).
Examples
Plan a mission
The server renders the board with its arrangement in data-state: each lane's cards, lanes separated by |. A move sends that arrangement with the event's detail to /demo/arrange/kanban-board, which answers with the board as it is after the move. Nothing is stored: try it in two tabs.
Since each request carries the arrangement the page had when it left, two moves made faster than the server answers both start from the same board, and Datastar cancels the first request when the second one starts: the board then shows the second move only. A server that keeps the board applies both.
To visit
En route
Visited
<style>
.demo-kanban { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 10px; inline-size: 100%; }
.demo-kanban [data-kanban-lane] {
display: grid;
grid-template-rows: auto 1fr;
gap: 8px;
padding: 8px;
border: 1px solid var(--sb-border);
background: var(--sb-surface-inset);
}
.demo-kanban [data-kanban-lane][data-drop-active] { border-color: var(--sb-brand); }
.demo-kanban__title { margin: 0; color: var(--sb-text-2); font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
.demo-kanban [data-kanban-lane-cards] { display: grid; align-content: start; gap: 6px; min-block-size: 3rem; }
.demo-kanban [data-kanban-card], [data-drag-preview][data-kanban-card] {
padding: 0.5rem 0.75rem;
border: 1px solid var(--sb-border);
background: var(--sb-surface-card);
color: var(--sb-text-1);
cursor: grab;
touch-action: none;
user-select: none;
}
.demo-kanban [data-kanban-card] { position: relative; }
.demo-kanban [data-kanban-card]:focus-visible, .demo-kanban [data-kanban-lane]:focus-visible { outline: 2px solid var(--sb-brand-light); outline-offset: 2px; }
.demo-kanban [data-dragging] { opacity: 0.35; }
.demo-kanban[data-key-staging] [data-kanban-card]:focus { border-color: var(--sb-brand); }
.demo-kanban [data-drop-before]::before, .demo-kanban [data-drop-end]::after {
content: "";
display: block;
block-size: 3px;
background: var(--sb-brand);
}
.demo-kanban [data-drop-before]::before { position: absolute; inset: -5px 0 auto; }
[data-drag-preview][data-kanban-card] { box-shadow: var(--sb-shadow-overlay, 0 8px 16px rgb(0 0 0 / 0.35)); cursor: grabbing; }
@media (forced-colors: active) {
.demo-kanban [data-kanban-lane][data-drop-active] { outline: 2px solid Highlight; outline-offset: -2px; }
.demo-kanban[data-key-staging] [data-kanban-card]:focus { border-color: Highlight; }
.demo-kanban [data-kanban-card]:focus-visible, .demo-kanban [data-kanban-lane]:focus-visible { outline-color: Highlight; }
.demo-kanban [data-drop-before]::before, .demo-kanban [data-drop-end]::after { forced-color-adjust: none; background: Highlight; }
.demo-kanban [data-dragging] { border: 1px dashed CanvasText; }
[data-drag-preview][data-kanban-card] { border: 2px solid Highlight; }
}
</style>
<sb-kanban-board id="mission-board" data-ignore-morph class="demo-kanban" data-state="mars jupiter neptune | europa titan | moon"
data-on:sb-kanban-move="@get('/demo/arrange/kanban-board', {payload: {id: el.id, state: el.dataset.state, move: evt.detail}})">
<section data-kanban-lane data-col="4" tabindex="-1" aria-label="To visit">
<p class="demo-kanban__title">To visit</p>
<div data-kanban-lane-cards>
<article id="mission-board-mars" data-kanban-card="mars" tabindex="0">馃獝 Mars</article>
<article id="mission-board-jupiter" data-kanban-card="jupiter" tabindex="0">馃獝 Jupiter</article>
<article id="mission-board-neptune" data-kanban-card="neptune" tabindex="0">馃獝 Neptune</article>
</div>
</section>
<section data-kanban-lane data-col="7" tabindex="-1" aria-label="En route">
<p class="demo-kanban__title">En route</p>
<div data-kanban-lane-cards>
<article id="mission-board-europa" data-kanban-card="europa" tabindex="0">馃寫 Europa</article>
<article id="mission-board-titan" data-kanban-card="titan" tabindex="0">馃寫 Titan</article>
</div>
</section>
<section data-kanban-lane data-col="9" tabindex="-1" aria-label="Visited">
<p class="demo-kanban__title">Visited</p>
<div data-kanban-lane-cards>
<article id="mission-board-moon" data-kanban-card="moon" tabindex="0">馃寫 Moon</article>
</div>
</section>
</sb-kanban-board>
Markup and events
A lane is an element with data-kanban-lane and the lane's id, a number, in data-col (4, 7 and 9 in the example): an id, not a position, so it stays with the lane when lanes are added or reordered. Its cards sit in an element with data-kanban-lane-cards, which is also where a card dropped at the end of the lane goes. A card is an element with data-kanban-card="<id>" and tabindex="0", and an id that is unique on the page, so that the morph moves the card's element instead of writing another card into it. The card's content is yours: a button or link inside a card keeps its own clicks and keys, unless it carries data-kanban-card-main, which makes it the card's handle for dragging and the keyboard.
| Event | Detail | When |
|---|---|---|
sb-kanban-move |
{ cardId, col, before } |
A card was dropped, or a keyboard move committed. col is the lane's data-col as a number, and before the id of the card it now precedes in that lane, or "" for the end. |
sb-kanban-select |
{ cardId } |
The arrow keys moved the focus to another card, or the pointer pressed a card (not a button or link inside it). Focusing an empty lane sends nothing. |
Both events bubble. When boards are nested, check evt.target in a handler that several of them reach.
Keyboard
| Keys | Action |
|---|---|
| Arrow Down / Arrow Up, or j / k | Focus the next / previous card, across lanes in board order |
| Arrow Left / Arrow Right, or h / l | Focus the card in the same row of the previous / next lane |
| Home / End | Focus the first / last card on the board |
| Alt + Arrow Down / Up, or Alt + j / k | Move the focused card down / up in its lane |
| Alt + Arrow Left / Right, or Alt + h / l | Move the focused card into the previous / next lane, in the same row |
| Alt + Home / End | Move the focused card to the top / bottom of its lane |
| Escape | Cancel a move before Alt is released |
Releasing Alt sends the move, and until then each key moves the card on from where the last one put it. At the first or last lane, Alt + Arrow Left or Right does nothing, so the browser doesn't go back or forward in its history either.
A lane with tabindex="-1", as in the example, takes the focus while it has no cards: the arrows stop there, so a keyboard user learns that the lane exists. Without the attribute, the arrows pass over an empty lane. Alt and the arrows move a card into an empty lane either way, and so does a drag.
On macOS, Ctrl + n and Ctrl + p also move the focus. Each action takes its keys from a data-key-<action> attribute on the board, a space-separated list such as data-key-focus-next="ArrowDown n"; an empty value turns the action off. The actions are focus-next, focus-previous, focus-left, focus-right, focus-first, focus-last, move-up, move-down, move-left, move-right, move-first, move-last and cancel. The older names select-next, select-previous, select-left and select-right still work when the focus- attribute is absent.
On the server
The server owns the board. A handler applies the move and renders the board again. Since col names the lane by its id, a move lands in the right lane even when another user added or reordered lanes in the meantime. A server that stores the board should send the move with requestCancellation: 'disabled', as in the usage example: Datastar otherwise cancels a request when the next one from the same board starts, and a move can be lost. This is the demo's handler, which keeps the arrangement in the markup instead of a database:
// A kanbanLane is a lane of the demo board. Its ID is its data-col: a move
// names the lane by id, which stays the same wherever the lane is.
type kanbanLane struct {
ID int
Title string
}
// kanbanLanes are the demo board's lanes, in their order.
var kanbanLanes = []kanbanLane{{4, "To visit"}, {7, "En route"}, {9, "Visited"}}
// arrangeKanban applies an sb-kanban-move ({cardId, col, before}): the card
// leaves its lane and goes into the lane whose id is col, before another
// card ("": last).
func arrangeKanban(state string, move json.RawMessage) (string, error) {
lanes, err := kanbanState(state)
if err != nil {
return "", err
}
var m struct {
CardID string `json:"cardId"`
Col int `json:"col"`
Before string `json:"before"`
}
if err := json.Unmarshal(move, &m); err != nil {
return "", err
}
to := slices.IndexFunc(kanbanLanes, func(l kanbanLane) bool { return l.ID == m.Col })
if to < 0 {
return "", fmt.Errorf("no lane %d", m.Col)
}
from := -1
for i, lane := range lanes {
if j := slices.Index(lane, m.CardID); j >= 0 {
from = i
lanes[i] = slices.Delete(lane, j, j+1)
}
}
if from < 0 {
return "", fmt.Errorf("no card %q", m.CardID)
}
// In at the end, then before its neighbour: moveBefore checks it.
if lanes[to], err = moveBefore(append(lanes[to], m.CardID), m.CardID, m.Before); err != nil {
return "", err
}
out := make([]string, len(lanes))
for i, lane := range lanes {
out[i] = strings.Join(lane, " ")
}
// Fields drops the extra space an empty lane leaves: "mars | | moon".
return strings.Join(strings.Fields(strings.Join(out, " | ")), " "), nil
}
// renderKanban is the board's markup: the host the morph replaces, with the
// arrangement in data-state, a lane per column and a card per body. Each card
// has an id, so the morph moves it (and its focus) instead of rewriting
// another card in its place.
func renderKanban(id, state string) string {
lanes, _ := kanbanState(state)
esc := html.EscapeString
var b strings.Builder
fmt.Fprintf(&b, "<sb-kanban-board id=\"%s\" data-ignore-morph class=\"demo-kanban\" data-state=\"%s\"\n\tdata-on:sb-kanban-move=\"%s\">\n", esc(id), esc(state), arrangeOn("kanban-board"))
for i, lane := range kanbanLanes {
fmt.Fprintf(&b, "\t<section data-kanban-lane data-col=\"%d\" tabindex=\"-1\" aria-label=\"%s\">\n\t\t<p class=\"demo-kanban__title\">%s</p>\n\t\t<div data-kanban-lane-cards>\n", lane.ID, esc(lane.Title), esc(lane.Title))
if i < len(lanes) {
for _, card := range lanes[i] {
fmt.Fprintf(&b, "\t\t\t<article id=\"%s-%s\" data-kanban-card=\"%s\" tabindex=\"0\">%s</article>\n", esc(id), esc(card), esc(card), label(card))
}
}
b.WriteString("\t\t</div>\n\t</section>\n")
}
b.WriteString("</sb-kanban-board>")
return b.String()
}
Styling
The component adds no styles: the lanes and cards are your page's markup, styled by your page's CSS. While a card moves, it marks the elements involved:
data-drag-activeon the board anddata-draggingon the card while you drag.data-drop-activeon the lane the card would land in,data-drop-beforeon the card it would land before, anddata-drop-endon the lane'sdata-kanban-lane-cardselement when it would land at the end.data-key-stagingon the board while a keyboard move waits for Alt to be released.- The card under the pointer is a copy of the card in
<body>, markeddata-drag-preview, with the original's size in--sb-source-widthand--sb-source-height. A<template data-sb-preview>that is a direct child of a card replaces the copy. - A
<template data-sb-target="before">(or"end") that is a direct child of the board is copied into the landing place as a drop marker, markeddata-sb-target-indicator.
The example above shows one way to draw them.
Accessibility
Lanes and cards are elements of your page, so their roles and names are yours: give each lane an aria-label, as the example does. Keyboard moves need no pointer. The component announces nothing itself: when the server sends the new board, the moved card keeps the focus, after a drag too when the cards have ids. In forced colours, the example draws the drop marker, the target lane and the focus ring in Highlight.
Licence
The code is PD rockets', under its Beer-Ware licence, in LICENSE-pd-rockets.txt next to it. go run ./cmd/vendorpd brings in a newer release.
Installation
Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag: the autoloader imports each component the first time its tag appears, including tags a Datastar morph adds later. The Datastar here is v1.0.4 with Starbase's fixes until they are released; the components also run on the official release.
<script type="importmap">
{ "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<sb-kanban-board data-on:sb-kanban-move="@post('/board/move', {payload: evt.detail, requestCancellation: 'disabled'})">
<section data-kanban-lane data-col="1" tabindex="-1" aria-label="To do">
<div data-kanban-lane-cards>
<article id="card-a" data-kanban-card="a" tabindex="0">First</article>
</div>
</section>
<section data-kanban-lane data-col="2" tabindex="-1" aria-label="Done">
<div data-kanban-lane-cards></div>
</section>
</sb-kanban-board>No flash of undefined elements: put class="sb-cloak" on <html> and add .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the page's components are defined.
Load just this component, pinned to this version: the minified module with its integrity hash, so the browser refuses it if a single byte changes.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/kanban-board.min.js": "sha384-M9MA234qf0muP0ZkPGaCHAhwgrwBUd3wf1hyvv5twOdjrEFtFpL7G8QmxAqMN6lu",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/contracts/kanban.min.js": "sha384-vaFAgbo2i4Wl7XaC4ElR4Dx06ZKgZXxfIzJ5WWbEkdR2Kf90lwKINcXDKgN+5Iso",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/drag-state.min.js": "sha384-5Y+afnEzy5l/9zFkAh+LOmom6361otYZ1mfJztIuXD7yOlTGx+TVHk1h9biAKgyX",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/flip.min.js": "sha384-1lsCQwc6EMFn4yzzI9FZbnF3Zbw6G59SB/cMrp533rqQ2gnI5eehw/RCrds3YJfr",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/focus-recovery.min.js": "sha384-JHh8h56l3OTHOT2hMP71pjVnh+AhoEBm41Fi2GetKZB35rHk5hVh0l+HXr92ZFqH",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/insertion-target.min.js": "sha384-WdOMwKgasfqQ4iOd9LI7wcmw3a4pAIkNdBRA63gnNUsjzw5NmDiyBKyf8jWkOB27",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/keyboard-staging.min.js": "sha384-WnYZ2saImC9lL8d4IfSLm3FT5TEzIPPsMviBcJe5NTuVh/vFpZm6/d0oMDYs8s8E",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/keyboard.min.js": "sha384-5AVhHD8gnbU5fkXCucowCxLCa0MxEQ7d2mQ9dRh+5eXhHEkfV8py2h6jinGH4PDY",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/ownership.min.js": "sha384-HhT3ZO2nxuGm6d66kwqQaE7Q9YADIW0cQqCQVTx5hKVW0D9PNvRBAqDxI26f8xBo",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/pointer-drag.min.js": "sha384-yslRfcHaD24bjpam90FqtgQVDHZXb4QT/GOfuQElOyb8qqgacm6PaesXPD9CbYa6",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/visual-outlets.min.js": "sha384-+AzW6If/d7MsA7+32XnB3SaY6VIbxTTzS8jc6fHLwSem6VyO3eH8ADPyLfTiSc3D"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/kanban-board.min.js" integrity="sha384-M9MA234qf0muP0ZkPGaCHAhwgrwBUd3wf1hyvv5twOdjrEFtFpL7G8QmxAqMN6lu"></script>
<sb-kanban-board data-on:sb-kanban-move="@post('/board/move', {payload: evt.detail, requestCancellation: 'disabled'})">
<section data-kanban-lane data-col="1" tabindex="-1" aria-label="To do">
<div data-kanban-lane-cards>
<article id="card-a" data-kanban-card="a" tabindex="0">First</article>
</div>
</section>
<section data-kanban-lane data-col="2" tabindex="-1" aria-label="Done">
<div data-kanban-lane-cards></div>
</section>
</sb-kanban-board>In production, pin today's whole catalog: its autoloader and an import map with integrity hashes, so the browser refuses any file that changed. This map covers Datastar and this component; importmap.json has the hashes of every component, to merge into yours.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/kanban-board.min.js": "sha384-M9MA234qf0muP0ZkPGaCHAhwgrwBUd3wf1hyvv5twOdjrEFtFpL7G8QmxAqMN6lu",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/contracts/kanban.min.js": "sha384-vaFAgbo2i4Wl7XaC4ElR4Dx06ZKgZXxfIzJ5WWbEkdR2Kf90lwKINcXDKgN+5Iso",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/drag-state.min.js": "sha384-5Y+afnEzy5l/9zFkAh+LOmom6361otYZ1mfJztIuXD7yOlTGx+TVHk1h9biAKgyX",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/flip.min.js": "sha384-1lsCQwc6EMFn4yzzI9FZbnF3Zbw6G59SB/cMrp533rqQ2gnI5eehw/RCrds3YJfr",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/focus-recovery.min.js": "sha384-JHh8h56l3OTHOT2hMP71pjVnh+AhoEBm41Fi2GetKZB35rHk5hVh0l+HXr92ZFqH",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/insertion-target.min.js": "sha384-WdOMwKgasfqQ4iOd9LI7wcmw3a4pAIkNdBRA63gnNUsjzw5NmDiyBKyf8jWkOB27",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/keyboard-staging.min.js": "sha384-WnYZ2saImC9lL8d4IfSLm3FT5TEzIPPsMviBcJe5NTuVh/vFpZm6/d0oMDYs8s8E",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/keyboard.min.js": "sha384-5AVhHD8gnbU5fkXCucowCxLCa0MxEQ7d2mQ9dRh+5eXhHEkfV8py2h6jinGH4PDY",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/ownership.min.js": "sha384-HhT3ZO2nxuGm6d66kwqQaE7Q9YADIW0cQqCQVTx5hKVW0D9PNvRBAqDxI26f8xBo",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/pointer-drag.min.js": "sha384-yslRfcHaD24bjpam90FqtgQVDHZXb4QT/GOfuQElOyb8qqgacm6PaesXPD9CbYa6",
"https://starbase.zweiundeins.gmbh/c/kanban-board@54c777be6a22/core/visual-outlets.min.js": "sha384-+AzW6If/d7MsA7+32XnB3SaY6VIbxTTzS8jc6fHLwSem6VyO3eH8ADPyLfTiSc3D"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@a7896c28d7cd/autoloader.js" integrity="sha384-avgqrgW9PG1hl/YoLOqUcsiTNPl4fikGkS1FmaQ01yZWKg402iP0IJLdzzl76811"></script>
<sb-kanban-board data-on:sb-kanban-move="@post('/board/move', {payload: evt.detail, requestCancellation: 'disabled'})">
<section data-kanban-lane data-col="1" tabindex="-1" aria-label="To do">
<div data-kanban-lane-cards>
<article id="card-a" data-kanban-card="a" tabindex="0">First</article>
</div>
</section>
<section data-kanban-lane data-col="2" tabindex="-1" aria-label="Done">
<div data-kanban-lane-cards></div>
</section>
</sb-kanban-board>Components import only datastar and their own files, so copying the folder is enough: save these files, keeping each component's folder, and point the import map at your own copy of datastar-rocket.js (the /js/ paths stand for yours).
<sb-kanban-board>kanban-board.jsminified 2.2 kB brotli 路 readable 14.0 kB, 3.1 kB brotlicontracts/kanban.jsminified 494 B brotli 路 readable 1.3 kB, 556 B brotlicore/drag-state.jsminified 489 B brotli 路 readable 1.4 kB, 530 B brotlicore/flip.jsminified 800 B brotli 路 readable 3.1 kB, 1.1 kB brotlicore/focus-recovery.jsminified 579 B brotli 路 readable 2.2 kB, 733 B brotlicore/insertion-target.jsminified 324 B brotli 路 readable 798 B, 430 B brotlicore/keyboard-staging.jsminified 616 B brotli 路 readable 2.8 kB, 822 B brotlicore/keyboard.jsminified 1.1 kB brotli 路 readable 4.3 kB, 1.5 kB brotlicore/ownership.jsminified 403 B brotli 路 readable 1.2 kB, 549 B brotlicore/pointer-drag.jsminified 1.4 kB brotli 路 readable 7.1 kB, 1.8 kB brotlicore/visual-outlets.jsminified 534 B brotli 路 readable 2.0 kB, 715 B brotli
<script type="importmap">
{ "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/kanban-board/kanban-board.min.js"></script>
<sb-kanban-board data-on:sb-kanban-move="@post('/board/move', {payload: evt.detail, requestCancellation: 'disabled'})">
<section data-kanban-lane data-col="1" tabindex="-1" aria-label="To do">
<div data-kanban-lane-cards>
<article id="card-a" data-kanban-card="a" tabindex="0">First</article>
</div>
</section>
<section data-kanban-lane data-col="2" tabindex="-1" aria-label="Done">
<div data-kanban-lane-cards></div>
</section>
</sb-kanban-board>Size
Each file compressed on its own, the way it is served (gzip -9, brotli -11). The autoloader loads the minified files (esbuild), so that last column is what a page downloads; the readable source is always there too. Datastar and Rocket are shared by every component and not counted.
| File | Original | gzip | brotli | minified |
|---|---|---|---|---|
kanban-board.js | 14.0 kB | 3.5 kB | 3.1 kB | 2.2 kB |
contracts/kanban.js | 1.3 kB | 667 B | 556 B | 494 B |
core/drag-state.js | 1.4 kB | 641 B | 530 B | 489 B |
core/flip.js | 3.1 kB | 1.2 kB | 1.1 kB | 800 B |
core/focus-recovery.js | 2.2 kB | 908 B | 733 B | 579 B |
core/insertion-target.js | 798 B | 502 B | 430 B | 324 B |
core/keyboard-staging.js | 2.8 kB | 969 B | 822 B | 616 B |
core/keyboard.js | 4.3 kB | 1.7 kB | 1.5 kB | 1.1 kB |
core/ownership.js | 1.2 kB | 677 B | 549 B | 403 B |
core/pointer-drag.js | 7.1 kB | 2.0 kB | 1.8 kB | 1.4 kB |
core/visual-outlets.js | 2.0 kB | 891 B | 715 B | 534 B |
| Total | 40.2 kB | 13.7 kB | 11.7 kB | 8.9 kB |
API reference
Props
This component takes no props.
Events
| Name | Description |
|---|---|
sb-kanban-move | A card was dropped, or a keyboard move committed. detail: { cardId, col, before }: col is the lane's data-col as a number, before the id of the card it now precedes in that lane, or "" for the end. |
sb-kanban-select | A card was selected: arrow focus moved to it, or the pointer pressed it. detail: { cardId }. |