WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/column-transit-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.

0.6
<ColumnTransitBoard />

Installation

npx shadcn@latest add https://webberui.com/r/column-transit-board.json

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

PropTypeDefaultDescription
childrenReact.ReactNodePut TransitColumn children directly inside, and TransitCards inside those
flightDurationnumber0.6Duration of a card's arcing flight between columns (seconds)
classNamestringAppended to the board container's class

TransitColumn

PropTypeDefaultDescription
idstringUnique key for the column, used for column-change detection and positioning the landing glow
titleReact.ReactNodeColumn title (also used as the aria-label when it is a string)
accentAccent"neutral"Column accent colour, applied to the marker dot and the landing glow
childrenReact.ReactNodePut TransitCard children directly inside
classNamestringAppended to the column container's class

TransitCard

PropTypeDefaultDescription
idstringUnique key for the card; must be unique across the board and stable (the cross-column FLIP pairs on it)
titleReact.ReactNodeCard title (default content when no children are given)
descriptionReact.ReactNodeCard description (default content when no children are given)
accentAccent"neutral"Card accent colour, applied to the bar on the left
childrenReact.ReactNodeCustom card content; overrides title / description when provided
classNamestringAppended 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 by id (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-index floats 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 is role="list" (labelled with its title as aria-label), and cards are role="listitem".
  • The landing glow is a decorative layer (aria-hidden, pointer-events-none) and affects neither layout measurement nor interaction.

On this page