Layout Composition

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.

How to install Pro components →See the plans →

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>"

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.28
2
320
0.06
Active edited usageDefault props
<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

PropTypeDefaultDescription
childrenReactNode—Put PinCard children here
connectionsPinConnection[][]Red strings between cards; either the [from, to] shorthand or { from, to, color }
positionsRecord<string, PinPosition>—Controlled coordinates, syncing outside state into the pinboard (still draggable)
defaultPositionsRecord<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
stringColorstring"#e11d48"String and pin color
sagnumber0.28How slack the string is (0–1); higher sags deeper and snaps taut sooner when pulled apart
stringWidthnumber2String width (px)
heightnumber400Pinboard height (px)
staggernumber0.06Interval between cards as they reveal in sequence (seconds)

PinCard

PropTypeDefaultDescription
idstring—Unique key, used for connection pairing and coordinate tracking
xnumber50Initial X coordinate (percentage 0–100, pointing at the pin at the top of the card)
ynumber50Initial Y coordinate (percentage 0–100, pointing at the pin at the top of the card)
labelstringidDisplay name, used for the accessible label
childrenReactNode—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; pass positions and the outside drives the layout, while dragging and keyboard movement still reflect immediately and report through onPositionsChange.
  • Surface styles: cork for corkboard speckle, paper for graph-paper gridlines, slate for plain slate — all of them adapt to light and dark mode.

Accessibility

  • Each card is a role="button", is keyboard focusable, and carries aria-roledescription="Draggable clue cards" (draggable clue card) plus an aria-label including 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, and ResizeObserver are all cleaned up on unmount.