Interaction Choreography

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.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add column-transit-board.en

New project or installation trouble? Start here →

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
Active edited usageDefault props
<ColumnTransitBoard/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/column-transit-board.en.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.ReactNode—Put TransitColumn children directly inside, and TransitCards inside those
flightDurationnumber0.6Duration of a card's arcing flight between columns (seconds)
classNamestring—Appended to the board container's class

TransitColumn

PropTypeDefaultDescription
idstring—Unique key for the column, used for column-change detection and positioning the landing glow
titleReact.ReactNode—Column 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.ReactNode—Put TransitCard children directly inside
classNamestring—Appended to the column container's class

TransitCard

PropTypeDefaultDescription
idstring—Unique key for the card; must be unique across the board and stable (the cross-column FLIP pairs on it)
titleReact.ReactNode—Card title (default content when no children are given)
descriptionReact.ReactNode—Card description (default content when no children are given)
accentAccent"neutral"Card accent colour, applied to the bar on the left
childrenReact.ReactNode—Custom card content; overrides title / description when provided
classNamestring—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 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.