WebberUI

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.

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

<LineupPosterSection />

Installation

npx shadcn@latest add https://webberui.com/r/lineup-poster-section.json

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

PropTypeDefaultDescription
names(string | { id, name })[]The lineup, laid out in array order; strings generate an id as name-index
promotedIdstringControlled: the id of the name currently promoted to the featured row
defaultPromotedIdstringthe first nameInitially promoted name id in uncontrolled mode
onPromotedChange(id: string) => voidCallback when the promotion changes
trigger"hover" | "click" | "both""both"How promotion is triggered
revertOnLeavebooleantrueWhether leaving the poster reverts to the name pinned by clicking
tierCountsnumber[]automatic pyramid distributionCapacity of each tier (excluding the featured row)
separatorReact.ReactNode"•"Separator between names
titleReact.ReactNodeTitle above the poster
footerReact.ReactNodeNote below the poster
aria-labelstring"演出陣容"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 tierCounts is not specified, the remaining names are cut into a pyramid of rows with increasing capacities of 2, 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-between to fill the full width, separated by the separator; the featured row centres when it holds only one name.
  • Hover debounce: mouseenter is 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-pressed reflects 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.

On this page