I built a Chrome extension called Site Bomb. It does exactly one thing: you drop little bombs anywhere on a webpage, and the text near the blast gets physically blown apart, letter by letter.
It is completely useless. It is also genuinely satisfying when you're stressed.
Here's how it works, and the three things I had the most fun figuring out.
1. Turning page text into something you can blow up
You can't apply physics to a plain text node — a paragraph is one indivisible blob as far as the DOM is concerned. So before anything explodes, Site Bomb walks the page and rewrites every visible text node, wrapping each individual character in its own element:
string.split("").map((char) =>
/\s/.test(char)
? " "
: `<particle style="display:inline-block;">${char}</particle>`
);
Now every letter is its own inline-block box that can be moved, rotated, and flung around independently — while the page still reads normally until a bomb goes off. Whitespace is preserved so the layout doesn't collapse.
2. The blast: a little bit of fake physics
When a bomb explodes, each particle (letter) looks at its distance from the blast center and gets pushed away with a force that falls off with distance — roughly an inverse-square feel, capped so close letters don't fly off to infinity:
const distX = this.x - blast.x;
const distY = this.y - blast.y;
const distance = distX * distX + distY * distY;
let force = 150000 / distance; // closer = stronger
if (force > 40) force = 40; // clamp so it doesn't explode *too* hard
const rad = Math.asin((distY * distY) / distance);
this.velocityX = Math.cos(rad) * force * (distX < 0 ? -1 : 1);
this.velocityY = Math.sin(rad) * force * (distY < 0 ? -1 : 1);
Then every frame each letter drifts by its velocity, slowly bleeding off speed (a crude friction) and rotating as it goes, all driven through a CSS transform inside a requestAnimationFrame loop. It's not real physics — it just has to feel explosive, and a distance-based force plus some decay gets you there surprisingly cheaply.
3. The bug that annoyed me for years: clicks getting eaten
The most satisfying fix had nothing to do with explosions.
Originally the click handler lived on document.body.onclick. That relies on the click event bubbling up to <body>. The problem: tons of sites call event.stopPropagation() on their own click handlers, so the event dies before it ever reaches body. The symptom was maddening — on some sites bombs only dropped intermittently, and you'd click five times and then all five would go off at once.
The fix was to stop relying on bubbling and listen on the way down instead,
using the capture phase:
document.addEventListener(
"click",
(event) => dropBomb(event),
true // useCapture — fires before any element can stopPropagation()
);
Capture-phase listeners run as the event travels from the root down to the target, before the bubbling phase where stopPropagation() usually lives. One true and clicks now register instantly, everywhere.
Keeping it well-behaved
Actually I released this a few years ago but then one day it got pulled from the Chrome Web Store because it didn't follow Chrome extension best practice.
So, whilst updating Manifest from V2 to V3 this time, I also made corrections to the following minor details.
A couple of small things that matter for an extension that injects into any site:
-
Minimal permissions. It only needs
activeTab— temporary access to the current tab, granted when you click the toolbar icon. No "read all your data on every website" scare. (It's also Manifest V3, with the script injected viachrome.scripting.) -
One click to toggle. Clicking the icon turns bomb mode on for that tab
(with an
ONbadge), and clicking again turns it off — state is tracked per tab. -
Don't break the page. A re-injection guard stops it from running twice,
and elements like
<script>,<input>, and<video>are skipped so the page stays usable.
Try it
If you ever want to blow up a webpage:
🔗 https://chromewebstore.google.com/detail/site-bomb/hnnabnffilimfgdcinlijkjkdemdonea?authuser=0&hl=ja
Top comments (8)
Wrapping every character on the whole page up front is a wild amount of DOM, easily tens of thousands of nodes on a long article. Nobody's benchmarking a joke tool, fair enough, but if you ever wanted it to feel instant on huge pages, only wrapping the text near where the bomb lands (instead of the whole document on inject) would cut most of that cost. Either way, blowing up a webpage letter by letter is a genuinely great way to burn off a stressful afternoon.
Yeah, wrapping the entire document up front is the lazy approach for sure. Doing it around the blast would be way lighter. Never felt slow enough to bother me but you're right it wouldn't hold up on a huge page. Good idea!
The capture-phase fix deserves more attention than a joke bomb extension is going to get it. Almost everyone learns the event model as "clicks bubble up" and then spends years fighting stopPropagation() without realizing there's a whole phase running the other direction, before any element gets to cancel anything. addEventListener(..., true) is one of those one-character fixes that feels like cheating.
The nice side effect for something that injects into arbitrary sites: capture puts you first in line, so you still see the click on pages that are actively hostile to it. The flip side you clearly already handled — being first means you can also break the page if you're careless, which is why skipping inputs/video and not calling preventDefault is what keeps it from being annoying.
Also "completely useless but satisfying when stressed" is a perfectly good reason to build something. Half my best debugging started as a toy.
Thanks, this is exactly the part I hoped someone would pick up on. The "there's a whole phase running the other direction" framing is perfect — that's precisely the moment it clicked for me too. And you nailed the trade-off: being first in line is the whole point on hostile pages, but it's also why I'm careful never to call preventDefault() and to skip inputs/video — capture gives you the power to quietly break every page if you're not disciplined about it. "Half my best debugging started as a toy" — couldn't agree more. 🧨
This sounds like a fun project! Did you consider
Thanks! Looks like your comment might have gotten cut off.. Curious what you had in mind, happy to dig in.
@toffy One thing worth adding for anyone reading this later: capture doesn't fully save you on pages that call stopImmediatePropagation() in their own capture listener registered before yours. Load order matters, and content scripts don't always win that race. In practice I've found the pages hostile enough to do that are rare, but it's the failure mode I'd check first if someone reports the extension "just not firing" on a specific site.
And yeah — the toy framing is doing real work here. Skipping inputs and never calling preventDefault aren't polish, they're the difference between a fun gag and an extension that silently eats form submissions and gets one-star reviewed into oblivion. Good instincts baking that in from the start.
Yeah, good shout. stopImmediatePropagation in an earlier capture listener is the one case where being first still isn't enough, and the registration order race makes it worse. document_start injection is probably the only real fallback, though I'd rather not go there.
And the one-star point is a good way to put it. Skipping inputs felt obvious at the time but you're right that it's the whole ballgame.