#How a browser turns HTML into a page
AddedA browser never "reads" your HTML file as text. It goes through a fixed pipeline: bytes → characters → tokens → DOM tree. The bytes are decoded into characters using the page's encoding (that's what <meta charset="utf-8"> is for). The tokenizer, a state machine, turns the characters into tokens: a doctype, start tags with their attributes, end tags, text, comments. The tree builder takes those tokens one at a time and grows the DOM, keeping a "stack of open elements" that says where the next node goes. Only once there is a DOM (and a CSSOM from the stylesheets) can the browser lay out and paint anything; that later half is covered in the rendering pipeline.
The part that surprises people is that the HTML parser never fails. A JSON or XML parser stops at the first error; the HTML parser has a rule for every mistake. Since HTML5, those recovery rules are written down in the spec, step by step, so every modern browser builds the same DOM from the same broken markup. Before that, each browser guessed differently, and "works in IE, broken in Firefox" was often a parsing difference.
The mental model: the parser is a strict but forgiving copy editor. It never rejects your text, but it silently rewrites it to follow its rulebook, and the rewritten version is what your CSS and JavaScript see. That's why the browser's Elements panel (the DOM) can disagree with View Source (your markup), and why a selector like p > div can never match anything in a parsed HTML page.
.case { border: 1px solid #c7cfe0; border-radius: 6px; padding: 4px 10px; margin-bottom: 8px; }
.case p { margin: 2px 0; background: #e8f0ff; }
.case div { background: #fdeccf; }
.case table { border: 1px dashed #9aa5b8; }<div class="case" id="paras"><p>One<p>Two<div>A block</div></p></div>
<div class="case" id="links"><a href="#a">outer <a href="#b">inner</a> tail</a></div>
<div class="case" id="table"><table><tr><td>cell</td></tr>stray text</table></div>
<div class="case" id="tags"><b>bold <i>both</b> italic</i></div>- Paragraphs.
<p>One<p>Twohas no</p>, but a<p>start tag automatically closes an openp, so you get two siblings. Then<div>arrives whileTwo'spis open; apcan only hold phrasing content, so the parser closes thepfirst and puts thedivafter it. The final</p>now has no openpto close, so the spec says to create an empty one. Measured:<p>One</p><p>Two</p><div>A block</div><p></p>, threepelements and zero matches forp > div. - Links. A link can't contain a link. When the inner
<a>starts, the parser closes the outer one, soouteris in the first link,innerin the second, andtailis in no link at all (it renders as plain text in the live result). - Table. Text isn't allowed between table rows, so the parser "foster-parents" it:
stray textis moved to just before the table. It also inserts the<tbody>you left out. The DOM isstray text<table><tbody>…</tbody></table>, which is whytable > trdoesn't match in HTML buttable > tbody > trdoes. - Misnested tags.
<b>bold <i>both</b> italic</i>closesbwhileiis still open. The "adoption agency" algorithm closesi, closesb, then reopens a freshifor the rest:<b>bold <i>both</i></b><i> italic</i>. Both "both" and "italic" stay italic, just as you meant. - All four outputs come from the spec's recovery rules, not browser guesswork, so they're the same everywhere. The practical lesson: write valid markup anyway, because the "fixed" tree is often not the tree you wanted (the empty
padds a margin; thetailtext loses its link), and your CSS selectors run against the fixed tree.
The doctype has one job in modern HTML: it picks the rendering mode. <!doctype html> (any capitalisation) puts the page in standards mode. No doctype, or certain legacy doctypes, put it in quirks mode, where the browser imitates bugs of late-1990s browsers so old pages keep working: unitless lengths are accepted, tables don't inherit font sizes, percentage heights behave differently, and the box model of some elements changes. A third mode, limited quirks, only changes how images in table cells line up.
document.compatMode; // "CSS1Compat" (standards or limited quirks) or "BackCompat" (quirks)
const frame = document.createElement("iframe");
document.body.append(frame);
frame.contentDocument.write("<div style='width:120;height:30px'>box</div>"); // no doctype
frame.contentDocument.compatMode; // "BackCompat"document.compatModeis the quick check: the demo frame above reports"CSS1Compat"because the page has<!doctype html>. You can run the same line in DevTools on any site.- The test writes the same
divinto two fresh frames, one without a doctype and one with<!doctype html>. The first reports"BackCompat", the second"CSS1Compat". - In quirks mode
width:120(no unit) is read as120px: the box measures 120px wide. In standards mode it's an invalid declaration and is dropped, so the box takes the frame's full width: 284px (300px frame minus the default 8px body margin on each side). - Legacy doctypes are matched by their exact text. The HTML 4.01 Transitional doctype without a URL gives
"BackCompat"; with its URL it gives limited quirks, whichcompatModereports as"CSS1Compat". That's why you sometimes see old sites behave oddly even though they "have a doctype". - Accidental quirks mode usually comes from something printed before the doctype. Whitespace and comments are fine (the test's
<!-- built by CMS -->page is still"CSS1Compat"), but any other text, such as stray output from a server-side include, switches to quirks ("x<!doctype html>"is"BackCompat"). If layout looks inexplicably different between two pages, checkcompatModefirst.