<h1>🔮 MutationObserver Magic Show</h1>
<p class="intro">Watch the DOM change before your eyes and see how MutationObserver catches every transformation!</p>

<div class="playground-container">
	<div class="observer-status status-inactive" id="observerStatus">
		Observer Status: INACTIVE
	</div>
	<div class="stats">
		<div class="stat">
			<div class="stat-value" id="elemCount">1</div>
			<div class="stat-label">Elements</div>
		</div>
		<div class="stat">
			<div class="stat-value" id="addCount">0</div>
			<div class="stat-label">Additions</div>
		</div>
		<div class="stat">
			<div class="stat-value" id="removeCount">0</div>
			<div class="stat-label">Removals</div>
		</div>
		<div class="stat">
			<div class="stat-value" id="attrCount">0</div>
			<div class="stat-label">Attributes</div>
		</div>
		<div class="stat">
			<div class="stat-value" id="textCount">0</div>
			<div class="stat-label">Text Changes</div>
		</div>
	</div>

	<div class="controls">
		<div class="control-group">
			<h3>Observer Controls</h3>
			<div class="buttons">
				<button id="startBtn" class="btn-start">Start Observing</button>
				<button id="stopBtn" class="btn-stop">Stop Observing</button>
			</div>
			<div class="options">
				<label class="checkbox-container">
					<input type="checkbox" id="childListOpt" checked>
					childList
				</label>
				<label class="checkbox-container">
					<input type="checkbox" id="attributesOpt" checked>
					attributes
				</label>
				<label class="checkbox-container">
					<input type="checkbox" id="characterDataOpt" checked>
					characterData
				</label>
				<label class="checkbox-container">
					<input type="checkbox" id="subtreeOpt" checked>
					subtree
				</label>
			</div>
		</div>

		<div class="control-group">
			<h3>DOM Manipulation</h3>
			<div class="buttons">
				<button id="addBtn" class="btn-add">Add Element</button>
				<button id="removeBtn" class="btn-remove">Remove Element</button>
				<button id="colorBtn" class="btn-color">Change Color</button>
				<button id="textBtn" class="btn-text">Change Text</button>
				<button id="clearLogBtn" class="btn-clear">Clear Log</button>
			</div>
		</div>
	</div>

	<div class="playground">
		<div class="dom-container" id="domContainer">
			<div class="element" data-id="initial">Initial Element</div>
		</div>

		<div class="log-container">
			<h3>Mutation Log</h3>
			<div id="logContainer"></div>
		</div>
	</div>

	<div class="challenge" id="challengeContainer">
		<h3>Challenge: Observer Mastery</h3>
		<p>Complete these tasks to master MutationObserver:</p>
		<ol>
			<li id="challenge1">Add at least 3 elements</li>
			<li id="challenge2">Remove at least 1 element</li>
			<li id="challenge3">Change the color of an element</li>
			<li id="challenge4">Change text content of an element</li>
			<li id="challenge5">Observe with only childList option enabled</li>
		</ol>
	</div>
</div>
:root {
	--primary: #6200ee;
	--secondary: #03dac6;
	--danger: #cf6679;
	--warning: #ffab00;
	--success: #00c853;
	--info: #2196f3;
	--light: #f5f5f5;
	--dark: #121212;
}

body {
	font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
	margin: 0;
	padding: 20px;
	max-width: 1000px;
	margin: 0 auto;
	background-color: var(--light);
	color: var(--dark);
}

h1,
h2 {
	text-align: center;
	color: var(--primary);
}

.playground-container {
	display: flex;
	flex-direction: column;
	gap: 20px;
}

.observer-status {
	text-align: center;
	padding: 10px;
	border-radius: 8px;
	font-weight: bold;
	color: white;
	margin-bottom: 15px;
	font-size: 18px;
	text-transform: uppercase;
	letter-spacing: 1px;
	transition: all 0.3s ease;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}

.status-active {
	background-color: var(--success);
}

.status-inactive {
	background-color: var(--danger);
}

