WebberUI

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.

How to install Pro components →See the plans →

Loading preview…
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.

0.28
2
320
0.06
<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

PropTypeDefaultDescription
childrenReactNodePut 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) => voidFires 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
idstringUnique 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
childrenReactNodeCard 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 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.

On this page