Pain Point Orbit
As you type into the input, surrounding pain-point cards crowd inward one by one; on submit they scatter away — a marketing visual for the pain of fragmented tools.
An interactive hero block for marketing pages: a real, typeable input sits in the center, with 6–10 small pain-point cards on an elliptical orbit around it. The more you type, the higher the pressure — cards move inward one by one in array order, tilting slightly, shrinking and overlapping; once the character count reaches pressureChars the whole cluster does a subtle squeeze-and-breathe. Press Enter or the submit button and the cards fly outward along their angles and fade, while the center swaps to the resolved node you provide. Turn on autoPlay to simulate typing on a loop; the moment the user clicks the input, control is handed back. On narrow screens the cards stack in two rows above and below the input.
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 pain-point-orbit.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<PainPointOrbit/>
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/pain-point-orbit.en.jsonOr, once registries are configured in components.json, install it as @webberui/pain-point-orbit.
Usage
import { Mail, Sheet, CalendarClock } from "lucide-react";
import { PainPointOrbit } from "@/components/ui/pain-point-orbit";
<PainPointOrbit
points={[
{ id: "quote", text: "Quotes buried somewhere in the inbox", icon: <Mail /> },
{ id: "sheets", text: "Client data split across three spreadsheets", icon: <Sheet /> },
{ id: "weekly", text: "Compiling the weekly report by hand", icon: <CalendarClock /> },
]}
placeholder="What do you want to tackle first today?"
autoPlay
demoText="Pull together the latest quotes for every client"
resolved={<div className="rounded-xl border px-4 py-3">Everything in one workspace</div>}
onSubmit={(value) => console.log(value)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
points | PainPoint[] | — | Pain-point cards orbiting the input (6–10 recommended); they crowd inward one by one in array order |
value | string | — | Controlled input value; when omitted the component manages it internally (uncontrolled) |
defaultValue | string | "" | Initial input value in uncontrolled mode |
onValueChange | (value: string) => void | — | Called when the input value changes (including autoPlay simulated typing) |
onSubmit | (value: string) => void | — | Called on Enter or the submit button, with the current input value |
onReset | () => void | — | Called when "Start over" sends the cards back to the outer ring |
placeholder | string | "What do you want to tackle first today?" | Input placeholder text |
label | string | "What to tackle first" | Accessible label for the input (visually hidden) |
submitLabel | string | "Submit" | Accessible label for the submit button |
resetLabel | string | "Start over" | Start-over button text |
pressureChars | number | 24 | Character count at which pressure reaches 1 (every card has crowded in) |
autoPlay | boolean | false | Simulate typing demoText character by character, submit, then loop; stops as soon as the user interacts |
demoText | string | "" | Text typed by the autoPlay simulation |
resolved | ReactNode | — | Result node that fades in at the center after submit; a default result card is shown when omitted |
accent | string | — | Color of the card's left accent strip; falls back to --wb-pain-orbit-accent (default #f43f5e) |
stiffness | number | 160 | Stiffness of the card position spring — higher values crowd in faster |
outerRadius | number | 0.42 | Outer ring radius as a fraction of the container width (height) |
innerRadius | number | 0.26 | Inner ring radius after crowding in, as a fraction of the container width (height) |
className | string | — | Passed through to the outermost container |
The Chinese-language build of this component defaults to "今天想先處理什麼?" (What do you want to tackle first today?), "想先處理的事" (What to tackle first), "送出" (Submit) and "重新開始" (Start over).
PainPoint
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique identifier, used as the React key |
text | string | — | One-line pain-point description |
icon | ReactNode | — | Icon on the left of the card; falls back to a default alert icon |
How it works
- Pressure:
pressure = min(1, characters / pressureChars), counted in characters (code points), so each CJK character counts as one. Card k (zero-based) activates whenpressure > k / N: it moves to the inner ring, tilts by a deterministic angle within ±3°, scales to 0.92 and stacks above earlier cards by activation order. - Starting angles are evenly spaced plus a small deterministic offset derived from the index (no random numbers, so SSR and the client match); orbit radii are clamped inside the container so cards are never clipped.
- Card motion is a spring (damping fixed at 18,
stiffnessadjustable); on submit the cards fly outward along their angles, staggered 40ms apart. - Tempo CSS variables:
--wb-pain-orbit-scatter(scatter duration, default400ms),--wb-pain-orbit-breathe(crowded breathing cycle, default1.5s),--wb-pain-orbit-accent(left accent strip, default#f43f5e). Override them on a wrapper or globally. - Enter-to-submit is IME-safe: pressing Enter while a CJK input method is composing does not submit, including Safari's case of firing keydown after
compositionend. autoPlaycallsonValueChangeandonSubmit; it stops immediately when the user focuses, clicks or types, and its timers pause while the component is off-screen.- Below 640px (the Tailwind
smbreakpoint) the layout switches to two stacked rows above and below the input.
Accessibility
- The input is associated with a visually hidden
label; the submit button istype="button"with anaria-labeland is disabled while the input is empty - The card cluster is
role="list"with each card as alistitem; cards are not focusable and never steal keyboard focus - The submit result is announced through an
aria-live="polite"region; after a user submit, focus moves to "Start over" and returns to the input after reset - All buttons have a
focus-visiblering - When the user's system has reduce motion enabled, cards neither move nor breathe — they only change opacity and border color with pressure — and on submit they simply fade out
Integration Connect Grid
A React component where integration cards drop into a grid in sequence, then flip one by one to a connected state with a drawn checkmark. Autoplay it as a showcase, or let users click to connect and await async authorization.
Slide Deck Shell
A 16:9 web presentation shell React component with proportional scaling, keyboard and touch navigation, four transitions, progress bar and page counter, speaker notes, URL-synced slide index and one-slide-per-page printing, plus nine built-in slide layouts.