Lineup Poster Section
A festival-poster lineup layout — names are grouped into rows by tier with the size stepping down each level, and hovering or clicking one promotes it to a bigger row with a shared layout animation while the rest re-justify to fill the width.
npx shadcn@latest add https://webberui.com/r/lineup-poster-section.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<LineupPosterSection />
Installation
npx shadcn@latest add https://webberui.com/r/lineup-poster-section.jsonOr, once registries are configured in components.json, install it as @webberui/lineup-poster-section.
Usage
import { LineupPosterSection } from "@/components/ui/lineup-poster-section";
const lineup = ["Aurora Halo", "Neon Tide", "Velvet Static", "Glass Meridian"];
<LineupPosterSection
names={lineup}
title="Sunrise Festival 2026"
footer="Jul 12–14 · Green Valley"
/>;The featured row pinned at the top always shows the currently promoted name (the first one by default). Hovering over any name previews the promotion instantly and it reverts when the pointer leaves the poster; clicking pins that name. You can also take over the promotion state with controlled mode:
const [promoted, setPromoted] = React.useState("Neon Tide-1");
<LineupPosterSection
names={lineup}
promotedId={promoted}
onPromotedChange={setPromoted}
trigger="click"
/>;Names can also be passed as full objects to give them stable ids of your own:
<LineupPosterSection
names={[
{ id: "aurora", name: "Aurora Halo" },
{ id: "neon", name: "Neon Tide" },
]}
defaultPromotedId="neon"
/>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
names | (string | { id, name })[] | — | The lineup, laid out in array order; strings generate an id as name-index |
promotedId | string | — | Controlled: the id of the name currently promoted to the featured row |
defaultPromotedId | string | the first name | Initially promoted name id in uncontrolled mode |
onPromotedChange | (id: string) => void | — | Callback when the promotion changes |
trigger | "hover" | "click" | "both" | "both" | How promotion is triggered |
revertOnLeave | boolean | true | Whether leaving the poster reverts to the name pinned by clicking |
tierCounts | number[] | automatic pyramid distribution | Capacity of each tier (excluding the featured row) |
separator | React.ReactNode | "•" | Separator between names |
title | React.ReactNode | — | Title above the poster |
footer | React.ReactNode | — | Note below the poster |
aria-label | string | "演出陣容" | Accessible label for the region |
How it works
- Shared layout animation: every name carries a
layoutId, so on promotion Motion tweens it continuously from its original tier row to the featured row.layout="position"is used deliberately (tweening position only, with the font size switching instantly) so large text is not stretched and blurred while scaling up. - Automatic tiering: when
tierCountsis not specified, the remaining names are cut into a pyramid of rows with increasing capacities of2, 3, 4, …, with the leftover tail merged into the last row and the size stepping down each level. - Justified rows: rows with several names use
justify-betweento fill the full width, separated by the separator; the featured row centres when it holds only one name. - Hover debounce:
mouseenteris briefly locked at the instant the promotion causes a reflow, so a stationary cursor cannot cause a chain of switches just because elements moved under it.
Accessibility
- Every name is a native
<button>, focusable with Tab and promotable with Enter / Space;aria-pressedreflects the currently pinned name. - Arrow keys (←→↑↓) additionally move focus through the original lineup order; moving focus does not itself trigger a promotion, so a reflow cannot interrupt it.
- After promoting with the keyboard or a click, if the name loses focus because it remounted into a different row, the component restores focus to the promoted name automatically.
- When the user has "reduce motion" enabled at the system level, the shared layout animation is disabled and promotion becomes an instant switch, leaving the layout structure completely unchanged.
Leading Unfold
Narrative through line height — a paragraph starts compressed into a near-solid block of ink, and scroll progress gradually unfolds the leading and tracking into a readable paragraph, growing the layout height and pushing the following sections down.
Type Scale Playground
An interactive modular type scale playground: pick a ratio or drag the slider, and every level on the page recomputes and reflows in sync with a spring animation.