webdevpuneet.com

Event Bubbling Visualizer — Free HTML CSS JS Snippet

Event Bubbling Visualizer · Visualizers · Plain HTML, CSS & JS · Live preview

CategoryVisualizers

Copy & paste

Source Code

<div class="wrap">
  <div class="stage-panel">
    <div class="boxes" id="boxes">
      <div class="box outer" id="box-outer">
        <span class="box-label">Outer</span>
        <div class="phase-toggles">
          <label><input type="checkbox" id="outer-capture" /> capture</label>
          <label><input type="checkbox" id="outer-bubble" checked /> bubble</label>
        </div>
        <div class="box middle" id="box-middle">
          <span class="box-label">Middle</span>
          <div class="phase-toggles">
            <label><input type="checkbox" id="middle-capture" /> capture</label>
            <label><input type="checkbox" id="middle-bubble" checked /> bubble</label>
            <label class="stop-label"><input type="checkbox" id="middle-stop" /> stopPropagation()</label>
          </div>
          <div class="box inner" id="box-inner">
            <span class="box-label">Inner (click me)</span>
            <div class="phase-toggles">
              <label><input type="checkbox" id="inner-capture" /> capture</label>
              <label><input type="checkbox" id="inner-bubble" checked /> bubble</label>
            </div>
          </div>
        </div>
      </div>
      <svg id="pulse-svg"></svg>
    </div>
  </div>
  <div class="log-panel">
    <div class="log-title">Event Log</div>
    <div class="log-list" id="log-list"></div>
    <button class="clear-btn" id="clear-log">Clear log</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), 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

Genuine addEventListener capture (true) and bubble (false) listeners on three real nested elements, not a scripted order
Per-box, per-phase checkboxes (6 total) let you enable or disable any handler independently before clicking
Two sequential SVG pulse animations (dim capture, bright bubble) computed from live getBoundingClientRect() positions
requestAnimationFrame-driven pulse movement with cubic ease-out, not fixed CSS keyframes, since target points are measured at click time
Live event log records the real order handlers fired in, styled distinctly for capture, bubble, and stopped entries
stopPropagation() toggle on the Middle box demonstrably halts both the log and the bubble pulse at that exact point
Clear log button resets the panel without needing to reload or re-click through a fresh scenario
Directly demonstrates the DOM Level 3 event model: capture-phase completes fully before the bubble phase begins

About this UI Snippet

Event Bubbling Visualizer — Animated Capture & Bubble Phase Demo with stopPropagation() in Vanilla JS

Screenshot of the Event Bubbling Visualizer snippet rendered live

The DOM event model has three phases — capture, target, and bubble — and almost every explanation of it draws a static arrow diagram instead of showing an actual click event traveling through actual nested elements. This snippet fixes that: three real nested boxes (Outer, Middle, Inner) each carry genuine addEventListener handlers registered in both phases, a live log prints exactly which handler fires and in what order, and an animated pulse physically travels down through the tree during capture and back up during bubble — using the browser's real event dispatch, not a scripted approximation of it.

Real capture and bubble listeners, not a simulated order

Each box registers two listeners on the exact same element: addEventListener('click', handler, true) for the capture phase and addEventListener('click', handler, false) (the default) for the bubble phase. When Inner is clicked, the browser's actual dispatch algorithm runs: it walks from document down to inner calling every registered capture-phase listener along the way (outer capture, then middle capture, then inner capture), then calls the target's own bubble-phase listener, then walks back up calling every bubble-phase listener from inner to middle to outer. The log entries this snippet prints are not scripted to look like this order — they are the direct, real-time record of the callbacks the browser itself invoked, which is why toggling any of the six checkboxes (capture/bubble per box) genuinely changes which lines appear, because it genuinely changes which listeners exist.

Two animated pulses on the same physical positions

runVisualPulse() computes the screen-space center of each box with getBoundingClientRect() and animates a small SVG <circle> through those points using requestAnimationFrame with a cubic ease-out (1 - Math.pow(1 - p, 3)), not CSS keyframes, because the destination points depend on live layout measurements taken at click time rather than fixed values that could be hardcoded into a @keyframes rule. The first pulse (grey, thinner, radius 4) runs Inner → Middle → Outer to represent the capture phase's top-down path rendered in reverse for legibility, immediately followed by a second pulse (indigo, thicker, radius 6) that runs the same Inner → Middle → Outer path representing the bubble phase — the phase most real-world event listeners actually use, which is why it is drawn heavier and brighter.

Why capture is drawn dim and bubble is drawn bright

This is a deliberate legibility choice, not an accident: capture-phase listeners are comparatively rare in real code (most handlers use the default bubble phase), so demoting the capture pulse to a thin grey line keeps visual weight on the phase developers actually rely on day to day, while still making the true dispatch order — capture always completes fully before bubble begins — visible as two distinct, sequential animations rather than one ambiguous blur.

The stopPropagation() toggle and where it actually breaks the chain

Checking "stopPropagation()" on the Middle box's bubble-phase checkbox flips a flag that is read inside Middle's own bubble handler: if (name === 'middle' && stopChecked) { e.stopPropagation(); }. Calling stopPropagation() from inside a handler stops the event from continuing to any further listeners in the remaining phase — since this call happens during Middle's *bubble* handler, it prevents Outer's bubble listener from ever firing, but it does not retroactively undo the capture phase, which already ran to completion before bubbling even started. runVisualPulse() mirrors this precisely: when the stop toggle and Middle's bubble checkbox are both on, the second (bubble) pulse animation is only given two points, [innerPt, middlePt], so it visibly halts at Middle instead of continuing to Outer — and the log shows the real consequence, with no "OUTER — bubble phase handler fired" line ever appearing.

