Reaction Burst
Emoji reaction bar for live streaming: per-particle cubic Bézier path, lateral sine wobble, recycled DOM pool (max 40), and a simulated viewer feed — no canvas, native prefers-reduced-motion support.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Atmosphere has 57 effects, including 10 free. Explore the category →
3 usage examples



How it works
The effect creates one RBXScene instance per .rbx-scene element found in the DOM. Each instance manages a pool of reusable <span> elements (max 40 active at once): when a span completes its trajectory, it is pushed back into the pool rather than removed — zero element creation after the first wave.
On each click or Enter/Space keypress, react() computes the start position from the stage's bottom edge (client → local), then calls _launch() for each particle in the burst. The trajectory is a 1D cubic Bézier (bz(t, p0, p1, p2, p3)) applied independently to x and y — the four control points are randomized per launch. A sine wobble (Math.sin(t × π × wFreq) × wAmp) is added to x for lateral oscillation. Opacity stays flat from 0 to 0.55 then fades; scale grows from 0→0.12, plateaus, then decreases slightly toward the end.
A simulated live feed runs in parallel via a self-rescheduling setTimeout (random interval 600–2 600 ms by default). It picks a random emoji, increments its counter, and launches a particle from the right third of the stage to mimic other viewers reacting. Counters are formatted by fmt(n): integers below 1 000, one decimal between 1 000 and 9 999 (1.2k), integer above 10 000 (12k); aria-live="polite" elements relay updates to screen readers.
The rAF loop iterates _active in reverse and removes particles that reach t = 1. If the pool is full when a new particle launches, the oldest entry is evicted (_active.shift()) — a hard cap of 40. The public API window.RBX_0839.scenes[i] exposes setBurst(n), setLiveSpeed(preset), fireAll(), and resetCounts() to control each scene independently.
Accessibility
- prefers-reduced-motion: detected at startup via
matchMedia. In reduced mode, CSS disables therbx-bumpingscale animation and the live-dot pulse; JS fires only 1 particle per click (instead of 3), duration 420 ms, in a straight line (linear interpolation, no Bézier). The live feed keeps incrementing counters but launches no particles. - Each
.rbx-btnis a native<button>with a descriptivearia-label("Like", "Heart"…). JS bindskeydownfor Enter and Space — the effect is fully operable by keyboard. .rbx-btn-countand.rbx-total-countcarryaria-live="polite": screen readers announce each counter update without interrupting ongoing reading.- Flying particles (
.rbx-particle) carryaria-hidden="true"and are excluded from the tab order — they do not interfere with keyboard navigation or screen readers. - The focused button receives a visible
:focus-visibleorange ring (outline-color: #f59e0b) on dark backgrounds (contrast ratio ≥ 3:1 against#0a0a0f). The button group is annotatedrole="group" aria-label="Reactions".
Browser compatibility
Requires modern DOM, requestAnimationFrame, and classList — no canvas, no external dependencies.
With JS disabled, the reaction bar remains visible as plain HTML with its static initial counters — no errors, the page stays functional.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="rbx-scene" role="region" aria-label="Live reaction bar — click an emoji to send a reaction">
<!-- Particle flight zone + visual background + LIVE badge -->
<div class="rbx-stage">
<div class="rbx-video-bg" aria-hidden="true"></div>
<div class="rbx-live-badge" aria-hidden="true">
<span class="rbx-live-dot"></span>
<span class="rbx-live-text">LIVE</span>
<span class="rbx-viewer-count">1,284 viewers</span>
</div>
<!-- Free slot: thumbnail, title, player… -->
<div class="rbx-video-meta" aria-hidden="true">
<div class="rbx-video-icon">…</div>
<span class="rbx-video-title">Live title</span>
</div>
</div>
<!-- Reaction bar -->
<div class="rbx-bar" role="group" aria-label="Reactions">
<button class="rbx-btn" data-initial="847" aria-label="Like">
<span class="rbx-btn-emoji" aria-hidden="true">👍</span>
<span class="rbx-btn-count" aria-live="polite">847</span>
</button>
<!-- repeat for each emoji -->
<div class="rbx-total" aria-live="polite" aria-label="Total reactions">
<span class="rbx-total-label">Total</span>
<span class="rbx-total-count">3.1k</span>
</div>
</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| data-initial="n" | varies per button | Starting counter value for each .rbx-btn. Set it to display realistic social proof from page load. |
| RBXScene.setBurst(n) | 3 | Number of particles fired per user click. 1 = subtle, 3 = default, 5–6 = festive. Apply per scene via window.RBX_0839.scenes[i].setBurst(n). |
| RBXScene.setLiveSpeed(preset) | 2 (600–2 600 ms) | Simulated feed cadence: 1 = slow (1 800–4 000 ms), 2 = normal, 3 = fast (200–800 ms). Tune to match the desired perceived pace. |
| RBXScene.fireAll() | — | Fires one reaction from each of the 5 emojis in a cascade (90 ms apart). Best on business events: payment confirmed, sign-up validated, milestone reached. |
| RBXScene.resetCounts() | — | Resets all counters to their data-initial value. Useful to reinitialize the demo between tests or sessions. |
| MAX_P (JS constant, line 6) | 40 | Hard cap on the active particle pool. Drop to 20 for low-end devices, raise to 60 for denser effects — beyond the cap the oldest particle is evicted. |
| .rbx-scene.rbx-hc | absent | CSS class that switches the bar and buttons to high-contrast mode (background #111118, borders at 40% opacity, white text). Toggle via JS based on detected accessibility needs. |
| Flight duration (JS, _launch function) | 1 100–1 800 ms | Constant 1100 + Math.random() * 700 inside _launch(). Lower to 600 + Math.random() * 300 for snappy reactions, raise for a more contemplative float. |
FAQ
<span> inside the .rbx-stage. The transform and opacity properties are updated every frame via requestAnimationFrame. This simplifies integration in any framework (React, Vue, Svelte) and makes particles inherently accessible via aria-hidden._startLive() via a setTimeout. To replace it, simply call scene.react(emojiIndex, clientX, clientY) from your WebSocket listener — emojiIndex runs 0 (👍) to 4 (👏). The pool and the 40-particle cap apply identically.EMOJIS constant at the top of the IIFE (['👍','❤️','🎉','🔥','👏']). It must stay in sync with the .rbx-btn elements in the HTML (same order, same length): button at index i maps to EMOJIS[i]. Edit both together to swap the emoji set.