Display
Kanban
A stage board on top of the drag engine: columns that scroll on their own, cards that move by mouse, touch or keyboard, per-column loading and a mobile tab strip.
<script>
import { Kanban } from "@codeatlasdev/ui";
let columns = $state([
{ id: "new", label: "New", color: "var(--ca-info-main)", total: 42, hasMore: true, items },
{ id: "won", label: "Won", color: "var(--ca-success-main)", items: [] },
]);
</script>
<Kanban {columns} getId={(card) => card.id} onmove={({ card, from, to }) => {}} onloadmore={(columnId) => {}}>
{#snippet card(item, { dragging })}…{/snippet}
</Kanban>
Kanban is the board around useDnd: you pass columns of cards and apply onmove to your data; the component owns the layout, the drag, the keyboard map and the touch alternative. Give it a parent with a height (a flex child or height), because every column list scrolls on its own. A plain wrapper with a scoped class is enough, no :global needed:
A plain wrapper with a scoped class is enough, no :global needed:
<div class="board"><Kanban {columns} getId={(card) => card.id} …>…</Kanban></div>
<style>
.board { display: flex; height: 28rem; }
</style>
Leads pipeline
Drag a card to another column, lift it with Space and move it with the arrows, or use the menu on the card. Scroll the New column to load more. Narrow the window below 600px for the tab strip.
Press Space to lift, the arrow keys to move, Enter to drop and Escape to cancel. Touch: press and hold, then drag.
New
24- N lead 1 Atlas Co
- N lead 2 Atlas Co
- N lead 3 Atlas Co
- N lead 4 Atlas Co
- N lead 5 Atlas Co
- N lead 6 Atlas Co
- N lead 7 Atlas Co
- N lead 8 Atlas Co
Talking
3- T lead 1 Atlas Co
- T lead 2 Atlas Co
- T lead 3 Atlas Co
Won
0- Drop a lead here to win it
Reference #760 · Pick in the gallery
Code
Column summary
Pass a summary snippet to show an aggregate (a money total, a weight, hours) after each column’s count. The colour rail, title and count stay; header is for replacing the whole row and drops all three. The snippet receives the column, so compute from column.items; for a server-paged column (total larger than the loaded items) send the aggregate with the column data instead. On the mobile tab layout the summary shows as a row above the active column’s cards.
Money total per column
Each column shows the sum of its deals next to the count. Move a card and both totals follow.
Press Space to lift, the arrow keys to move, Enter to drop and Escape to cancel. Touch: press and hold, then drag.
New
3 $5,400- N deal 1 $1,200
- N deal 2 $3,400
- N deal 3 $800
Talking
2 $24,800- T deal 1 $9,800
- T deal 2 $15,000
Won
1 $22,000- W deal 1 $22,000
Reference #761 · Pick in the gallery
Code
Drops
By default a column is the drop target as a whole (drop="container"): it is highlighted, no insertion line is drawn and to.index is always 0. That is right for columns the server orders (newest first), where dropping “between cards” would mean nothing. Set drop="position" to let the card land between cards; the line appears and to.index is the position.
Moving cards
| Input | How it works |
|---|---|
| Mouse and pen | Press a card and move 4px. The empty board background pans sideways; a vertical wheel over the gaps scrolls the board sideways; trackpad deltaX stays native. |
| Touch | Press and hold, then drag. Near the left or right edge the board scrolls, faster the closer you are. |
| Keyboard | Focus a card, Space lifts it, Left and Right move it to the same index of the neighbouring column (an empty column receives it), Up, Down, Home and End move inside a column (with drop="position"), Enter drops, Escape cancels. Every step is announced. |
| Menu | Every card has a “Move to…” button that opens a menu of the other columns. It is the touch and screen reader alternative to dragging and announces the result. |
Loading more
A column with hasMore fires onloadmore(columnId) when its list scrolls within nearEnd pixels of the end (once per page: it waits until the item count changes) and shows a “Load more (N)” button, where N is total - items.length. Set loadingMore on the column while the page loads to show skeleton cards, and loading for the first page.
Heading level
Each column title is a heading, <h2> by default (inside a PageShell it follows the page’s section level), so a board under the page <h1> keeps the heading order for axe and screen readers. Pass headingLevel={3} when the board sits under an <h2> of its own. Only the tag changes, never the look. A custom header snippet replaces the title row, so you choose its tag; the mobile tab layout renders no column headings.
Mobile
Below 600px of its own width (or with compact) the board shows one column at a time. A role="tablist" strip with a colour dot, the label and the count selects the column (arrow keys move between tabs; every tab is 44px tall), and the board never scrolls sideways. The “Move to…” menu replaces dragging between columns.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | KanbanColumn<T>[] | required | `{ id, label, color?, items, total?, hasMore?, loading?, loadingMore? }` per column. `color` is any CSS colour or token. |
getId | (card: T) => string | required | Stable id of a card. |
onmove | (move: { card, from, to }) => void | undefined | A card moved by drag, keyboard or the menu. `from` and `to` are `{ column, index }`. |
onloadmore | (columnId: string) => void | undefined | A column wants its next page. |
canMove | boolean | true | `false` turns dragging off and hides the menu. |
drop | 'container' | 'position' | 'container' | Whole-column drop (index 0, no line) or positional drop. |
compact | boolean | undefined | Force the mobile tab layout. Default: automatic below 600px of the board's width. |
label | string | 'Board' | Accessible name of the board region. |
headingLevel | 2 | 3 | 4 | 5 | 6 | 2 | Heading level of the column titles; only the tag changes. 2 under a page `<h1>`, 3 under an `<h2>`. Not used by a custom `header` or the mobile tab layout. |
nearEnd | number | 240 | Distance from the end of a list (px) at which `onloadmore` fires. |
card | Snippet<[T, { dragging, active }]> | required | Card content; the wrapper, focus, drag state and menu are the board's. |
empty | Snippet<[KanbanColumn<T>, { canMove }]> | undefined | Content of an empty column. |
header | Snippet<[KanbanColumn<T>]> | undefined | Replaces the colour rail, title and count. |
summary | Snippet<[KanbanColumn<T>]> | undefined | Aggregate shown after the count (a money total); rail, title and count stay. A row above the cards on mobile. Ignored when `header` is set. |