The story behind it
What do web animations usually look like? Fade in, fade out, a slight movement, some scaling.
There is nothing wrong with that. Most interfaces don't need anything more complicated. Sometimes, the best solution is not to add animation at all. It keeps the UI fast, clear, and focused on what actually matters.
But every once in a while, you want to make one small interaction memorable. Not a long intro or a heavyweight full-screen visual effect, but a short response to a user's action. Less than a second — and removing a card, restoring an element, or undoing an action suddenly feels completely different.
I ran into exactly this situation while implementing yet another "remove from favorites" interaction. The usual flow is familiar: the user clicks a bookmark, the card disappears, and the surrounding elements smoothly shift into place. You can animate opacity, you can add a little transform, but these patterns are so familiar that we barely notice them anymore.
I wanted something more interesting without making the interaction distracting. I had seen a similar animation when deleting messages in a messenger. Later, I learned that this kind of effect is often called the "Thanos snap."
I assumed there had to be an existing solution. There were a few similar implementations, but none of them quite matched what I needed. I wasn't looking for a hardcoded implementation of a single effect. I wanted something that could work with regular DOM elements, remain framework-agnostic, and let me adapt the animation to the interface around it.
So I built my own version for the project I was working on.
When I created my first open-source project, I started asking myself a question whenever I solved something interesting at work: could this be useful to other developers too?
Nobody pays me to turn these things into open-source libraries, and I still can't fully explain why I spend so much time doing it. I think I simply like the idea that code I wrote might be used by people in completely different parts of the world — from someone building their first project to an experienced developer — and that someone might notice and appreciate my work.
And perhaps I just want to leave something useful behind in the digital world.
That small work task eventually grew into another open-source library — Vanilla Disintegrate.
It animates the removal and restoration of DOM elements using particle effects. It comes with ready-to-use presets, but you don't have to tweak configuration values blindly in your code. The website includes an interactive playground where you can adjust particle movement, timing, and sound, preview the result, and copy the generated configuration.
And if the built-in particle renderer isn't enough, you can replace the rendering approach entirely.
Below, I'll explain how it works, why "remove an element, but make it look cool" turns out to involve much more than a few CSS animations, and where the real limitations of this approach are.
Your first effect in a few lines
In the simplest case, you can initialize the library like this:
import Disintegrator from 'vanilla-disintegrate/snapdom';
const effects = new Disintegrator({ preset: 'dust' });
const card = document.querySelector<HTMLElement>('.card');
document.querySelector('#remove')?.addEventListener('click', () => {
if (card) effects.remove(card);
});
After the click, the card disintegrates and the original element is removed from the DOM.
There is no special markup, no mandatory CSS file, and no component tied to a particular framework.
At first glance, it may seem like the library simply draws a nice animation over an element. But if it were that simple, a few CSS properties would have been enough.
Why not CSS?
CSS is excellent for many animation tasks. Showing a modal smoothly, highlighting a button, moving items in a list, creating a hover effect — CSS is usually exactly the right tool.
But CSS animates a DOM element as a whole. It can't take an already rendered card — including its text, image, gradients, shadows, and other visual details — split it into hundreds of tiny fragments, and give every fragment its own trajectory.
Technically, you could create hundreds of tiny <div> elements and animate them independently. But imagine what happens to performance when there are hundreds or thousands of those nodes. And even then, how do you reproduce the real text, images, shadows, and every other detail of the original element inside them?
Instead, Vanilla Disintegrate first turns the element into an image and then uses that image as the source material for the particles:
regular DOM element
↓
Canvas snapshot
↓
particles rendered with WebGL2
↓
temporary layer above the page
While the user is watching the particles, the original DOM element can already be gone from the list.
Visually, the transition remains continuous: the card is still visible in the form of its captured image, but that image is now gradually turning into dust.
The hardest part: capturing the DOM
Capturing a regular DOM element turned out to be much more interesting than I expected.
The browser is obviously very good at rendering a web page. What it doesn't give us is a stable, universal API that says:
Take this element and give me exactly the pixels the user is currently looking at.
So the visual representation of the element has to be reconstructed separately in a Canvas.
The ready-to-use integration of Vanilla Disintegrate uses SnapDOM. It takes an element, transfers its content and styles into an intermediate representation, and produces a Canvas.
That Canvas is enough to create the particle effect.
But SnapDOM isn't part of the core library, nor is it the only possible option. Vanilla Disintegrate accepts a custom capture adapter, so you can use another capture engine, provide a pre-rendered Canvas, or implement your own solution.
This matters because different projects have different requirements for performance, visual fidelity, and the types of content they need to capture.
And there is an important limitation worth stating explicitly: a DOM snapshot will not always match the original element pixel-for-pixel.
Differences can be particularly noticeable with isolated characters, very thin lines, complex filters, or unusual page scaling. For cards, images, and complete UI blocks, those differences are usually difficult to notice, but claiming perfect reproduction in every browser would be misleading.
Turning an image into particles
WebGL2 is a browser API for rendering graphics on the GPU. It's commonly associated with 3D scenes, visualizations, and games, but it never became the default way to animate regular user interfaces.
And for good reason.
If you're animating a button, a modal, or a list transition, CSS or the Web Animations API is usually a better choice. They work directly with the DOM and don't require you to manually manage textures, write shaders, or clean up GPU resources.
This effect is different.
We need to move a large number of small fragments of the same image independently. At that point, using the GPU makes much more sense than creating hundreds of DOM nodes.
This is where the complexity of WebGL2 becomes justified.
Once the Canvas snapshot is ready, Vanilla Disintegrate passes it to its built-in WebGL2 renderer. The renderer uploads the image as a texture and divides its non-transparent areas into small blocks. Each block becomes a particle.
Every particle has its own initial position, release time, direction, velocity, rotation, and opacity. That makes it possible to create very different kinds of motion: directional bursts, chaotic scattering, slow dust, vapor-like movement, or swirling particles.
The library currently includes four presets:
const dust = new Disintegrator({ preset: 'dust' });
const scatter = new Disintegrator({ preset: 'scatter' });
const vapor = new Disintegrator({ preset: 'vapor' });
const wind = new Disintegrator({ preset: 'wind' });
But choosing a preset is only the beginning.
Removal and restoration are two independent phases. For example, a card can burst to the left when removed and return from above with a slight swirl:
import Disintegrator, {
createParticleEffect,
} from 'vanilla-disintegrate/snapdom';
const effect = createParticleEffect({
remove: {
curve: 'burst',
release: 'left',
duration: 700,
},
restore: {
curve: 'float',
release: 'top',
duration: 900,
swirl: 5,
},
});
const effects = new Disintegrator({ effect });
The point of this example isn't to demonstrate every available option. There are quite a few of them, and you don't need to memorize any of them.
Removing doesn't have to mean losing
While building the library, I quickly realized that a removal animation alone wasn't enough.
Real interfaces often have an Undo action: a user removes a card from their favorites, changes their mind, and wants it back.
Of course, you could create the element again. But sometimes it's much more convenient to retain the original DOM node and later insert it wherever the application needs it:
const operation = effects.remove(card, { retain: true });
// The user clicked "Undo".
await operation.finished;
const retainedCard = effects.take(operation.removalId);
if (retainedCard) {
document.querySelector('.favorites')?.append(retainedCard);
effects.restore(retainedCard);
}
The card doesn't even have to return to its original position. It can be inserted into another list or into a layout that has changed since the removal.
The application continues to manage the DOM normally. Vanilla Disintegrate simply handles the visual transition when the element appears wherever the application has placed it.
Not just WebGL2, and not just particles
Particles are where the project started, but I didn't want to build a library capable of exactly one trick.
Every effect in Vanilla Disintegrate consists of two independent phases: remove and restore.
Each phase can use the Web Animations API, CSS, Canvas 2D, SVG, another WebGL library, or your own renderer. And if a particular effect doesn't need a Canvas snapshot at all, capture can be disabled for that phase.
This means Vanilla Disintegrate doesn't have to be about the "Thanos snap."
One part of an interface can use particles, another can use a subtle fade, and a third can use a completely custom animation. The DOM lifecycle, operation cancellation, audio, and cleanup of temporary resources remain shared across all of them.
What about performance?
That's an obvious question.
A particle effect is considerably more complex than changing opacity, so naturally it requires more resources.
There are two separate workloads involved. First, the library needs to capture the element. That's a short CPU-bound operation. After that, the particles are rendered on the GPU using WebGL2.
A page can contain any number of cards that may use the effect, but that doesn't mean all of them are being animated simultaneously.
Typically, an animation starts in response to a specific user action. One operation runs, its temporary resources are released when it finishes, and another operation may start later.
In that scenario, simply having many elements on the page doesn't create significant additional load.
Things change if you try to disintegrate several huge full-screen elements at once, especially on a high-DPI device.
That's why Vanilla Disintegrate has an auto quality mode. By default, it limits the working resolution so that a single effect can't unexpectedly consume an excessive amount of memory.
For smaller elements where maximum detail matters, you can use exact:
import { createParticleEffect } from 'vanilla-disintegrate/snapdom';
const effect = createParticleEffect({
remove: { renderQuality: 'exact' },
restore: { renderQuality: 'exact' },
});
But exact isn't a magic switch.
It preserves the full resolution of the snapshot that has already been captured. It doesn't make the DOM capture itself more accurate.
If the captured representation differs slightly from the original DOM element, increasing its resolution won't fix that difference.
Another important consideration is WebGL contexts.
Browsers aren't designed for pages to create an unlimited number of new WebGL contexts for individual cards. So once an effect is finished, temporary textures are deleted, while contexts are reused through a small shared pool.
What if WebGL2 isn't available?
WebGL2 is only required for the built-in particle effects.
If the browser or device can't create a WebGL2 context, the user's action should still work. The element will still be removed or restored, while the visual phase completes with a skipped status.
That doesn't mean the user necessarily has to see an abrupt disappearance.
Any effect that requires WebGL2 can have a fallback animation implemented with CSS, the Web Animations API, Canvas 2D, SVG, or a custom renderer. The fallback runs if WebGL2 isn't available or if the built-in particle renderer fails to start.
So a modern device can get the full particle effect, while other environments fall back to a simple, graceful transition.
Custom effects implemented with CSS, Canvas 2D, or the Web Animations API don't depend on WebGL2 at all.
Vanilla Disintegrate also respects prefers-reduced-motion, so unnecessary animation is skipped when the user has enabled reduced motion at the operating-system level.
Vanilla Disintegrate targets modern browsers. I've documented the complete browser support matrix, capture limitations, and Safari-specific behavior in the documentation rather than turning this article into a compatibility reference.
When does an effect like this actually make sense?
I don't think every close button in an interface should suddenly produce a cloud of particles.
Animation for its own sake gets old quickly and eventually becomes distracting.
But there are interactions where a stronger visual response can work well: removing an important card, clearing a list, undoing an action, completing a task, or moving something to the trash.
In these situations, a short visual response can make the result of an action easier to notice — and simply make the interface feel a little more alive.
That's what I built Vanilla Disintegrate for.
It isn't intended to replace regular CSS animations. It's for those moments when you want one distinctive but controlled effect, without tying your UI to a framework or building an entire WebGL engine from scratch.
Final thoughts
Vanilla Disintegrate is released under the MIT license and is free to use in any project.
I'd be glad to see it become useful to other developers — whether that means a star on GitHub, an issue describing a real-world use case, an idea for a new effect, or simply a link to something you've built with it.
- Documentation and playground: disintegrate.uvarov.tech
- GitHub: uvarov-frontend/vanilla-disintegrate
- See the effect in a real project: Vanilla Calendar Pro
Top comments (0)