π In the DOM, everything is a node
Element nodes β
<div>,<p>,<h1>Text nodes β spaces, line breaks, text inside elements
Comment nodes β
<!-- comment -->
π Whitespace also becomes a text node.
π³ Sample HTML
<div id="box">
<h2>Title</h2>
<p>Paragraph</p>
</div>β οΈ Notice the new lines and spaces β they matter.
πΉ 1. childNodes
βοΈ What it does: Returns ALL child nodes of an element.
βοΈ Includes
β Text nodes (spaces, line breaks)
β Element nodes
π§ͺ Example
const box = document.getElementById("box");
console.log(box.childNodes);NodeList(5)
0: text β newline
1: h2
2: text β newline
3: p
4: text β newlineπ Why?
Because childNodes follows the raw DOM tree, not just HTML tags.
πΉ 2. children
βοΈ What it does: Returns ONLY element nodes.
β Does NOT include
Text nodes
Comments
π§ͺ Example
console.log(box.children);HTMLCollection(2) [h2, p]π Why?
This property is designed for clean element traversal, ignoring whitespace.
πΉ 3. firstChild
βοΈ What it does: Returns the first node (not element) inside a parent.
β οΈ Important: It may return a text node!
π§ͺ Example
console.log(box.firstChild);#textπ Why?
The first thing inside <div> is a newline, which becomes a text node.
πΉ 4. firstElementChild
βοΈ What it does: Returns the first ELEMENT only.
π§ͺ Example
console.log(box.firstElementChild);<h2>Title</h2>π Why?
It skips text nodes and finds the first real HTML element.
<div class="infographic-childnodes">
<style>
.infographic-childnodes {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
--amber: #a4720a;
--amber-soft: #faf1de;
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-childnodes[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;
--amber: #d9a94a;
--amber-soft: #2c2413;
}
.infographic-childnodes * { box-sizing: border-box; }
.infographic-childnodes h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-childnodes p { margin: 0; }
.infographic-childnodes button { font-family: inherit; }
.cn-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.cn-hero { text-align: center; margin-bottom: 1.2rem; }
.cn-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.cn-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.84rem; }
.cn-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: center; margin-bottom: 1.2rem; }
.cn-tab { border: 1px solid var(--line); background: var(--paper-alt); color: var(--ink-soft); padding: 0.34rem 0.6rem; font-size: 0.64rem; font-weight: 700; cursor: pointer; font-family: 'IBM Plex Mono', monospace; }
.cn-tab.is-active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.cn-tab:hover:not(.is-active) { border-color: var(--ink-soft); color: var(--ink); }
/* shared mini page */
.cn-page { border: 1.5px solid var(--line); background: var(--paper-alt); border-radius: 6px; overflow: hidden; margin-bottom: 0.9rem; }
.cn-page-bar { background: #2b2b2b; padding: 0.4rem 0.6rem; display: flex; gap: 0.3rem; }
.cn-page-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }
.cn-page-body { padding: 0.9rem 1rem; background: var(--paper); }
.cn-container { border: 1.5px dashed var(--line); border-radius: 5px; padding: 0.5rem; }
.cn-tag { font-family: 'IBM Plex Mono', monospace; font-size: 0.56rem; color: var(--ink-soft); display: block; margin-bottom: 0.3rem; }
.cn-node { border-radius: 4px; margin-top: 0.3rem; transition: border-color 0.35s ease, background 0.35s ease, box-shadow 0.35s ease, opacity 0.3s ease; }
.cn-node:first-of-type { margin-top: 0; }
.cn-node.el { border: 1.5px dashed var(--line); padding: 0.3rem 0.5rem; font-size: 0.78rem; color: var(--ink); }
.cn-node.el .cn-elname { font-family: 'IBM Plex Mono', monospace; font-size: 0.56rem; color: var(--ink-soft); display: block; margin-bottom: 0.1rem; }
.cn-node.txt { border: 1.5px dotted var(--line); padding: 0.15rem 0.5rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.6rem; color: var(--ink-soft); opacity: 0.55; }
.cn-node.is-hit { border-style: solid; border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 3px var(--accent-soft); opacity: 1; }
.cn-node.el.is-hit { color: var(--accent); }
.cn-node.is-first { border-style: solid; border-color: var(--good); background: var(--good-soft); box-shadow: 0 0 0 3px var(--good-soft); opacity: 1; }
.cn-node.el.is-first { color: var(--good); }
.cn-node.is-skip { opacity: 0.3; text-decoration: line-through; }
.cn-panel { display: none; }
.cn-panel.is-active { display: block; }
.cn-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.6rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.68rem; line-height: 1.6; margin-bottom: 1rem; 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; }
.cn-play { display: block; margin: 0 auto; border: none; background: var(--accent); color: #fff; padding: 0.5rem 1.2rem; font-size: 0.8rem; font-weight: 600; cursor: pointer; }
.cn-play:active { transform: scale(0.96); }
.cn-console { margin: 1rem auto 0; max-width: 340px; text-align: left; border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.5rem 0.75rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; min-height: 1.8em; line-height: 1.6; }
.cn-console .cn-cline { opacity: 0; transition: opacity 0.25s ease; color: var(--good); display: block; white-space: pre; }
.cn-console .cn-cline.is-shown { opacity: 1; }
.cn-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.3rem; }
.cn-note strong { color: var(--ink); }
.cn-legend { display: flex; justify-content: center; gap: 1.2rem; margin-top: 0.7rem; margin-bottom: 1.4rem; font-size: 0.66rem; color: var(--ink-soft); flex-wrap: wrap; }
.cn-legend span { display: inline-flex; align-items: center; gap: 0.3rem; }
.cn-legend .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.cn-legend .dot.hit { background: var(--accent); }
.cn-legend .dot.first { background: var(--good); }
.cn-legend .dot.txt { background: var(--ink-soft); opacity: 0.5; }
@media (prefers-reduced-motion: reduce) {
.infographic-childnodes *, .infographic-childnodes *::before, .infographic-childnodes *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="cn-wrap">
<div class="cn-hero">
<h1>childNodes vs children</h1>
<p>Whitespace between your tags becomes real text nodes β some methods see them, some don't.</p>
</div>
<div class="cn-tabs">
<button class="cn-tab is-active" data-tab="childnodes" type="button">childNodes</button>
<button class="cn-tab" data-tab="children" type="button">children</button>
<button class="cn-tab" data-tab="firstchild" type="button">firstChild</button>
<button class="cn-tab" data-tab="firstelchild" type="button">firstElementChild</button>
</div>
<div class="cn-page">
<div class="cn-page-bar"><div class="cn-page-dot"></div><div class="cn-page-dot"></div><div class="cn-page-dot"></div></div>
<div class="cn-page-body">
<div class="cn-container">
<span class="cn-tag"><div id="box"></span>
<div class="cn-node txt" id="nT0">#text β΅</div>
<div class="cn-node el" id="nH2"><span class="cn-elname"><h2></span>Title</div>
<div class="cn-node txt" id="nT1">#text β΅</div>
<div class="cn-node el" id="nP"><span class="cn-elname"><p></span>Paragraph</div>
<div class="cn-node txt" id="nT2">#text β΅</div>
</div>
</div>
</div>
<div class="cn-legend">
<span><span class="dot hit"></span> included</span>
<span><span class="dot first"></span> the answer</span>
<span><span class="dot txt"></span> text node (whitespace)</span>
</div>
<!-- childNodes -->
<div class="cn-panel is-active" data-panel="childnodes">
<div class="cn-code"><span class="tok-kw">const</span> <span class="tok-var">box</span> <span class="tok-punc">=</span> <span class="tok-var">document</span><span class="tok-punc">.</span><span class="tok-fn">getElementById</span><span class="tok-punc">(</span><span class="tok-str">"box"</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">box</span><span class="tok-punc">.</span><span class="tok-var">childNodes</span><span class="tok-punc">);</span></div>
<button class="cn-play" data-run="childnodes" type="button">run</button>
<div class="cn-console">
<span class="cn-cline" id="outCN">NodeList(5)
0: text 1: h2 2: text 3: p 4: text</span>
</div>
<p class="cn-note">Every node, <strong>including</strong> the whitespace between tags β five, not two.</p>
</div>
<!-- children -->
<div class="cn-panel" data-panel="children">
<div class="cn-code"><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">box</span><span class="tok-punc">.</span><span class="tok-var">children</span><span class="tok-punc">);</span></div>
<button class="cn-play" data-run="children" type="button">run</button>
<div class="cn-console"><span class="cn-cline" id="outCH">HTMLCollection(2) [h2, p]</span></div>
<p class="cn-note">Text nodes are filtered out entirely β just the real elements.</p>
</div>
<!-- firstChild -->
<div class="cn-panel" data-panel="firstchild">
<div class="cn-code"><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">box</span><span class="tok-punc">.</span><span class="tok-var">firstChild</span><span class="tok-punc">);</span></div>
<button class="cn-play" data-run="firstchild" type="button">run</button>
<div class="cn-console"><span class="cn-cline" id="outFC">#text</span></div>
<p class="cn-note">The very first node β which is usually the newline, <strong>not</strong> your <h2>.</p>
</div>
<!-- firstElementChild -->
<div class="cn-panel" data-panel="firstelchild">
<div class="cn-code"><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">box</span><span class="tok-punc">.</span><span class="tok-var">firstElementChild</span><span class="tok-punc">);</span></div>
<button class="cn-play" data-run="firstelchild" type="button">run</button>
<div class="cn-console"><span class="cn-cline" id="outFEC"><h2>Title</h2></span></div>
<p class="cn-note">Steps past the text node this time, straight to the real element.</p>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-childnodes')
|| document.querySelector('.infographic-childnodes');
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 nodeIds = ['nT0', 'nH2', 'nT1', 'nP', 'nT2'];
const nodes = {};
nodeIds.forEach(id => { nodes[id] = wrapper.querySelector('#' + id); });
function clearAll() {
nodeIds.forEach(id => nodes[id].classList.remove('is-hit', 'is-first', 'is-skip'));
}
const runners = {
childnodes: () => {
nodeIds.forEach(id => nodes[id].classList.add('is-hit'));
},
children: () => {
nodes.nH2.classList.add('is-hit');
nodes.nP.classList.add('is-hit');
nodes.nT0.classList.add('is-skip');
nodes.nT1.classList.add('is-skip');
nodes.nT2.classList.add('is-skip');
},
firstchild: () => {
nodes.nT0.classList.add('is-first');
},
firstelchild: () => {
nodes.nT0.classList.add('is-skip');
nodes.nH2.classList.add('is-first');
},
};
const outIds = { childnodes: 'outCN', children: 'outCH', firstchild: 'outFC', firstelchild: 'outFEC' };
wrapper.querySelectorAll('.cn-tab').forEach(tab => {
tab.addEventListener('click', () => {
wrapper.querySelectorAll('.cn-tab').forEach(t => t.classList.remove('is-active'));
wrapper.querySelectorAll('.cn-panel').forEach(p => p.classList.remove('is-active'));
tab.classList.add('is-active');
wrapper.querySelector('.cn-panel[data-panel="' + tab.dataset.tab + '"]').classList.add('is-active');
clearAll();
});
});
wrapper.querySelectorAll('.cn-play').forEach(btn => {
btn.addEventListener('click', () => {
const key = btn.dataset.run;
clearAll();
const out = wrapper.querySelector('#' + outIds[key]);
out.classList.remove('is-shown');
void out.offsetWidth;
setTimeout(() => runners[key](), 60);
setTimeout(() => out.classList.add('is-shown'), 350);
});
});
})();
</script>π§ Final Comparison Table
Property | What it Returns | Why |
|---|---|---|
| Text + Elements | Raw DOM nodes |
| Only Elements | Cleaner traversal |
| Text OR Element | First node (whitespace matters) |
| Only Element | Ignores text nodes |
β Rule of Thumb (VERY IMPORTANT)
βοΈ Use these in real projects:
children
firstElementChild
lastElementChild
nextElementSibling
previousElementSiblingβ Avoid these unless needed:
childNodes
firstChild
lastChildProperty | Includes Text Nodes | Includes Elements |
|---|---|---|
| β Yes | β Yes |
| β No | β Yes |
| β Yes | β Maybe |
| β No | β Yes |