String Pinboard
A red-string pinboard layout — cards are clues pinned to corkboard, connected by red strings that sag naturally, with slack and droop changing live as you drag.
This is a WebberUI Pro component
Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.
npx shadcn@latest add "https://webberui.com/r/string-pinboard.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<StringPinboard />
Installation
npx shadcn@latest add "https://webberui.com/r/string-pinboard.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/string-pinboard.
Usage
Place clue cards with PinCard, and define the red strings between them with connections. Drag any card and every connected string changes its slack and droop live.
import { StringPinboard, PinCard } from "@/components/ui/string-pinboard";
<StringPinboard
connections={[
["idea", "design"],
["design", "ship"],
]}
variant="cork"
height={360}
>
<PinCard id="idea" x={18} y={20} label="Idea">
Idea
</PinCard>
<PinCard id="design" x={52} y={16} label="Design">
Design
</PinCard>
<PinCard id="ship" x={76} y={64} label="Ship">
Ship
</PinCard>
</StringPinboard>;connections also accepts an object form to override the color of an individual string:
connections={[{ from: "idea", to: "design", color: "#0ea5e9" }]}Props
StringPinboard
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Put PinCard children here |
connections | PinConnection[] | [] | Red strings between cards; either the [from, to] shorthand or { from, to, color } |
positions | Record<string, PinPosition> | — | Controlled coordinates, syncing outside state into the pinboard (still draggable) |
defaultPositions | Record<string, PinPosition> | — | Initial coordinates in uncontrolled mode, overriding each PinCard's own x/y |
onPositionsChange | (positions) => void | — | Fires after any card moves, with all the latest coordinates |
variant | "cork" | "paper" | "slate" | "cork" | Pinboard surface style |
stringColor | string | "#e11d48" | String and pin color |
sag | number | 0.28 | How slack the string is (0–1); higher sags deeper and snaps taut sooner when pulled apart |
stringWidth | number | 2 | String width (px) |
height | number | 400 | Pinboard height (px) |
stagger | number | 0.06 | Interval between cards as they reveal in sequence (seconds) |
PinCard
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique key, used for connection pairing and coordinate tracking |
x | number | 50 | Initial X coordinate (percentage 0–100, pointing at the pin at the top of the card) |
y | number | 50 | Initial Y coordinate (percentage 0–100, pointing at the pin at the top of the card) |
label | string | id | Display name, used for the accessible label |
children | ReactNode | — | Card content |
How it works
- Catenary droop: each string records a "resting rope length" on first measurement (stored as a proportion of the pinboard diagonal), and that length is fixed and does not change as you drag. When two cards are pulled apart and the chord length approaches the rope length, the string gradually goes taut; bring them closer and the droop deepens, matching the feel of a real hanging string.
- Coordinate system: card coordinates are percentages (0–100) pointing at the pin at the top of the card, and the card hangs down from that pin — so the layout scales proportionally when the pinboard is resized.
- Controlled and uncontrolled modes: with no
positions, the component tracks the coordinates itself; passpositionsand the outside drives the layout, while dragging and keyboard movement still reflect immediately and report throughonPositionsChange. - Surface styles:
corkfor corkboard speckle,paperfor graph-paper gridlines,slatefor plain slate — all of them adapt to light and dark mode.
Accessibility
- Each card is a
role="button", is keyboard focusable, and carriesaria-roledescription="可拖曳的線索卡"(draggable clue card) plus anaria-labelincluding its connection count. - Once a card has focus, the arrow keys nudge its position in all four directions, and those moves also report through
onPositionsChange. - When the user has "reduce motion" enabled at the system level, the card reveal and the string drawing animation are disabled and the final layout is shown directly; dragging and keyboard operation are unaffected.
- Dragging is implemented with pointer events and supports touch (
touch-action: none); listeners,requestAnimationFrame, andResizeObserverare all cleaned up on unmount.
Archive Index
A studio-archive-style full-page index table — multiple sortable columns that reorder with FLIP, and rows that expand into full-width detail while the rest elastically make way.
Art Direction Morph
One set of headline, image, and body copy morphing seamlessly between several magazine-grade layouts with FLIP layout animation, plus a keyboard-driven tab indicator switcher.