# Kanban Board

> A kanban board with drag and drop: cards lift and tilt as you drag between columns, other cards Flip aside and column counts roll.

Canonical: https://gsapvault.com/ui-elements/kanban-drag-board
Live demo: https://gsapvault.com/demos/kanban-drag-board/index.html

| Property | Value |
|----------|-------|
| Type | ui-element |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | Flip |
| Techniques | flip-layout, drag, keyboard-reorder, touch-drag, velocity-reactive, micro-interaction |
| Uses Lenis | No |

## Lighthouse, as measured

Google Lighthouse on the demo, 15 September 2026. A measurement of the demo as shipped, not a promise for your page.

| Category | Score |
|----------|-------|
| Accessibility | 100 |
| Best practices | 100 |

No performance score is published: it depends on the deployment (server compression, caching, CDN, connection and device) rather than on the code, so measure it where it will live.

## Overview

A kanban board where the space a card will land in is always visible. Pick up a card and it lifts: a small scale, a longer shadow, a 1px accent ring and a tilt that leans with the pointer's horizontal speed and settles back as the pointer slows. A slot the height of the card opens under it, in its own column or any other, and every card in the way Flips aside to make room. Let go and the card settles into the slot on a short damped ease. The column counts roll up and down as the slot crosses between columns, a card dropped into Done has its title struck through with a quick draw, and dragging a card out of Done takes the line away again. An emptied column shows a drop zone that takes a card like any other.

The keyboard can do everything the pointer can. Focus a card, press Space to pick it up, move it with the arrow keys within a column and across columns, then Space to drop or Escape to put it back where it started. Each step is announced through a live region: "Moved 'Audit the onboarding email sequence' to In progress, position 2 of 4." On a phone a long press of about 200ms picks a card up, so an ordinary swipe still scrolls the page; the columns become a horizontal snap scroller, one column per view, and holding a card near the edge pages to the next column.

Every committed move dispatches a bubbling kanban:move event with the card id, the source and target columns and the new index, so saving the board is one listener. One HTML file, one stylesheet, one script: vanilla JavaScript with GSAP and the Flip plugin, no framework. Light and dark themes come from one set of custom properties switched by a data-variant attribute.

## Features

- Cards drag across and within columns, lifted with a scale, a shadow and a 1px accent ring
- A tilt driven by the pointer's horizontal velocity, damped twice so it leans and recovers rather than wobbles
- A slot the height of the card opens where it will land, and every other card Flips aside, across columns too
- A settle on drop from wherever the card is drawn into the slot, with no transform left on any card
- Column counts that roll up or down as they change, including while the slot is crossing columns
- Dropping into the done column draws a strike through the title; dragging out removes it
- An empty column shows a drop zone that accepts a card
- Full keyboard moves: Space picks up, arrows move within and across columns, Space drops, Escape cancels to the origin
- A polite live region announcing every pick-up, move, drop and cancel by column and position
- Touch-aware: a 200ms long press picks up, so swipes still scroll; a snap scroller of one column per view on a phone, with edge paging while dragging
- A bubbling kanban:move event carrying id, from, to and index
- Light and dark themes on one set of custom properties

## Use Cases

- Task and issue boards in a project management or internal tool
- Sales pipelines and hiring funnels where each card is a deal or a candidate
- Content calendars and editorial workflows moving pieces from draft to published
- Support queues and triage boards in an admin console
- Onboarding checklists a customer moves through by stage

## Vibe-Code Ready Setup

This UI element includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

### Picking up, and the slot that follows

When a press becomes a drag (4px of net travel with a mouse, a 200ms hold with a finger) the card's place in its list is taken by a slot of the same height, and the card itself is lifted out, fixed to the viewport where it was, so it can travel over every column and past the edge of a scrolling container. On every move the component works out which column is under the pointer and how many resting cards sit above the card's middle; resting positions come from offsetTop, so the transforms of cards still in flight never make the answer flicker.

### Flip for everything that makes room

Moving the slot is one DOM insertion wrapped in Flip.getState and Flip.from, so every card that has to make room, in the same column and in the column the slot just left, glides from its old position to its new one. Interrupting a Flip mid-flight with another move is safe: the next state is captured from where the cards are drawn, not where they were headed.

### The tilt, the settle and the strike

A ticker smooths the pointer's horizontal speed and then eases the rotation toward it, so a quick flick leans the card a few degrees and a pause brings it upright. On release the card goes back into the list where the slot was and is tweened from its drawn position, rotation and scale back to rest. The done strike is a background line on the inline title, so it follows every line of a wrapped title, drawn by tweening one custom property from 0% to 100%.

### Keyboard, touch and reduced motion

