Column Transit Board
A cross-column transit board — cards fly along an arc between columns because of a programmatic state change (not a drag), while the target column opens a landing spot, neighbours in the source column close ranks, and the flying card lifts with a shadow.
npx shadcn@latest add https://webberui.com/r/column-transit-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.
<ColumnTransitBoard />
Installation
npx shadcn@latest add https://webberui.com/r/column-transit-board.jsonOr, once registries are configured in components.json, install it as @webberui/column-transit-board.
Usage
Which column a card belongs to is decided entirely by your state — move a TransitCard into a different TransitColumn in your JSX and the board detects the column change and plays the flight animation. This is a purely controlled component; it holds no internal layout state.
import * as React from "react";
import {
ColumnTransitBoard,
TransitColumn,
TransitCard,
} from "@/components/ui/column-transit-board";
const COLUMNS = [
{ id: "todo", title: "To do" },
{ id: "doing", title: "In progress" },
{ id: "done", title: "Done" },
];
const [placement, setPlacement] = React.useState<Record<string, string>>({
a: "todo",
b: "doing",
});
<ColumnTransitBoard>
{COLUMNS.map((col) => (
<TransitColumn key={col.id} id={col.id} title={col.title}>
{Object.entries(placement)
.filter(([, colId]) => colId === col.id)
.map(([cardId]) => (
<TransitCard key={cardId} id={cardId} title={cardId} />
))}
</TransitColumn>
))}
</ColumnTransitBoard>;
// Change column: updating the state triggers the flight
setPlacement((prev) => ({ ...prev, a: "doing" }));Props
ColumnTransitBoard
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Put TransitColumn children directly inside, and TransitCards inside those |
flightDuration | number | 0.6 | Duration of a card's arcing flight between columns (seconds) |
className | string | — | Appended to the board container's class |
TransitColumn
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique key for the column, used for column-change detection and positioning the landing glow |
title | React.ReactNode | — | Column title (also used as the aria-label when it is a string) |
accent | Accent | "neutral" | Column accent colour, applied to the marker dot and the landing glow |
children | React.ReactNode | — | Put TransitCard children directly inside |
className | string | — | Appended to the column container's class |
TransitCard
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique key for the card; must be unique across the board and stable (the cross-column FLIP pairs on it) |
title | React.ReactNode | — | Card title (default content when no children are given) |
description | React.ReactNode | — | Card description (default content when no children are given) |
accent | Accent | "neutral" | Card accent colour, applied to the bar on the left |
children | React.ReactNode | — | Custom card content; overrides title / description when provided |
className | string | — | Appended to the card's class |
Accent is "neutral" | "blue" | "violet" | "sky" | "emerald" | "amber" | "rose".
How it works
- Tracked by
id, not by DOM node: when a card changes column, React unmounts it in the source column and remounts it in the target, so the board records its before and after positions keyed byid(FLIP) — which lets the flight continue seamlessly across the remount. - Only structural changes cause a re-measure: the board computes a "column → cards" layout fingerprint on every render; when the fingerprint is unchanged it does not measure at all and never disturbs an animation in progress.
- Three-part choreography: the transiting card flies along a parabolic arc while lifting its shadow (
z-indexfloats it to the top); the neighbouring cards in the same column close ranks in a straight line with a faster spring, so the landing spot in the target column "opens first"; and a ring-shaped landing glow appears in the target column at the same time. - Changing column mid-flight: the previous flight immediately settles at its landing spot, then re-choreographs from the new starting point, keeping the state consistent.
Accessibility
- When the user has "reduce motion" enabled at the system level, cards arrive in place directly with no flight, and the layout structure is unchanged.
- The board is
role="group", each column isrole="list"(labelled with its title asaria-label), and cards arerole="listitem". - The landing glow is a decorative layer (
aria-hidden,pointer-events-none) and affects neither layout measurement nor interaction.
Roving Highlight System
One shared morphing highlight surface that travels between any registered elements across tabs, sidebars, and grids, with built-in roving tabindex keyboard navigation and a synchronized focus ring.
Coachmark Tour Flow
A product tour overlay — an SVG mask morphs the spotlight between target elements while the explanation card anchors to the best side and flies along as you step, tracking the target through scroll.