Roadmap Lane Board
A Now / Next / Later three-lane roadmap board where item cards move between lanes with shared layout animation, supporting both read-only display and interactive dragging.
npx shadcn@latest add https://webberui.com/r/roadmap-lane-board.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<RoadmapLaneBoard />
Installation
npx shadcn@latest add https://webberui.com/r/roadmap-lane-board.jsonOr, once registries are configured in components.json, install it as @webberui/roadmap-lane-board.
Usage
Data-driven: lanes defines the lanes from left to right, and items are assigned to each lane by laneId. Turn on interactive to enable dragging and moving items with the arrow buttons.
import { RoadmapLaneBoard, type RoadmapItem } from "@/components/ui/roadmap-lane-board";
const items: RoadmapItem[] = [
{ id: "auth", laneId: "now", title: "Rework the sign-in flow", tag: "Eng" },
{ id: "billing", laneId: "next", title: "Usage-based billing", tag: "Growth" },
{ id: "ai", laneId: "later", title: "AI weekly digest", tag: "AI" },
];
export function Roadmap() {
const [list, setList] = React.useState(items);
return (
<RoadmapLaneBoard items={list} onItemsChange={setList} interactive />
);
}For a read-only display, just omit interactive and pass defaultItems:
<RoadmapLaneBoard defaultItems={items} />Custom lanes and status accent bars:
<RoadmapLaneBoard
lanes={[
{ id: "todo", label: "To do", accent: "#64748b" },
{ id: "doing", label: "In progress", accent: "#f59e0b" },
{ id: "done", label: "Done", accent: "#10b981" },
]}
defaultItems={items}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
lanes | RoadmapLane[] | Now / Next / Later | Lane definitions; determines the lane contents and their left-to-right order |
items | RoadmapItem[] | — | Controlled: all current items |
defaultItems | RoadmapItem[] | [] | Uncontrolled initial items |
onItemsChange | (items: RoadmapItem[]) => void | — | Fires when the item collection changes |
onItemMove | (itemId, fromLaneId, toLaneId) => void | — | Fires when a single item moves across lanes (reordering within the same lane does not fire it) |
interactive | boolean | false | Enables dragging and the promote buttons; false is a read-only display |
laneMinWidth | number | 176 | Minimum width of each lane (px); when lanes get too narrow the whole board scrolls horizontally |
laneMaxHeight | number | 260 | Maximum height of each lane's content area (px); past that, the lane scrolls vertically |
className | string | — | Styles for the outermost container |
RoadmapLane
| Field | Type | Description |
|---|---|---|
id | string | Unique lane id, matched against RoadmapItem.laneId |
label | string | Name shown in the lane header |
accent | string | Color of the status accent bar (any CSS color value); when omitted, a default palette color is picked by lane order |
RoadmapItem
| Field | Type | Description |
|---|---|---|
id | string | Unique item id; the pairing key for the shared layout animation between lanes |
laneId | string | Id of the lane the item currently sits in |
title | string | Card title |
description | React.ReactNode | Card description (optional) |
tag | string | Small label in the top-right corner (optional) |
How it works
- Every card carries a stable
layoutId, so when it moves across lanes Motion animates it with shared layout from its old position into the new lane, while the remaining cards FLIP into place at the same time. - Dragging uses native HTML5 drag-and-drop: on hover it computes the drop position within the lane from the pointer's Y coordinate and highlights the target lane, then inserts the card at that position on release.
- Controlled and uncontrolled modes: pass
items+onItemsChangeto drive it from outside, or pass onlydefaultItemsand the component manages it internally. - The count badge on each lane reflects that lane's item count in real time; the accent bar in the lane header can be colored to match your own semantics (status, priority).
Accessibility
- Each lane is its own
sectionwith anaria-labelcarrying the lane name and item count; inside, the lane is arole="list"and each card arole="listitem". - Touch and keyboard users can promote or defer an item with the arrow buttons on the card; the buttons carry explicit
aria-labels (for example "Move 'Usage-based billing' to Later") and are disabled automatically at the first and last lane. - After a cross-lane move the new state is announced through
aria-live="polite", so assistive technology reads it immediately. - When the user has "reduce motion" enabled at the system level, the shared layout and FLIP animations are disabled and moves land instantly; functionality is unaffected.
Mouse Parallax Scene
Multiple layers shift with the mouse to create depth, with a gyroscope fallback.
Case Study Dossier
A dossier-style case study layout — a sticky metrics rail on the left lights up and counts as the long-form copy scrolls by, while full-bleed pull quotes periodically break the two-column rhythm to let it breathe.