Marketing Showcase Blocks

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.en

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.

12
160
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
pointsPainPoint[]—Pain-point cards orbiting the input (6–10 recommended); they crowd inward one by one in array order
valuestring—Controlled input value; when omitted the component manages it internally (uncontrolled)
defaultValuestring""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
placeholderstring"What do you want to tackle first today?"Input placeholder text
labelstring"What to tackle first"Accessible label for the input (visually hidden)
submitLabelstring"Submit"Accessible label for the submit button
resetLabelstring"Start over"Start-over button text
pressureCharsnumber24Character count at which pressure reaches 1 (every card has crowded in)
autoPlaybooleanfalseSimulate typing demoText character by character, submit, then loop; stops as soon as the user interacts
demoTextstring""Text typed by the autoPlay simulation
resolvedReactNode—Result node that fades in at the center after submit; a default result card is shown when omitted
accentstring—Color of the card's left accent strip; falls back to --wb-pain-orbit-accent (default #f43f5e)
stiffnessnumber160Stiffness of the card position spring — higher values crowd in faster
outerRadiusnumber0.42Outer ring radius as a fraction of the container width (height)
innerRadiusnumber0.26Inner ring radius after crowding in, as a fraction of the container width (height)
classNamestring—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

FieldTypeDefaultDescription
idstring—Unique identifier, used as the React key
textstring—One-line pain-point description
iconReactNode—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 when pressure > 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, stiffness adjustable); on submit the cards fly outward along their angles, staggered 40ms apart.
  • Tempo CSS variables: --wb-pain-orbit-scatter (scatter duration, default 400ms), --wb-pain-orbit-breathe (crowded breathing cycle, default 1.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.
  • autoPlay calls onValueChange and onSubmit; it stops immediately when the user focuses, clicks or types, and its timers pause while the component is off-screen.
  • Below 640px (the Tailwind sm breakpoint) 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 is type="button" with an aria-label and is disabled while the input is empty
  • The card cluster is role="list" with each card as a listitem; 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-visible ring
  • 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