Keyword Converge
A React text animation where a scattered keyword cloud drifts toward the center, merges and resolves into a single concluding sentence — for distilling scattered ideas into one message.
Distill a pile of scattered ideas into one core message. Keywords start spread across the block on a golden-angle spiral, each floating on its own; once triggered, springs pull them toward the center from farthest to nearest, shrinking, blurring and fading on the way. As they near the center a glow flares, then the conclusion surfaces character by character out of a blur, with words marked by {{ }} shown in the accent color. It can play when scrolled into view, on click, or fully controlled, and it can loop.
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 keyword-converge.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<KeywordConverge/>
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/keyword-converge.en.jsonOr, once registries are configured in components.json, install it as @webberui/keyword-converge.
Usage
import { KeywordConverge } from "@/components/ui/keyword-converge";
<KeywordConverge
keywords={[
{ text: "客訴變多", weight: 3 },
{ text: "回覆太慢", weight: 3 },
{ text: "資料分散", weight: 2 },
"人力不足",
"重複輸入",
"找不到紀錄",
]}
conclusion="問題不在人,在於{{資訊沒有集中}}"
trigger="inView"
delay={800}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
keywords | (KeywordConvergeItem | string)[] | — | Keywords, as strings or objects (8–20 recommended) |
conclusion | string | — | Conclusion revealed after converging; words wrapped in {{ }} use the accent color |
phase | "scatter" | "converge" | "resolved" | — | Controlled current phase; managed internally when omitted |
defaultPhase | "scatter" | "converge" | "resolved" | "scatter" | Initial phase in uncontrolled mode |
onPhaseChange | (phase: KeywordConvergePhase) => void | — | Called when the phase changes |
trigger | "inView" | "click" | "manual" | "inView" | Converge after a delay once in view, on container click, or only via phase |
delay | number | 800 | With trigger="inView", delay from entering the viewport to converging (ms) |
loop | boolean | false | Return to scatter after the conclusion holds for holdDuration; with inView it converges again |
holdDuration | number | 3000 | How long the conclusion holds when looping (ms) |
glow | boolean | true | Flare a glow as the keywords near the center |
stiffness | number | 90 | Flight spring stiffness; higher is snappier |
damping | number | 16 | Flight spring damping; higher bounces less |
as | "p" | "h2" | "h3" | "h4" | "div" | "span" | "p" | Tag for the conclusion |
convergeHint | string | "點擊收斂" (Click to converge) | Hint shown in the scatter state with trigger="click" |
resetHint | string | "點擊重新散開" (Click to scatter again) | Hint shown in the resolved state with trigger="click" |
className | string | — | Passed to the outermost container; defaults to min-h-[320px], override the height here |
KeywordConvergeItem
| Field | Type | Default | Description |
|---|---|---|---|
id | string | index + text | Stable id |
text | string | — | Keyword text |
weight | 1 | 2 | 3 | 2 | Visual weight: sets the font size (three steps) and opacity (0.5 / 0.75 / 1) |
CSS variables
| Variable | Default | Description |
|---|---|---|
--wb-keyword-converge-stagger | 40ms | Departure interval between keywords (farthest first) |
--wb-keyword-converge-accent | #3b82f6 | Color of the glow and the {{ }} highlighted words |
--wb-keyword-converge-glow | 600ms | Total time for the glow to expand and retract |
How it works
- Scatter positions are deterministic: a golden-angle spiral plus a small per-index hash offset, with no
Math.random, so server and browser render the same layout and it does not reshuffle on reload; the center is kept free for the conclusion. - Keywords converge in order of distance from the center, farthest first, so they arrive almost together; when scattering back the nearest leave first.
- The conclusion is split with
Array.from, so each CJK character surfaces on its own (25ms apart), while English is grouped by word and never breaks mid-word. --wb-keyword-converge-staggerand--wb-keyword-converge-gloware read once on mount to schedule the glow and the conclusion.- When looping with
trigger="inView", it waits about 1.2s longer after returning to scatter before converging again, so the scattered keywords settle first.
Accessibility
- The keyword layer and the per-character animation are
aria-hidden; asr-onlyline reads "Keywords: A, B, C. Conclusion: …" so screen readers get the full content at once - The conclusion container is
aria-live="polite"and only gets text on entering resolved, so nothing is announced while scattering or converging - With
trigger="click", atype="button"covers the whole block: it is focusable with Tab, activates with Enter or Space, has afocus-visiblering and a visible text hint, and is disabled while converging - When the system "reduce motion" setting is on, keywords do not float or fly: they fade out in place and the whole conclusion fades straight in, with no glow or per-character blur
Liquid Reveal
Refracts children through an SVG displacement filter like a liquid droplet, then bursts the surface to reveal them crisply on click or in-view.
Overprint Register
A heading rendered as several overprinted colour plates that slide in from misalignment into exact register when they enter the viewport, then drift slightly out of register again on hover.