webdevpuneet.com

Text Scatter Assemble Animation — Letters Converge JS

Text Scatter Assemble · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<div class="scene">
  <h1 class="assemble-heading" id="heading">Pieces find their place</h1>
  <button class="replay" onclick="assemble()">Scatter and reassemble</button>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real DOM letters throughout — selectable, accessible text, not canvas particles
Each letter gets independent random offset, rotation, and scale on every scatter
transition: none applies the scattered starting state instantly with no visible jump
Forced reflow (void heading.offsetWidth) ensures the transition reliably re-enables
Staggered per-letter setTimeout convergence, decelerating cubic-bezier ease-out curve
Every replay produces a genuinely different scattered layout via fresh Math.random() calls
Splits any headline into per-letter spans automatically, with space handling
Zero dependencies — pure CSS transitions triggered by class and inline style toggling
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons

About this UI Snippet

Text Scatter Assemble — Random Offsets, Rotation & Staggered Convergence

Screenshot of the Text Scatter Assemble snippet rendered live

A text scatter assemble starts every letter of a headline flung to a random position around the screen — offset, rotated, scaled down, and invisible — then converges each one back to its correct spot in the sentence with a staggered, eased transition, so the whole headline looks like it is physically reassembling itself from scattered pieces. Unlike particle-based text formation effects that render text as thousands of tiny dots on a <canvas>, this uses the real DOM letters themselves — each character is still a genuine, selectable, accessible piece of text throughout the animation, just transformed in space.

Splitting into letters

buildLetters(el) wraps each character of the heading in its own <span class="letter">, converting literal spaces into non-breaking-space spans so word gaps hold their width once the spans become display: inline-block. This is the same splitting approach used by 3D character flip reveal and letter gravity drop, each applying a different physical motion to the resulting per-letter spans.

Scattering to random positions

scatter(letters) loops over every letter and computes three independent random values: dx/dy — a random horizontal and vertical offset up to ±350px and ±250px respectively ((Math.random() - 0.5) * 700 centers the random range on zero) — and rot, a random rotation up to ±270 degrees, plus a random scale between 0.3 and 0.9. Setting l.style.transition = 'none' first means this scattered starting state is applied instantly, with no visible animation, before the reassembly begins.

Converging back with a staggered ease-out

After scattering, assemble() forces a layout flush with void heading.offsetWidth (necessary so the browser registers the transition: none scattered state before a transition is re-enabled — the same forced-reflow technique used in word spring bounce heading), clears each letter's inline transition back to the CSS-declared 0.85s cubic-bezier(0.16, 1, 0.3, 1), and then, staggered by 35ms per letter index via setTimeout, adds a .placed class to each one. .placed resets opacity, transform to translate(0, 0) rotate(0deg) scale(1) — the letter's true position in the sentence — and the long, decelerating cubic-bezier(0.16, 1, 0.3, 1) curve (a common "ease-out-expo"-style curve) makes each letter's flight feel like it is gliding to a gentle stop rather than snapping into place.

Why random values are generated fresh each call

Because scatter() calls Math.random() independently for every letter on every invocation, no two runs of the "Scatter and reassemble" button produce the same scattered starting layout — every replay looks like a genuinely different, unpredictable dispersal that then converges to the identical final sentence.

Step by step

How to Use

  1. 1
    Watch it load, then replayLetters scatter and fly into place on page load. Click "Scatter and reassemble" to watch a fresh, differently-randomized scatter converge again.
  2. 2
    Change the headline textEdit the text inside #heading in the HTML panel — buildLetters() re-splits it into per-letter spans on load.
  3. 3
    Adjust the scatter radiusIn the JS panel, change the 700 and 500 multipliers on dx and dy inside scatter() for a wider or tighter starting dispersal.
  4. 4
    Adjust the rotation rangeChange the 540 multiplier on rot inside scatter() — larger values produce more dramatic tumbling before letters settle.
  5. 5
    Change the convergence speedIn the CSS panel, update 0.85s on the .letter transition, or the 35 multiplier in the setTimeout stagger inside assemble() in the JS panel.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Hero headline entrance with a sense of construction
The scatter-to-assemble motion reads as a headline being physically built or discovered, well suited to creative, design, and construction-adjacent product landing pages.
Loading and page-ready reveal moments
Trigger the assembly once content is ready to signal completion — pieces converging into a finished sentence works as a natural "everything is loaded" cue.
Learn staggered transitions with forced reflow
Edit the scatter ranges and stagger interval in the JS panel to see exactly how the transition: none plus forced-reflow pattern makes a replayable random-to-ordered animation reliable.
Case study and portfolio project title reveals
Use as a section-opening flourish for a portfolio case study title, giving each project page a distinct, memorable entrance moment.
Pair with a scroll-triggered observer
Combine with the IntersectionObserver pattern from 3D character flip reveal so assemble() fires automatically the first time the heading scrolls into view.
Puzzle, game, and creative-tool branding
The literal "pieces coming together" metaphor fits puzzle games, collaborative design tools, and any brand built around assembly or creative construction.

Got questions?

Frequently Asked Questions

No — every letter remains a real HTML span throughout the animation, just transformed with CSS translate, rotate, and scale. This keeps the text genuinely selectable and accessible, unlike canvas-based particle text effects that render pixels rather than characters.

Scattering is meant to be the instantaneous starting state, not an animated motion in itself — only the convergence back to the correct position should be visibly animated. Setting transition: none before applying the random scattered transform makes that jump invisible, so the only motion the viewer sees is letters flying into place.

Removing a style property (or setting transition: none) and then immediately changing it back in the same synchronous JS tick does not reliably force the browser to register the intermediate state. Reading a layout property like offsetWidth forces a synchronous style recalculation in between, guaranteeing the later transition change is actually observed and animates correctly.

scatter() calls Math.random() fresh for every letter's dx, dy, rotation, and scale on every single call to assemble(). Because these values are never cached or seeded, no two scatter states are ever identical, even though the final assembled sentence is always the same.

Change the 0.85s duration in the .letter transition rule in the CSS panel for the overall speed of each individual letter's flight, and change the 35ms multiplier in the setTimeout stagger inside assemble() in the JS panel for how quickly letters begin converging one after another.

Yes. Click "JSX" for a React component. Store each letter's scattered transform values in component state generated with Math.random() on trigger, apply them as inline styles, and toggle a "placed" boolean per letter (or all at once with staggered setTimeout calls) to animate the transition back to the identity transform.