Keyboard moves use the same Flip, with focus kept on the card through each DOM move and a blur the component did not cause dropping the card rather than stranding it. On touch the drag only starts after a still long press, and from then the touchmove is cancelled so the page stays put. Under reduced motion every move happens instantly with no tilt, which keeps the board fully usable.

## Documentation

How this UI element works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### What's Included

- `index.html`: the demo page and the markup to copy
- `assets/style.css`: component styles, plain CSS custom properties for colours
- `assets/script.js`: readable, commented source with an `onReady` guard

- Cards drag across and within columns, lifted with a small scale, a longer shadow and a 1px accent ring
- A tilt driven by the pointer's horizontal velocity, smoothed and then eased, so a flick leans the card a few degrees and a pause brings it upright
- A slot the height of the card opens where it will land, and every card in the way glides aside with GSAP Flip, in the same column and across columns
- A settle on drop from wherever the card is drawn into the slot, after which no card carries a leftover transform
- Column counts that roll up or down when they change, including while the slot crosses between columns
- A strike drawn through the title when a card lands in the done column, removed again when it leaves
- An empty-state drop zone in any column that has no cards
- A full keyboard model with a polite live region, and a long press on touch so ordinary swipes still scroll
- A bubbling `kanban:move` event carrying the card id, the two columns and the new index
- Roles used: `<section>` columns labelled by their `<h2>`, `<ul>` / `<li>` card lists, focusable cards described by the instruction line, and a `role="status" aria-live="polite"` region

### Quick Start

**1. Add to your `<head>`:**

_Code snippet omitted: it ships with the download._

**2. Add before the closing `</body>` tag:**

_Code snippet omitted: it ships with the download._

**3. Add the board markup** (one column shown; copy the whole board from `index.html`):

_Code snippet omitted: it ships with the download._

The script only looks for the `data-` attributes, so the classes are yours to rename (the one exception is `kb-count-num`, the class it gives the digits it creates, and `kb-slot`, the class of the landing slot):

| Attribute | On | What it does |
|-----------|----|--------------|
| `data-kanban` | the board root | Every board on the page with this attribute is wired up on its own |
| `data-kanban-scroller` | the element holding the columns | Optional. When it scrolls sideways (the phone layout), holding a dragged card near its edge pages to the next column |
| `data-kanban-col="id"` | a column | The value is what `kanban:move` reports as `from` and `to` |
| `data-kanban-done` | the done column | Cards dropped here are struck through; cards dragged out lose the line |
| `data-kanban-col-title` | the column heading | The column name used in announcements |
| `data-kanban-count` | the count badge | Rewritten and rolled by the script whenever the column's count changes |
| `data-kanban-list` | the card container | Cards must be its direct children. Keep it `position: relative`: card positions are measured against it |
| `data-kanban-card` | a card | Needs `tabindex="0"`; `data-id` is the id reported in the event |
| `data-kanban-title` | an inline element in a card | The title, used in announcements and carrying the strike |
| `data-kanban-empty` | an element in a column | The empty-state drop zone, shown only while the column holds no cards |
| `data-kanban-live` | any element in the board | The live region. Optional; leave it out and the board is silent |

Two pieces of CSS the component depends on if you restyle from scratch: **no card may carry a CSS `transform`** (GSAP reads one as pixels and a tween back to rest never clears it), and `.kb-card.is-dragging` must stay `position: fixed; pointer-events: none`, because the card travels over the viewport while it is held.

The theme switcher, the `.showcase-toolbar` and the `.stage` wrapper in `index.html` are demo furniture, not part of the component; leave them behind.

#### Saving a move

_Code snippet omitted: it ships with the download._

A drop back in the slot it came from, or a cancel, changes nothing and fires nothing.

### Keyboard & Accessibility

| Key | What it does |
|-----|--------------|
| `Tab` | Moves through the cards |
| `ArrowUp` / `ArrowDown` | Moves focus to the card above or below; while a card is picked up, moves the card itself |
| `ArrowLeft` / `ArrowRight` | Moves focus to the nearest card in the neighbouring column; while picked up, moves the card into that column at the same position |
| `Home` / `End` | While picked up, sends the card to the top or bottom of its column |
| `Space` / `Enter` | Picks up the focused card, or drops the one that is picked up |
| `Escape` | Cancels, returning the card to the column and position it was picked up from. Also cancels a pointer drag in progress |

