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>

Text Scatter Assemble Animation — Letters Converge JS

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

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why transition: none is applied before the scattered position is set, and why void heading.offsetWidth is necessary between disabling and re-enabling the transition for the convergence to animate reliably every single time the button is clicked. It's also worth an accessibility conversation: ask how you would respect prefers-reduced-motion by skipping straight from a still-scattered start to the assembled end state without the intervening flight animation. For extending it, ask for a version where letters converge from a shared edge (like they are all flying in from off-screen right) instead of fully random positions, one where the convergence order follows reading order more loosely with some overlap for a busier effect, or one that scatters letters again on a timer for a continuously looping "assemble, hold, scatter, reassemble" cycle. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a headline animation where every letter starts scattered at a random position, rotation, and scale around the screen, invisible, and then converges into its correct place in the sentence with a staggered, decelerating transition, using plain HTML, CSS, and vanilla JavaScript — no animation library, using real DOM text the whole time (no canvas, no particles).

Requirements:
- On page load, split the heading's text into one inline-block span per character, preserving spaces as non-breaking-space spans in their own element so word gaps do not collapse.
- A "scatter" step that, for every letter independently, generates a fresh random horizontal offset, vertical offset, rotation angle, and scale factor each time it runs, applies them as a combined CSS transform with the element's own transition temporarily disabled (so the scattered starting position appears instantly, not animated), and sets the letter fully transparent.
- After scattering, force a synchronous layout read on the container element so the browser reliably registers the transition being disabled, then re-enable each letter's CSS transition (a slow, strongly decelerating easing curve) before triggering the convergence.
- Converge each letter back to its natural position, zero rotation, full scale, and full opacity by adding a class, staggering when each letter's class is added using a per-letter delay proportional to its index in the string, so the assembly sweeps in roughly left to right even though the starting positions were random.
- Include a button that re-runs the entire scatter-then-assemble sequence with freshly randomized starting positions every time it is clicked, and run the sequence once automatically on page load as well.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

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.