.controls {
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
	padding: 15px;
	background-color: white;
	border-radius: 8px;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.control-group {
	flex: 1;
	min-width: 300px;
}

.buttons {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
}

button {
	padding: 10px 16px;
	border: none;
	border-radius: 6px;
	font-weight: bold;
	cursor: pointer;
	transition: all 0.2s;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
	min-width: 100px;
}

button:hover {
	transform: translateY(-2px);
	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}

button:active {
	transform: translateY(0);
}

.btn-start {
	background-color: var(--success);
	color: white;
}
.btn-stop {
	background-color: var(--danger);
	color: white;
}
.btn-add {
	background-color: var(--primary);
	color: white;
}
.btn-remove {
	background-color: var(--danger);
	color: white;
}
.btn-color {
	background-color: var(--secondary);
	color: var(--dark);
}
.btn-text {
	background-color: var(--info);
	color: white;
}
.btn-clear {
	background-color: var(--warning);
	color: var(--dark);
}

.options {
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
	margin-top: 15px;
}

.checkbox-container {
	display: flex;
	align-items: center;
	margin-right: 15px;
}

.checkbox-container input {
	margin-right: 5px;
}

.playground {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 20px;
}

.dom-container {
	border: 3px dashed var(--primary);
	min-height: 300px;
	padding: 15px;
	border-radius: 8px;
	background-color: white;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
	overflow-y: auto;
	display: flex;
	flex-wrap: wrap;
	align-content: flex-start;
	gap: 10px;
	transition: all 0.3s ease;
}

.dom-container-inactive {
	border: 3px dashed var(--danger);
	background-color: #ffebee;
}

.element {
	padding: 12px;
	border-radius: 6px;
	margin: 5px;
	display: inline-block;
	background-color: var(--light);
	border: 1px solid #ddd;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
	transition: all 0.3s ease;
	user-select: none;
	cursor: pointer;
}

.element:hover {
	transform: scale(1.05);
	box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.log-container {
	border: 1px solid #ddd;
	min-height: 300px;
	padding: 15px;
	border-radius: 8px;
	background-color: white;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
	overflow-y: auto;
	display: flex;
	flex-direction: column;
}

.log-entry {
	padding: 8px 12px;
	margin-bottom: 8px;
	border-radius: 6px;
	font-size: 14px;
	animation: fadeIn 0.5s;
	border-left: 4px solid transparent;
}

.log-time {
	font-size: 12px;
	color: #666;
	margin-right: 8px;
}

.log-info {
	background-color: #e3f2fd;
	border-left-color: var(--info);
}
.log-add {
	background-color: #e8f5e9;
	border-left-color: var(--success);
}
.log-remove {
	background-color: #ffebee;
	border-left-color: var(--danger);
}
.log-attr {
	background-color: #fff8e1;
	border-left-color: var(--warning);
}
.log-text {
	background-color: #e0f7fa;
	border-left-color: var(--secondary);
}

.stats {
	display: flex;
	justify-content: space-between;
	padding: 10px;
	background-color: var(--dark);
	color: white;
	border-radius: 8px;
	margin-bottom: 15px;
}

.stat {
	text-align: center;
	flex: 1;
}

.stat-value {
	font-size: 24px;
	font-weight: bold;
	color: var(--secondary);
}

@keyframes fadeIn {
	from {
		opacity: 0;
		transform: translateY(10px);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}

@keyframes pulse {
	0% {
		transform: scale(1);
	}
	50% {
		transform: scale(1.05);
	}
	100% {
		transform: scale(1);
	}
}

.pulse {
	animation: pulse 0.5s;
}

.challenge {
	background-color: white;
	padding: 15px;
	border-radius: 8px;
	margin-top: 20px;
	box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.challenge h3 {
	color: var(--primary);
	margin-top: 0;
}

.challenge-complete {
	background-color: #e8f5e9;
	border-left: 4px solid var(--success);
}
// DOM Elements & State Management
const elements = {
	domContainer: document.getElementById("domContainer"),
	logContainer: document.getElementById("logContainer"),
	startBtn: document.getElementById("startBtn"),
	stopBtn: document.getElementById("stopBtn"),
	addBtn: document.getElementById("addBtn"),
	removeBtn: document.getElementById("removeBtn"),
	colorBtn: document.getElementById("colorBtn"),
	textBtn: document.getElementById("textBtn"),
	clearLogBtn: document.getElementById("clearLogBtn"),
	childListOpt: document.getElementById("childListOpt"),
	attributesOpt: document.getElementById("attributesOpt"),
	characterDataOpt: document.getElementById("characterDataOpt"),
	subtreeOpt: document.getElementById("subtreeOpt"),
	observerStatus: document.getElementById("observerStatus"),
	elemCount: document.getElementById("elemCount"),
	addCount: document.getElementById("addCount"),
	removeCount: document.getElementById("removeCount"),
	attrCount: document.getElementById("attrCount"),
	textCount: document.getElementById("textCount"),
	challenge1: document.getElementById("challenge1"),
	challenge2: document.getElementById("challenge2"),
	challenge3: document.getElementById("challenge3"),
	challenge4: document.getElementById("challenge4"),
	challenge5: document.getElementById("challenge5")
};

// State
const state = {
	observer: null,
	observing: false,
	stats: {
		elements: 1,
		additions: 0,
		removals: 0,
		attributes: 0,
		texts: 0
	},
	challenges: {
		addElements: false,
		removeElement: false,
		changeColor: false,
		changeText: false,
		childListOnly: false
	}
};

// Color palette for elements
const colors = [
	"#ffcdd2",
	"#f8bbd0",
	"#e1bee7",
	"#d1c4e9",
	"#c5cae9",
	"#bbdefb",
	"#b3e5fc",
	"#b2ebf2",
	"#b2dfdb",
	"#c8e6c9",
	"#dcedc8",
	"#f0f4c3",
	"#fff9c4",
	"#ffecb3",
	"#ffe0b2"
];

// Utility Functions
const getRandomColor = () => colors[Math.floor(Math.random() * colors.length)];

const getRandomElement = () => {
	const elementsList = elements.domContainer.querySelectorAll(".element");
	if (elementsList.length === 0) return null;
	return elementsList[Math.floor(Math.random() * elementsList.length)];
};

// Logging & UI Updates
const addLog = (message, type) => {
	const entry = document.createElement("div");
	entry.className = `log-entry log-${type}`;

	const time = document.createElement("span");
	time.className = "log-time";
	time.textContent = new Date().toLocaleTimeString();

	entry.appendChild(time);
	entry.appendChild(document.createTextNode(message));

	elements.logContainer.insertBefore(entry, elements.logContainer.firstChild);
};

const updateStats = () => {
	elements.elemCount.textContent = state.stats.elements;
	elements.addCount.textContent = state.stats.additions;
	elements.removeCount.textContent = state.stats.removals;
	elements.attrCount.textContent = state.stats.attributes;
	elements.textCount.textContent = state.stats.texts;

	// Apply pulse animation
	elements.elemCount.classList.add("pulse");
	setTimeout(() => elements.elemCount.classList.remove("pulse"), 500);
};

const checkChallenges = () => {
	// Challenge 1: Add elements
	if (state.stats.additions >= 3 && !state.challenges.addElements) {
		state.challenges.addElements = true;
		elements.challenge1.style.textDecoration = "line-through";
		elements.challenge1.style.color = "green";
	}

	// Challenge 2: Remove element
	if (state.stats.removals >= 1 && !state.challenges.removeElement) {
		state.challenges.removeElement = true;
		elements.challenge2.style.textDecoration = "line-through";
		elements.challenge2.style.color = "green";
	}

	// Challenge 3: Change color
	if (state.stats.attributes >= 1 && !state.challenges.changeColor) {
		state.challenges.changeColor = true;
		elements.challenge3.style.textDecoration = "line-through";
		elements.challenge3.style.color = "green";
	}

	// Challenge 4: Change text
	if (state.stats.texts >= 1 && !state.challenges.changeText) {
		state.challenges.changeText = true;
		elements.challenge4.style.textDecoration = "line-through";
		elements.challenge4.style.color = "green";
	}

	// Check if all challenges complete
	if (Object.values(state.challenges).every(Boolean)) {
		const challengeContainer = document.getElementById("challengeContainer");
		challengeContainer.classList.add("challenge-complete");
		challengeContainer.innerHTML =
			"<h3>🎉 Congratulations! You've mastered MutationObserver!</h3><p>You now understand how to track DOM changes like a pro. This powerful API lets you respond to dynamic content in real-time.</p>";
	}
};

// Observer Functions
const startObserving = () => {
	if (state.observing) return;

	// Get current element count for accuracy
	state.stats.elements = elements.domContainer.querySelectorAll(
		".element"
	).length;
	updateStats();

	// Create options object based on checkboxes
	const options = {
		childList: elements.childListOpt.checked,
		attributes: elements.attributesOpt.checked,
		characterData: elements.characterDataOpt.checked,
		subtree: elements.subtreeOpt.checked,
		attributeOldValue: true
	};

	// Check for childList only challenge
	if (
		options.childList &&
		!options.attributes &&
		!options.characterData &&
		!state.challenges.childListOnly
	) {
		state.challenges.childListOnly = true;
		elements.challenge5.style.textDecoration = "line-through";
		elements.challenge5.style.color = "green";
		checkChallenges();
	}

	// Create new MutationObserver
	state.observer = new MutationObserver((mutations) => {
		for (const mutation of mutations) {
			// Handle different mutation types
			if (mutation.type === "childList") {
				handleChildListMutation(mutation);
			} else if (mutation.type === "attributes") {
				handleAttributeMutation(mutation);
			} else if (mutation.type === "characterData") {
				handleCharacterDataMutation(mutation);
			}
		}
	});

	// Start observing with options
	state.observer.observe(elements.domContainer, options);
	state.observing = true;

	// Update UI
	elements.startBtn.disabled = true;
	elements.stopBtn.disabled = false;
	elements.observerStatus.textContent = "Observer Status: ACTIVE";
	elements.observerStatus.classList.remove("status-inactive");
	elements.observerStatus.classList.add("status-active");
	elements.domContainer.classList.remove("dom-container-inactive");

	// Add log entry
	addLog(`Started observing with options: ${JSON.stringify(options)}`, "info");
};

const stopObserving = () => {
	if (!state.observing) return;

	state.observer.disconnect();
	state.observing = false;

	// Update UI
	elements.startBtn.disabled = false;
	elements.stopBtn.disabled = true;
	elements.observerStatus.textContent = "Observer Status: INACTIVE";
	elements.observerStatus.classList.remove("status-active");
	elements.observerStatus.classList.add("status-inactive");
	elements.domContainer.classList.add("dom-container-inactive");

	// Add log entry
	addLog("Stopped observing", "info");
};

// Mutation Handlers
const handleChildListMutation = (mutation) => {
	// Added nodes
	if (mutation.addedNodes.length > 0) {
		state.stats.additions += mutation.addedNodes.length;
		state.stats.elements = elements.domContainer.querySelectorAll(
			".element"
		).length;
		updateStats();

		const nodeInfo = Array.from(mutation.addedNodes)
			.filter((node) => node.classList?.contains("element"))
			.map((node) => node.dataset.id)
			.join(", ");

		if (nodeInfo) {
			addLog(`Added element(s): ${nodeInfo}`, "add");
			checkChallenges();
		}
	}

	// Removed nodes
	if (mutation.removedNodes.length > 0) {
		state.stats.removals += mutation.removedNodes.length;
		state.stats.elements = elements.domContainer.querySelectorAll(
			".element"
		).length;
		updateStats();

		const nodeInfo = Array.from(mutation.removedNodes)
			.filter((node) => node.classList?.contains("element"))
			.map((node) => node.dataset.id)
			.join(", ");

		if (nodeInfo) {
			addLog(`Removed element(s): ${nodeInfo}`, "remove");
			checkChallenges();
		}
	}
};

const handleAttributeMutation = (mutation) => {
	state.stats.attributes++;
	updateStats();

	const target = mutation.target.dataset.id;
	addLog(`Changed ${mutation.attributeName} on element ${target}`, "attr");
	checkChallenges();
};

const handleCharacterDataMutation = (mutation) => {
	state.stats.texts++;
	updateStats();

	// Find closest element with data-id
	let node = mutation.target;
	while (node && !node.dataset?.id) {
		node = node.parentNode;
	}
	const id = node?.dataset.id || "unknown";

	addLog(`Changed text content of element ${id}`, "text");
	checkChallenges();
};

// DOM Manipulation Functions
const addElement = () => {
	const id = `elem-${Date.now()}`;
	const element = document.createElement("div");
	element.className = "element";
	element.dataset.id = id;
	element.textContent = `Element ${id.split("-")[1]}`;

	// Add click handler to change color
	element.addEventListener("click", () => changeElementColor(element));

	elements.domContainer.appendChild(element);

	// Only log when observer is inactive, don't update stats
	if (!state.observing) {
		addLog(
			`Added element: ${id} (Observer inactive - changes not tracked!)`,
			"info"
		);
	}
};

const removeElement = () => {
	const elementsList = elements.domContainer.querySelectorAll(".element");
	if (elementsList.length <= 1) return; // Keep at least one element

	const randomIndex = Math.floor(Math.random() * elementsList.length);
	const element = elementsList[randomIndex];
	const id = element.dataset.id;
	element.remove();

	// Only log when observer is inactive, don't update stats
	if (!state.observing) {
		addLog(
			`Removed element: ${id} (Observer inactive - changes not tracked!)`,
			"info"
		);
	}
};

const changeColor = () => {
	const element = getRandomElement();
	if (element) {
		changeElementColor(element);
	}
};

const changeElementColor = (element) => {
	element.style.backgroundColor = getRandomColor();

	// Only log when observer is inactive, don't update stats
	if (!state.observing) {
		addLog(
			`Changed background color of element ${element.dataset.id} (Observer inactive - changes not tracked!)`,
			"info"
		);
	}
};

const changeText = () => {
	const element = getRandomElement();
	if (!element) return;

	const id = element.dataset.id;

	// This is the key fix: modify the text node's data property directly
	// instead of using textContent, which doesn't always trigger characterData mutations
	if (
		element.childNodes.length > 0 &&
		element.firstChild.nodeType === Node.TEXT_NODE
	) {
		// If there's already a text node, modify it
		element.firstChild.nodeValue = `Changed at ${new Date().toLocaleTimeString()}`;
	} else {
		// Otherwise create a new text node
		while (element.firstChild) element.removeChild(element.firstChild);
		element.appendChild(
			document.createTextNode(`Changed at ${new Date().toLocaleTimeString()}`)
		);
	}

	// Only log when observer is inactive, don't update stats
	if (!state.observing) {
		addLog(
			`Changed text content of element ${id} (Observer inactive - changes not tracked!)`,
			"info"
		);
	}
};

const clearLog = () => {
	elements.logContainer.innerHTML = "";
	addLog("Log cleared", "info");
};

// Event listeners
const setupEventListeners = () => {
	elements.startBtn.addEventListener("click", startObserving);
	elements.stopBtn.addEventListener("click", stopObserving);
	elements.addBtn.addEventListener("click", addElement);
	elements.removeBtn.addEventListener("click", removeElement);
	elements.colorBtn.addEventListener("click", changeColor);
	elements.textBtn.addEventListener("click", changeText);
	elements.clearLogBtn.addEventListener("click", clearLog);
};

// Initialize
const init = () => {
	elements.stopBtn.disabled = true;
	elements.domContainer.classList.add("dom-container-inactive");

	// Make sure characterData and subtree are checked by default
	elements.characterDataOpt.checked = true;
	elements.subtreeOpt.checked = true;

	addLog(
		'Welcome to the MutationObserver Magic Show! Click "Start Observing" to begin.',
		"info"
	);
	addLog("Tip: Click directly on elements to change their color!", "info");
	setupEventListeners();
};

// Start the app
init();
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.