WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/roadmap-lane-board.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

176
260
<RoadmapLaneBoard />

Installation

npx shadcn@latest add https://webberui.com/r/roadmap-lane-board.json

Or, 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

PropTypeDefaultDescription
lanesRoadmapLane[]Now / Next / LaterLane definitions; determines the lane contents and their left-to-right order
itemsRoadmapItem[]Controlled: all current items
defaultItemsRoadmapItem[][]Uncontrolled initial items
onItemsChange(items: RoadmapItem[]) => voidFires when the item collection changes
onItemMove(itemId, fromLaneId, toLaneId) => voidFires when a single item moves across lanes (reordering within the same lane does not fire it)
interactivebooleanfalseEnables dragging and the promote buttons; false is a read-only display
laneMinWidthnumber176Minimum width of each lane (px); when lanes get too narrow the whole board scrolls horizontally
laneMaxHeightnumber260Maximum height of each lane's content area (px); past that, the lane scrolls vertically
classNamestringStyles for the outermost container

RoadmapLane

FieldTypeDescription
idstringUnique lane id, matched against RoadmapItem.laneId
labelstringName shown in the lane header
accentstringColor of the status accent bar (any CSS color value); when omitted, a default palette color is picked by lane order

RoadmapItem

FieldTypeDescription
idstringUnique item id; the pairing key for the shared layout animation between lanes
laneIdstringId of the lane the item currently sits in
titlestringCard title
descriptionReact.ReactNodeCard description (optional)
tagstringSmall 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 + onItemsChange to drive it from outside, or pass only defaultItems and 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 section with an aria-label carrying the lane name and item count; inside, the lane is a role="list" and each card a role="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.

On this page