- Announcements go to the `role="status" aria-live="polite"` region: `"Picked up 'Draft the Q4 pricing page copy', To do, position 1 of 4. Arrow keys move it, Space drops it, Escape cancels."`, then `"Moved 'Draft the Q4 pricing page copy' to In progress, position 2 of 4."`, then a drop or `"Move cancelled. 'Draft the Q4 pricing page copy' is back in To do, position 1 of 4."`
- Each card points at the instruction line with `aria-describedby`, and columns are `<section>`s labelled by their headings, so a screen reader hears "In progress, list, 3 items".
- Moving a card in the DOM blurs it, so the component puts focus straight back and ignores that one blur. A blur it did not cause drops the card where it is rather than stranding it picked up.
- Touch: a still press of about 200ms picks a card up. A finger that moves before then is a scroll and the page gets it, so the board never traps vertical scrolling on a phone. After the pick-up the component cancels `touchmove`, so the page holds still under the drag, and the long-press callout and context menu are suppressed.
- `prefers-reduced-motion`: the same moves with every duration at zero and no tilt. Cards change places instantly; nothing is disabled.
- Without JavaScript the board is a readable set of lists, with the done column already struck through.

### How It Works

**Picking up.** A mouse press becomes a drag after 4px of net travel from the press point (net, not accumulated, so a wobbly click stays a click); a finger needs a 200ms hold. The card's place is taken by a slot of the same height and the card is lifted out to the board root, fixed to the viewport where it was, so it can cross every column and the edge of a scrolling container. `pointermove` and `pointerup` are bound to `window` for the life of the gesture rather than captured on the card, because a pointer capture is dropped the moment its element moves in the DOM.

**The slot, and Flip.** On every move the component picks the column under the pointer (or the nearest visible one) and counts the resting cards whose middle is above the dragged card's middle. Resting positions come from `offsetTop`, which ignores transforms, with the slot's height taken back out, so cards still gliding never make the target flicker. When the target changes, the slot moves with one `insertBefore` wrapped in `Flip.getState()` and `Flip.from()`, which is what glides every card that has to make room, in both columns.

**The tilt and the settle.** A `gsap.ticker` callback smooths the pointer's horizontal speed and eases the rotation toward it, capped at 7 degrees, which is why the lean is felt rather than seen as wobble. On release the card replaces the slot and is tweened with `gsap.fromTo` from its drawn position, rotation and scale back to rest, then its transform is cleared outright.

**Counts and the strike.** A count badge stacks the old and new digits in one grid cell and rolls them past each other with `yPercent`, up when the number rises and down when it falls. The strike is a background line on the inline title, so it follows every line of a wrapped title; the script tweens its `--strike` custom property from `0%` to `100%` to draw it.

**Phones.** Under 640px the columns become a horizontal `scroll-snap` scroller, one column per view with a sliver of the next. Holding a dragged card within 44px of the scroller's edge for a quarter of a second pages to the next column, and the target is recomputed as it scrolls.

### Customisation

- `--accent` is the focus ring, the slot outline and the lifted card's ring; `--accent-wash` is the slot's fill.
- The timings and the gesture tuning (`LIFT_SCALE`, `FLIP_TIME`, `SETTLE_TIME`, `TOUCH_HOLD`, `TILT_MAX`, `EDGE` and friends) are named constants at the top of `assets/script.js`.
- Add or remove columns freely: the board reads whatever `[data-kanban-col]` elements it contains. Change `grid-template-columns` on `.kb-cols` to match.
- To render cards from your own data, give each new card the same markup and attributes; the board reads the DOM on every gesture, so nothing needs re-initialising.

### Themes

The board ships in two themes, `light` (the default) and `dark`, which are one design at two token values. Set the attribute on `<body>`, or on the board's own wrapper in your app:

_Code snippet omitted: it ships with the download._

Every colour, radius and shadow the board uses is a custom property in the matching `body[data-variant="..."]` block in `assets/style.css`: `--ground`, `--ground-2`, `--raised`, `--hover`, `--ink`, `--ink-2`, `--ink-3`, `--line`, `--line-strong`, `--avatar`, `--accent`, `--accent-ink`, `--accent-wash`, `--shadow`, `--card-shadow`, `--lift-shadow`, `--radius`, `--radius-sm`, `--radius-xs` and `--font`. Nothing in `assets/script.js` reads the theme name, so re-valuing one block is the whole of a rebrand: point `--accent` at your brand colour and the surfaces and `--ink-*` ramp at your own. The demo's `?variant=dark` URL parameter only sets the same attribute.

### Requirements

- GSAP 3.12+ with the Flip plugin
- No build step, no framework

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium UI element. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required for our code. Bundled third-party assets retain their own licences and attribution requirements. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Kanban Board](https://gsapvault.com/ui-elements/kanban-drag-board)
- [The Vault (£99 one-time, best value): every collection in the Vault library, plus future items added to those collections](https://gsapvault.com/effects)

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
