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
The install command below needs a licence to run; without one it returns 401.
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 "https://webberui.com/r/string-pinboard.en.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/>
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/string-pinboard.en.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 cards"(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.