Text Animations

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

800
90
Active edited usageDefault props
<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.json

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

PropTypeDefaultDescription
keywords(KeywordConvergeItem | string)[]—Keywords, as strings or objects (8–20 recommended)
conclusionstring—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
delaynumber800With trigger="inView", delay from entering the viewport to converging (ms)
loopbooleanfalseReturn to scatter after the conclusion holds for holdDuration; with inView it converges again
holdDurationnumber3000How long the conclusion holds when looping (ms)
glowbooleantrueFlare a glow as the keywords near the center
stiffnessnumber90Flight spring stiffness; higher is snappier
dampingnumber16Flight spring damping; higher bounces less
as"p" | "h2" | "h3" | "h4" | "div" | "span""p"Tag for the conclusion
convergeHintstring"點擊收斂" (Click to converge)Hint shown in the scatter state with trigger="click"
resetHintstring"點擊重新散開" (Click to scatter again)Hint shown in the resolved state with trigger="click"
classNamestring—Passed to the outermost container; defaults to min-h-[320px], override the height here

KeywordConvergeItem

FieldTypeDefaultDescription
idstringindex + textStable id
textstring—Keyword text
weight1 | 2 | 32Visual weight: sets the font size (three steps) and opacity (0.5 / 0.75 / 1)

CSS variables

VariableDefaultDescription
--wb-keyword-converge-stagger40msDeparture interval between keywords (farthest first)
--wb-keyword-converge-accent#3b82f6Color of the glow and the {{ }} highlighted words
--wb-keyword-converge-glow600msTotal 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-stagger and --wb-keyword-converge-glow are 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; a sr-only line 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", a type="button" covers the whole block: it is focusable with Tab, activates with Enter or Space, has a focus-visible ring 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