✅ dispatchEvent() — Manually Triggering Events
Normally, events are triggered by user actions (click, keypress, scroll).
dispatchEvent() allows you to trigger an event using JavaScript.
📌 Why dispatchEvent() is Useful
Simulate user actions
Automate UI behavior
Write tests
Trigger existing event listeners programmatically
<div class="infographic-dispatchevent">
<style>
.infographic-dispatchevent {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-dispatchevent[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
}
.infographic-dispatchevent * { box-sizing: border-box; }
.infographic-dispatchevent h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-dispatchevent p { margin: 0; }
.infographic-dispatchevent button { font-family: inherit; }
.de-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.de-hero { text-align: center; margin-bottom: 1.2rem; }
.de-hero h1 { font-size: clamp(1.3rem, 4.5vw, 1.8rem); }
.de-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.84rem; }
.de-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.6rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; line-height: 1.6; margin-bottom: 1.3rem; white-space: pre; overflow-x: auto; }
.tok-kw { color: #569cd6; padding: 0; margin: 0; }
.tok-fn { color: #dcdcaa; padding: 0; margin: 0; }
.tok-str { color: #ce9178; padding: 0; margin: 0; }
.tok-var { color: #9cdcfe; padding: 0; margin: 0; }
.tok-punc { color: #d4d4d4; padding: 0; margin: 0; }
.tok-com { color: #6a9955; padding: 0; margin: 0; }
/* step tracker */
.de-phases { display: flex; align-items: center; justify-content: center; margin-bottom: 1.5rem; flex-wrap: wrap; }
.de-phase { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; width: 84px; }
.de-phase-num { width: 28px; height: 28px; border-radius: 50%; border: 1.5px solid var(--line); background: var(--paper-alt); display: flex; align-items: center; justify-content: center; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; font-weight: 700; color: var(--ink-soft); transition: border-color 0.3s ease, background 0.3s ease, color 0.3s ease, transform 0.3s ease; }
.de-phase-label { font-size: 0.56rem; text-align: center; color: var(--ink-soft); font-weight: 600; line-height: 1.2; }
.de-phase.is-active .de-phase-num { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); transform: scale(1.15); }
.de-phase.is-active .de-phase-label { color: var(--accent); }
.de-phase.is-done .de-phase-num { border-color: var(--good); background: var(--good-soft); color: var(--good); transform: scale(1); }
.de-phase.is-done .de-phase-label { color: var(--good); }
.de-phase-connect { width: 18px; height: 2px; background: var(--line); margin-bottom: 1.3rem; }
.de-page { border: 1.5px solid var(--line); background: var(--paper-alt); border-radius: 6px; overflow: hidden; margin-bottom: 1rem; }
.de-page-bar { background: #2b2b2b; padding: 0.4rem 0.6rem; display: flex; gap: 0.3rem; }
.de-page-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }
.de-page-body { padding: 1.1rem; background: var(--paper); text-align: center; }
.de-demo-btn { border: 1.5px solid var(--line); background: var(--paper-alt); color: var(--ink); padding: 0.55rem 1.2rem; font-size: 0.85rem; border-radius: 6px; transition: border-color 0.3s ease, background 0.3s ease, transform 0.3s ease; }
.de-demo-btn.is-fired { border-color: var(--good); background: var(--good-soft); transform: scale(1.06); }
.de-btnrow { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1rem; }
.de-trigger { border: none; background: var(--accent); color: #fff; padding: 0.5rem 1rem; font-size: 0.76rem; font-weight: 600; border-radius: 5px; cursor: pointer; }
.de-trigger:active { transform: scale(0.96); }
.de-trigger.alt { background: var(--good); }
.de-console { margin: 0 auto; max-width: 320px; text-align: left; border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.55rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.7rem; min-height: 3.2em; line-height: 1.6; }
.de-console .de-cline { display: block; opacity: 0; transition: opacity 0.25s ease; color: var(--good); }
.de-console .de-cline.is-shown { opacity: 1; }
.de-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.2rem; }
.de-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-dispatchevent *, .infographic-dispatchevent *::before, .infographic-dispatchevent *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="de-wrap">
<div class="de-code"><span class="tok-var">btn</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"click"</span><span class="tok-punc">, () => {</span>
<span class="tok-var">console</span><span class="tok-punc">.</span><span class="tok-fn">log</span><span class="tok-punc">(</span><span class="tok-str">"Button clicked"</span><span class="tok-punc">);</span>
<span class="tok-punc">});</span>
<span class="tok-kw">const</span> <span class="tok-var">clickEvent</span> <span class="tok-punc">=</span> <span class="tok-kw">new</span> <span class="tok-fn">Event</span><span class="tok-punc">(</span><span class="tok-str">"click"</span><span class="tok-punc">);</span>
<span class="tok-var">btn</span><span class="tok-punc">.</span><span class="tok-fn">dispatchEvent</span><span class="tok-punc">(</span><span class="tok-var">clickEvent</span><span class="tok-punc">);</span></div>
<div class="de-phases">
<div class="de-phase" id="ph1">
<div class="de-phase-num">1</div>
<div class="de-phase-label">create event</div>
</div>
<div class="de-phase-connect"></div>
<div class="de-phase" id="ph2">
<div class="de-phase-num">2</div>
<div class="de-phase-label">dispatch it</div>
</div>
<div class="de-phase-connect"></div>
<div class="de-phase" id="ph3">
<div class="de-phase-num">3</div>
<div class="de-phase-label">listeners run</div>
</div>
<div class="de-phase-connect"></div>
<div class="de-phase" id="ph4">
<div class="de-phase-num">4</div>
<div class="de-phase-label">normal event flow</div>
</div>
</div>
<div class="de-page">
<div class="de-page-bar"><div class="de-page-dot"></div><div class="de-page-dot"></div><div class="de-page-dot"></div></div>
<div class="de-page-body">
<button class="de-demo-btn" id="realBtn" type="button">#btn</button>
</div>
</div>
<div class="de-btnrow">
<button class="de-trigger" id="realClick" type="button">click it yourself</button>
<button class="de-trigger alt" id="dispatchClick" type="button">btn.dispatchEvent(clickEvent)</button>
</div>
<div class="de-console">
<span class="de-cline" id="c1">Button clicked</span>
</div>
<p class="de-note">Same listener, same console line, either way — <strong>dispatchEvent()</strong> just hands it a click it didn't get from a real mouse.</p>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-dispatchevent')
|| document.querySelector('.infographic-dispatchevent');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') { dark = true; }
else if (bodyAttr === 'false') { dark = false; }
else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const realBtn = wrapper.querySelector('#realBtn');
const c1 = wrapper.querySelector('#c1');
const phases = ['ph1', 'ph2', 'ph3', 'ph4'].map(id => wrapper.querySelector('#' + id));
// the actual listener — identical response no matter how the click arrives
realBtn.addEventListener('click', () => {
realBtn.classList.remove('is-fired');
void realBtn.offsetWidth;
realBtn.classList.add('is-fired');
c1.classList.remove('is-shown');
void c1.offsetWidth;
c1.classList.add('is-shown');
});
wrapper.querySelector('#realClick').addEventListener('click', () => {
phases.forEach(p => p.classList.remove('is-active', 'is-done'));
realBtn.click();
});
wrapper.querySelector('#dispatchClick').addEventListener('click', () => {
phases.forEach(p => p.classList.remove('is-active', 'is-done'));
const STEP = 380;
let t = 60;
setTimeout(() => { phases[0].classList.add('is-active'); }, t); // create event
t += STEP;
setTimeout(() => {
phases[0].classList.remove('is-active'); phases[0].classList.add('is-done');
phases[1].classList.add('is-active'); // dispatch it
}, t);
t += STEP;
setTimeout(() => {
phases[1].classList.remove('is-active'); phases[1].classList.add('is-done');
phases[2].classList.add('is-active'); // listeners run
const clickEvent = new Event('click');
realBtn.dispatchEvent(clickEvent);
}, t);
t += STEP;
setTimeout(() => {
phases[2].classList.remove('is-active'); phases[2].classList.add('is-done');
phases[3].classList.add('is-active'); // normal event flow
}, t);
t += STEP;
setTimeout(() => {
phases[3].classList.remove('is-active'); phases[3].classList.add('is-done');
}, t);
});
})();
</script>🧠 How It Works (Step-by-Step)
1️⃣ You create an event object
2️⃣ You dispatch it on an element
3️⃣ All listeners for that event type run
4️⃣ Event follows normal event flow (bubbling/capturing)
⚠️ Important Notes
Does not physically click the mouse
Only triggers JavaScript listeners
Respects event bubbling & capturing
📌 Real-World Uses
✔ Unit testing
✔ Auto-trigger actions
✔ Simulating clicks in tutorials
✅ Custom Events — Creating Your Own Events
Custom Events allow you to define application-specific events like:
logincartUpdatedthemeChanged
📌 Why Custom Events Are Powerful
Loose coupling between components
Cleaner architecture
Better scalability
🧪 Basic Custom Event Example
const loginEvent = new CustomEvent("login", {
detail: { user: "Bhaskar" }
});
// Listen for custom event
document.addEventListener("login", (event) => {
console.log(event.detail.user);
});
// Dispatch event
document.dispatchEvent(loginEvent);Bhaskar🧠 Understanding detail
detailcarries custom dataAvailable via
event.detailHelps pass information between components
🧪 Real-World Example — Component Communication
<div class="infographic-customevent">
<style>
.infographic-customevent {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-customevent[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
}
.infographic-customevent * { box-sizing: border-box; }
.infographic-customevent h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-customevent p { margin: 0; }
.infographic-customevent button, .infographic-customevent input { font-family: inherit; }
.ce-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.ce-hero { text-align: center; margin-bottom: 1.2rem; }
.ce-hero h1 { font-size: clamp(1.3rem, 4.5vw, 1.8rem); }
.ce-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.84rem; }
.ce-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.6rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; line-height: 1.6; margin-bottom: 1.2rem; white-space: pre; overflow-x: auto; }
.tok-kw { color: #569cd6; padding: 0; margin: 0; }
.tok-fn { color: #dcdcaa; padding: 0; margin: 0; }
.tok-str { color: #ce9178; padding: 0; margin: 0; }
.tok-var { color: #9cdcfe; padding: 0; margin: 0; }
.tok-punc { color: #d4d4d4; padding: 0; margin: 0; }
.tok-prop { color: #9cdcfe; padding: 0; margin: 0; }
.ce-field-label { font-family: 'IBM Plex Mono', monospace; font-size: 0.68rem; color: var(--ink-soft); margin-bottom: 0.35rem; text-align: center; }
.ce-input-row { display: flex; gap: 0.5rem; justify-content: center; align-items: center; margin-bottom: 1.1rem; flex-wrap: wrap; }
.ce-input { border: 1.5px solid var(--line); border-radius: 5px; padding: 0.45rem 0.7rem; font-size: 0.85rem; color: var(--ink); background: var(--paper); width: 170px; }
.ce-input:focus { outline: none; border-color: var(--accent); }
.ce-dispatch-btn { border: none; background: var(--accent); color: #fff; padding: 0.46rem 0.9rem; font-size: 0.78rem; font-weight: 600; border-radius: 5px; cursor: pointer; }
.ce-dispatch-btn:active { transform: scale(0.96); }
.ce-page { border: 1.5px solid var(--line); background: var(--paper-alt); border-radius: 6px; overflow: hidden; margin-bottom: 1rem; }
.ce-page-bar { background: #2b2b2b; padding: 0.4rem 0.6rem; display: flex; gap: 0.3rem; }
.ce-page-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }
.ce-page-body { padding: 1.1rem; background: var(--paper); text-align: center; }
.ce-badge { display: inline-flex; align-items: center; gap: 0.4rem; border: 1.5px dashed var(--line); border-radius: 6px; padding: 0.5rem 0.9rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.8rem; color: var(--ink-soft); transition: border-color 0.3s ease, background 0.3s ease, color 0.3s ease; }
.ce-badge.is-loggedin { border-style: solid; border-color: var(--good); background: var(--good-soft); color: var(--good); font-weight: 700; }
.ce-console { margin: 0 auto; max-width: 320px; text-align: left; border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.55rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.7rem; min-height: 1.8em; }
.ce-console .ce-cline { display: block; opacity: 0; transition: opacity 0.25s ease; color: var(--good); }
.ce-console .ce-cline.is-shown { opacity: 1; }
.ce-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.2rem; }
.ce-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-customevent *, .infographic-customevent *::before, .infographic-customevent *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="ce-wrap">
<div class="ce-code"><span class="tok-kw">const</span> <span class="tok-var">loginEvent</span> <span class="tok-punc">=</span> <span class="tok-kw">new</span> <span class="tok-fn">CustomEvent</span><span class="tok-punc">(</span><span class="tok-str">"login"</span><span class="tok-punc">, {</span>
<span class="tok-prop">detail</span><span class="tok-punc">: { </span><span class="tok-prop">user</span><span class="tok-punc">:</span> <span class="tok-str">"Bhaskar"</span> <span class="tok-punc">}</span>
<span class="tok-punc">});</span>
<span class="tok-var">document</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"login"</span><span class="tok-punc">, (</span><span class="tok-var">event</span><span class="tok-punc">) => {</span>
<span class="tok-var">console</span><span class="tok-punc">.</span><span class="tok-fn">log</span><span class="tok-punc">(</span><span class="tok-var">event</span><span class="tok-punc">.</span><span class="tok-var">detail</span><span class="tok-punc">.</span><span class="tok-var">user</span><span class="tok-punc">);</span>
<span class="tok-punc">});</span>
<span class="tok-var">document</span><span class="tok-punc">.</span><span class="tok-fn">dispatchEvent</span><span class="tok-punc">(</span><span class="tok-var">loginEvent</span><span class="tok-punc">);</span></div>
<div class="ce-field-label">detail.user</div>
<div class="ce-input-row">
<input class="ce-input" id="userInput" type="text" value="Bhaskar">
<button class="ce-dispatch-btn" id="dispatchBtn" type="button">dispatch "login"</button>
</div>
<div class="ce-page">
<div class="ce-page-bar"><div class="ce-page-dot"></div><div class="ce-page-dot"></div><div class="ce-page-dot"></div></div>
<div class="ce-page-body">
<div class="ce-badge" id="loginBadge">not logged in</div>
</div>
</div>
<div class="ce-console">
<span class="ce-cline" id="c1">Bhaskar</span>
</div>
<p class="ce-note">Change the name above, dispatch again — whatever you typed travels through <strong>event.detail</strong>, straight into the listener.</p>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-customevent')
|| document.querySelector('.infographic-customevent');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') { dark = true; }
else if (bodyAttr === 'false') { dark = false; }
else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const userInput = wrapper.querySelector('#userInput');
const dispatchBtn = wrapper.querySelector('#dispatchBtn');
const loginBadge = wrapper.querySelector('#loginBadge');
const c1 = wrapper.querySelector('#c1');
// the real listener — reads whatever detail it's handed
document.addEventListener('login', (event) => {
if (!wrapper.isConnected) return;
loginBadge.textContent = 'logged in as "' + event.detail.user + '"';
loginBadge.classList.add('is-loggedin');
c1.textContent = event.detail.user;
c1.classList.remove('is-shown');
void c1.offsetWidth;
c1.classList.add('is-shown');
});
dispatchBtn.addEventListener('click', () => {
const name = userInput.value.trim() || 'Anonymous';
const loginEvent = new CustomEvent('login', { detail: { user: name } });
document.dispatchEvent(loginEvent);
});
})();
</script>📌 When to Use Custom Events
✔ Component communication
✔ Decoupled systems
✔ Micro-frontend apps
✔ Plugin systems
✅ MutationObserver — Watching DOM Changes
MutationObserver allows you to observe changes in the DOM automatically.
📌 What Can It Observe?
Elements added or removed
Attribute changes
Text content changes
🧪 Basic Example
const observer = new MutationObserver(() => {
console.log("DOM changed");
});
observer.observe(document.body, {
childList: true,
subtree: true
});🧠 How It Works
1️⃣ Observer is created with a callback
2️⃣ You specify what to watch (observe)
3️⃣ When DOM changes → callback runs
🧪 Example — Detect Added Elements
<div class="infographic-mutobs">
<style>
.infographic-mutobs {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-mutobs[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
}
.infographic-mutobs * { box-sizing: border-box; }
.infographic-mutobs h1, .infographic-mutobs h2 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-mutobs p { margin: 0; }
.infographic-mutobs button { font-family: inherit; }
.mo-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; stroke: currentColor; }
.mo-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.mo-hero { text-align: center; margin-bottom: 1.1rem; }
.mo-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.mo-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
.mo-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.75rem 0.9rem; font-family: var(--mono-font); font-size: 0.68rem; line-height: 1.65; margin-bottom: 1.3rem; white-space: pre; overflow-x: auto; }
.tok-kw { color: #569cd6; }
.tok-fn { color: #dcdcaa; }
.tok-str { color: #ce9178; }
.tok-var { color: #9cdcfe; }
.tok-punc { color: #d4d4d4; }
.tok-prop { color: #9cdcfe; }
.tok-cmt { color: #6a9955; }
/* --- config row --- */
.mo-section-label { font-family: var(--mono-font); font-size: 0.68rem; color: var(--ink-soft); text-align: center; margin-bottom: 0.55rem; text-transform: lowercase; }
.mo-config { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1.4rem; }
.mo-config-item { display: flex; align-items: center; gap: 0.4rem; border: 1.5px solid var(--line); border-radius: 6px; padding: 0.4rem 0.7rem; font-family: var(--mono-font); font-size: 0.74rem; color: var(--ink-soft); cursor: pointer; background: var(--paper); transition: border-color 0.15s, color 0.15s, background 0.15s; }
.mo-config-item .mo-check-box { width: 14px; height: 14px; border: 1.5px solid var(--line); border-radius: 3px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: border-color 0.15s, background 0.15s; }
.mo-config-item .mo-check-box .mo-icon { width: 10px; height: 10px; opacity: 0; transition: opacity 0.1s; color: #fff; }
.mo-config-item.is-on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.mo-config-item.is-on .mo-check-box { background: var(--accent); border-color: var(--accent); }
.mo-config-item.is-on .mo-check-box .mo-icon { opacity: 1; }
/* --- playground --- */
.mo-page { border: 1.5px solid var(--line); background: var(--paper-alt); border-radius: 6px; overflow: hidden; margin-bottom: 1rem; }
.mo-page-bar { background: #2b2b2b; padding: 0.4rem 0.6rem; display: flex; gap: 0.3rem; }
.mo-page-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }
.mo-page-body { padding: 1.1rem 1.2rem 1.3rem; background: var(--paper); }
.mo-target-label { font-family: var(--mono-font); font-size: 0.68rem; color: var(--ink-soft); margin-bottom: 0.5rem; }
.mo-target { border: 1.5px dashed var(--accent); border-radius: 6px; padding: 0.7rem; min-height: 60px; transition: background 0.3s ease, border-color 0.15s ease; }
.mo-target.mo-flash { background: var(--accent-soft); }
.mo-node { background: var(--paper-alt); border: 1px solid var(--line); border-radius: 4px; padding: 0.4rem 0.6rem; margin-bottom: 0.4rem; font-family: var(--mono-font); font-size: 0.76rem; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; transition: background 0.3s ease, opacity 0.25s ease, transform 0.25s ease; }
.mo-node:last-child { margin-bottom: 0; }
.mo-node .mo-node-attr { color: var(--accent); }
.mo-node.mo-added { animation: mo-pop 0.4s ease; }
.mo-node.mo-removing { opacity: 0; transform: translateX(8px); }
.mo-empty-hint { font-family: var(--mono-font); font-size: 0.72rem; color: var(--ink-soft); font-style: italic; }
@keyframes mo-pop { 0% { opacity: 0; transform: scale(0.9); } 100% { opacity: 1; transform: scale(1); } }
.mo-actions { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; margin-top: 0.9rem; }
.mo-btn { display: inline-flex; align-items: center; gap: 0.35rem; border: 1px solid var(--line); background: var(--paper); color: var(--ink); padding: 0.4rem 0.7rem; font-size: 0.75rem; font-weight: 600; border-radius: 5px; cursor: pointer; transition: border-color 0.15s, color 0.15s; }
.mo-btn:hover { border-color: var(--accent); color: var(--accent); }
.mo-btn:active { transform: scale(0.96); }
.mo-btn .mo-icon { width: 13px; height: 13px; }
/* --- console --- */
.mo-console-label { font-family: var(--mono-font); font-size: 0.68rem; color: var(--ink-soft); text-align: center; margin: 1.1rem 0 0.5rem; }
.mo-console { margin: 0 auto; max-width: 460px; text-align: left; border: 1.5px solid var(--line); background: #1e1e1e; padding: 0.6rem 0.8rem; font-family: var(--mono-font); font-size: 0.7rem; min-height: 3.2em; max-height: 9em; overflow-y: auto; border-radius: 5px; }
.mo-console .mo-cline { display: block; opacity: 0; transform: translateY(-3px); transition: opacity 0.2s ease, transform 0.2s ease; margin-bottom: 0.15rem; }
.mo-console .mo-cline.is-shown { opacity: 1; transform: translateY(0); }
.mo-console .mo-cline .mo-tag { color: var(--good); }
.mo-console .mo-cline .mo-dim { color: #888; }
.mo-console .mo-cline.is-ignored { color: #6b6b6b; }
.mo-console .mo-cline.is-ignored .mo-tag { color: #6b6b6b; }
.mo-console-empty { color: #6b6b6b; font-style: italic; }
.mo-note { text-align: center; font-size: 0.8rem; color: var(--ink-soft); padding-top: 1.3rem; max-width: 560px; margin: 0 auto; }
.mo-note strong { color: var(--ink); }
@media (max-width: 480px) {
.mo-config { gap: 0.4rem; }
.mo-config-item { padding: 0.35rem 0.55rem; font-size: 0.68rem; }
}
@media (prefers-reduced-motion: reduce) {
.infographic-mutobs *, .infographic-mutobs *::before, .infographic-mutobs *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="mo-wrap">
<div class="mo-code"><span class="tok-kw">const</span> <span class="tok-var">observer</span> <span class="tok-punc">=</span> <span class="tok-kw">new</span> <span class="tok-fn">MutationObserver</span><span class="tok-punc">((</span><span class="tok-var">mutations</span><span class="tok-punc">) => {</span>
<span class="tok-var">mutations</span><span class="tok-punc">.</span><span class="tok-fn">forEach</span><span class="tok-punc">(</span><span class="tok-var">m</span> <span class="tok-punc">=></span> <span class="tok-var">console</span><span class="tok-punc">.</span><span class="tok-fn">log</span><span class="tok-punc">(</span><span class="tok-var">m</span><span class="tok-punc">.</span><span class="tok-prop">type</span><span class="tok-punc">));</span>
<span class="tok-punc">});</span>
<span class="tok-var">observer</span><span class="tok-punc">.</span><span class="tok-fn">observe</span><span class="tok-punc">(</span><span class="tok-var">target</span><span class="tok-punc">, {</span>
<span class="tok-prop">childList</span><span class="tok-punc">:</span> <span class="tok-kw">true</span><span class="tok-punc">,</span> <span class="tok-cmt">// nodes added/removed</span>
<span class="tok-prop">attributes</span><span class="tok-punc">:</span> <span class="tok-kw">true</span><span class="tok-punc">,</span> <span class="tok-cmt">// attribute changed</span>
<span class="tok-prop">characterData</span><span class="tok-punc">:</span> <span class="tok-kw">true</span><span class="tok-punc">,</span> <span class="tok-cmt">// text changed</span>
<span class="tok-prop">subtree</span><span class="tok-punc">:</span> <span class="tok-kw">true</span>
<span class="tok-punc">});</span></div>
<div class="mo-section-label">observer.observe(target, { ... }) — toggle what it watches</div>
<div class="mo-config" id="moConfig">
<button class="mo-config-item is-on" type="button" data-opt="childList">
<span class="mo-check-box"><svg class="mo-icon" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span>
childList
</button>
<button class="mo-config-item is-on" type="button" data-opt="attributes">
<span class="mo-check-box"><svg class="mo-icon" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span>
attributes
</button>
<button class="mo-config-item is-on" type="button" data-opt="characterData">
<span class="mo-check-box"><svg class="mo-icon" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span>
characterData
</button>
</div>
<div class="mo-page">
<div class="mo-page-bar"><div class="mo-page-dot"></div><div class="mo-page-dot"></div><div class="mo-page-dot"></div></div>
<div class="mo-page-body">
<div class="mo-target-label"><div id="target"> — the node being observed</div>
<div class="mo-target" id="moTarget">
<div class="mo-node" data-id="n1"><span><li class="<span class="mo-node-attr">item</span>">First child</li></span></div>
</div>
<div class="mo-actions">
<button class="mo-btn" id="moAdd" type="button">
<svg class="mo-icon" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
add node
</button>
<button class="mo-btn" id="moRemove" type="button">
<svg class="mo-icon" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
remove node
</button>
<button class="mo-btn" id="moAttr" type="button">
<svg class="mo-icon" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/></svg>
change attribute
</button>
<button class="mo-btn" id="moText" type="button">
<svg class="mo-icon" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16"/><path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"/><path d="M9 20h6"/></svg>
change text
</button>
</div>
</div>
</div>
<div class="mo-console-label">
<svg class="mo-icon" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:11px;height:11px;"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
observer callback log
</div>
<div class="mo-console" id="moConsole">
<span class="mo-console-empty">// waiting for a mutation…</span>
</div>
<p class="mo-note">Try unchecking <strong>attributes</strong>, then click "change attribute" — the DOM still changes, but with nothing watching for it, the callback stays silent.</p>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-mutobs')
|| document.querySelector('.infographic-mutobs');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') {
dark = true;
} else if (bodyAttr === 'false') {
dark = false;
} else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const config = { childList: true, attributes: true, characterData: true };
const configEl = wrapper.querySelector('#moConfig');
const target = wrapper.querySelector('#moTarget');
const consoleEl = wrapper.querySelector('#moConsole');
const addBtn = wrapper.querySelector('#moAdd');
const removeBtn = wrapper.querySelector('#moRemove');
const attrBtn = wrapper.querySelector('#moAttr');
const textBtn = wrapper.querySelector('#moText');
let consoleCleared = false;
let nodeCount = 1;
const attrWords = ['item', 'active', 'featured', 'pinned'];
const textWords = ['First child', 'Updated!', 'Changed again', 'Still watching'];
let attrIdx = 0, textIdx = 0;
function log(type, message, observed) {
if (!consoleCleared) { consoleEl.innerHTML = ''; consoleCleared = true; }
const line = document.createElement('span');
line.className = 'mo-cline' + (observed ? '' : ' is-ignored');
line.innerHTML = observed
? '<span class="mo-tag">[' + type + ']</span> ' + message
: '<span class="mo-tag">[' + type + ']</span> <span class="mo-dim">not observed — config unchecked</span>';
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
requestAnimationFrame(() => line.classList.add('is-shown'));
// trim to last 8 lines
while (consoleEl.children.length > 8) consoleEl.removeChild(consoleEl.firstChild);
}
function flashTarget() {
target.classList.add('mo-flash');
setTimeout(() => target.classList.remove('mo-flash'), 300);
}
// config toggles
configEl.querySelectorAll('.mo-config-item').forEach(item => {
item.addEventListener('click', () => {
const opt = item.dataset.opt;
config[opt] = !config[opt];
item.classList.toggle('is-on', config[opt]);
});
});
addBtn.addEventListener('click', () => {
nodeCount += 1;
const node = document.createElement('div');
node.className = 'mo-node mo-added';
node.innerHTML = '<span><li class="<span class="mo-node-attr">item</span>">New child ' + nodeCount + '</li></span>';
target.appendChild(node);
flashTarget();
log('childList', '1 node added', config.childList);
});
removeBtn.addEventListener('click', () => {
const nodes = target.querySelectorAll('.mo-node');
if (!nodes.length) return;
const last = nodes[nodes.length - 1];
last.classList.add('mo-removing');
flashTarget();
log('childList', '1 node removed', config.childList);
setTimeout(() => {
last.remove();
if (!target.querySelectorAll('.mo-node').length) {
target.innerHTML = '<span class="mo-empty-hint">// no children left</span>';
}
}, 250);
});
attrBtn.addEventListener('click', () => {
const first = target.querySelector('.mo-node');
if (!first) return;
attrIdx = (attrIdx + 1) % attrWords.length;
const attrSpan = first.querySelector('.mo-node-attr');
if (attrSpan) attrSpan.textContent = attrWords[attrIdx];
flashTarget();
log('attributes', 'class → "' + attrWords[attrIdx] + '"', config.attributes);
});
textBtn.addEventListener('click', () => {
const first = target.querySelector('.mo-node');
if (!first) return;
textIdx = (textIdx + 1) % textWords.length;
const parts = first.querySelector('span');
if (parts) {
const attrSpan = parts.querySelector('.mo-node-attr');
const attrWord = attrSpan ? attrSpan.textContent : 'item';
parts.innerHTML = '<li class="<span class="mo-node-attr">' + attrWord + '</span>">' + textWords[textIdx] + '</li>';
}
flashTarget();
log('characterData', 'text → "' + textWords[textIdx] + '"', config.characterData);
});
})();
</script>🧩 Observer Options Explained
Option | Meaning |
|---|---|
| Watch child add/remove |
| Watch attribute changes |
| Watch all descendants |
| Watch text changes |
⚠️ Best Practices
✔ Disconnect when not needed
✔ Avoid heavy logic in callback
✔ Observe specific elements
observer.disconnect();📌 Real-World Uses
✔ Dynamic content detection
✔ Framework internals (React, Vue)
✔ Ad blockers & extensions
✔ Chat apps & live feeds
🧠 Quick Comparison
Feature | Purpose |
|---|---|
| Trigger events manually |
Custom Events | App-specific communication |
MutationObserver | Watch DOM changes |