Independent per-box, per-phase toggles built for direct experimentation

Rather than one global on/off switch, each box exposes its own capture and bubble checkboxes (six total), checked via document.getElementById(name + '-capture').checked and ...'-bubble'.checked at the top of every handler before it logs anything or runs its side effects. This lets you construct any combination — for example, disabling Inner's own bubble listener while keeping Middle's and Outer's — and watch the log and pulse respond exactly as the real DOM event model dictates for that specific configuration, rather than only ever observing the one "all phases enabled" case most diagrams show.

Step by step

How to Use

  1. 1
    Click the Inner boxA dim grey pulse travels Outer to Middle to Inner during the capture phase (drawn first), followed by a brighter indigo pulse traveling Inner to Middle to Outer during the bubble phase.
  2. 2
    Read the event log on the rightEach line records a real handler firing, in the exact order the browser invoked it — capture-phase entries appear grey and thin, bubble-phase entries appear indigo and bold.
  3. 3
    Uncheck a box's "bubble" checkboxThat box's bubble-phase listener is removed entirely. Click Inner again and confirm no log entry appears for that box during the bubble pass, while capture (if enabled) still fires normally.
  4. 4
    Check a box's "capture" checkboxThat box's capture-phase listener is added. Click Inner again and watch a new grey log entry appear before any bubble entries, since capture always completes before bubbling begins.
  5. 5
    Enable stopPropagation() on the Middle boxClick Inner again. The bright bubble pulse now visibly stops at Middle instead of continuing to Outer, and the log confirms Outer's bubble handler never fires.
  6. 6
    Toggle stopPropagation() off and compare the two log sequencesWith it off, all six potential handlers can fire in full capture-then-bubble order; with it on, exactly one bubble-phase handler (Outer's) is missing from the second run — the clearest possible before/after contrast.

Real-world uses

Common Use Cases

Teaching the DOM event model in a JavaScript course
Most learners memorize "bubbling goes up" without ever seeing capture happen first — toggling the six phase checkboxes here makes the full three-phase model (capture, target, bubble) concrete instead of a diagram to memorize.
Debugging an event handler that fires unexpectedly or not at all
Recreate your component's nesting and listener phases here to quickly test whether a stopPropagation() call somewhere in the tree, or a capture-phase listener you forgot about, is the reason an outer handler isn't firing.
Explaining event delegation to a team
Event delegation (attaching one listener to a parent instead of many to children) relies entirely on bubbling — use this visualizer to justify the pattern before introducing it, pairing with the modal snippet's own click-outside-to-close logic as a real delegation example.
Interview prep for JavaScript event handling questions
Capture vs bubble vs stopPropagation vs stopImmediatePropagation are extremely common interview topics — use the toggles here to rehearse explaining the difference out loud with a working example instead of just the terminology.
Blog post or documentation embed on event propagation
Embed directly inside an article explaining addEventListener's third argument; readers can flip the exact toggle your prose is describing and watch the log confirm it, rather than trusting a static code sample.
Related: Liquid Swipe Page Transition
See the Liquid Swipe Page Transition for a related animations pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Yes, with one important adjustment: React's synthetic event system attaches a single delegated listener at the root and simulates bubbling through its own virtual tree, so onClickCapture and onClick props on JSX elements map to this snippet's capture and bubble listeners respectively, and calling e.stopPropagation() inside a React handler behaves the same way described here. Put the pulse animation loop (the requestAnimationFrame-based animateTo function) inside a useEffect or a plain event handler, and since it resolves via its own Promise chain rather than a persistent interval, there is no dangling timer to clear on unmount — just guard against updating state after unmount if the component could disappear mid-animation. Vue's @click.capture modifier and Angular's (click) with a manually attached capture listener follow the same conceptual mapping.

Because that mirrors the real DOM Level 3 event dispatch algorithm: the browser first walks from the document root down to the actual click target, invoking every capture-phase listener it encounters along the way, and only once that entire descent is complete does it begin the ascent back up, invoking bubble-phase listeners. There is no interleaving between the two phases — capture always fully completes first, which is exactly why this snippet always plays the grey pulse to completion before starting the indigo one.

No. stopPropagation() only prevents the event from continuing to listeners that have not yet run, at the point in the phase sequence where it is called. Since this snippet calls it from inside Middle's bubble-phase handler, Middle's own capture-phase listener (which ran earlier, during the descent) is unaffected — it already fired. What gets prevented is only Outer's bubble-phase listener, the next one in line after Middle's bubble handler runs.

stopPropagation() prevents the event from reaching listeners on other elements further along the capture or bubble path, but any other listeners already registered on the same element for the same event will still run. stopImmediatePropagation() does both of those things and additionally prevents any remaining listeners on the very same element from running, even ones registered for the same phase. This snippet only demonstrates stopPropagation(), since only one bubble listener is ever registered per element here — adding a second bubble listener to Middle and calling stopImmediatePropagation() instead would be a natural extension to see the extra effect.

Because whichever element you click becomes the actual event target, and only elements from that target up to the document root (or down to it, for capture) are part of that particular dispatch's path. Clicking Middle directly means Inner is never part of the path at all, so only Middle's and Outer's handlers (and Middle's own target-phase handler) can fire — Inner's listeners are only ever invoked when Inner itself, or one of Inner's own descendants, is what was actually clicked.