Learning objectives
- Tell well-formed markup from valid markup, and explain why HTML needs both words
- Describe how HTML's rules were written as a DTD, and how the living standard writes them now
- Read an element's entry in the HTML standard: its categories, its contexts and its content model
- Predict what the parser builds from the classic content-model violations, and check the prediction in DevTools
- Run the Nu HTML Checker and html-validate, read their messages, and fit them into a code, validate, preview cycle
Well-formed and valid
Two questions can be asked of any markup. The first is about its surface: do the tags balance, are the attribute values quoted, does anything overlap? Markup that passes is well-formed. The second is about its meaning: is every element one the language defines, in a place the language allows, with attributes and values the language accepts? Markup that passes both tests is valid.
The two are easy to tell apart with a line of nonsense:
Every tag is closed, every element sits fully inside its parent, and the one attribute value is quoted. An XML parser would accept it without complaint. It is still not HTML: there is no Fizz, baz or blurp element in the language, so the line is well-formed and invalid.
In HTML the line between the two is blurrier than in XML, because HTML's surface grammar is forgiving on purpose. The end tags of p and li may be left out, so may the html, head and body tags altogether, and an attribute value without spaces or quotes in it needs no quotes. Markup that would be malformed in XML can be valid HTML. The opposite happens too: <p><div>hello</div></p> balances perfectly and is invalid, because a paragraph may only contain phrasing content. So this series uses well-formed loosely, for the tag-level grammar, and puts the weight on valid. The HTML standard itself prefers the word conforming; the two mean the same thing here.
Checking a document against the rules is called validation, and a program that does it is a validator. In HTML's case a validator is the only reader that ever enforces the rules. Lesson 01 called this the asymmetric contract: authors must write conforming documents, and browsers must accept anything. The rest of this lesson is about the strict half of that contract, the half that runs only when you run it.
From a DTD to a living standard
HTML has written its rules down in two very different ways.
The DTD era
From HTML 2.0 to HTML 4.01, HTML was formally an application of SGML, and its grammar lived in a Document Type Definition. The DTD declares each element, its attributes and what it may contain. If the DTD is a class, each document is an instance, and validation asks whether the instance conforms. Four declarations from the HTML 4.01 Strict DTD show how formal HTML once was:
After the element's name come two characters for its tags: - - means both tags are required, and - O means the end tag may be omitted, so an unclosed paragraph was legal long before HTML5. Then comes the content model. %inline; is a macro that expands to the inline elements plus text, so a paragraph holds zero or more inline things; a UL holds one or more LIs and nothing else. The last part of the A declaration, -(A), is an exclusion: no A may appear anywhere inside an A, however deep. The BUTTON line excludes links, form controls, forms and fieldsets in the same way. Most of the rules this lesson teaches were already there, as grammar.
The trouble was that only validators read the DTD. Browsers never parsed HTML as SGML. The standard's own account is blunt: "the only user agents to strictly handle HTML as an SGML application have historically been validators," and the confusion, "with validators claiming documents to have one representation while widely deployed web browsers interoperably implemented a different representation," wasted "decades of productivity."
The living standard
HTML5 cut the tie to SGML. The living standard says that while HTML's syntax "bears a close resemblance to SGML and XML, it is a separate language with its own parsing rules." There is no DTD for HTML today. The short <!doctype html> at the top of a page names no DTD at all; the standard keeps it only "for legacy reasons," to put the browser in standards mode, as lesson 02 explains.
The rules now live in prose. Every element's entry opens with a block stating its categories, where it may be used and what it may contain, and the parsing chapter states what a browser does with every possible input. Prose can say things a DTD cannot, such as that a button may have no descendant with a tabindex, or that headings may go down only one level at a time. Validators implement those rules in code.
Content categories: a type system
Rather than list every allowed parent and child pair, the standard groups elements into content categories and states each content model in terms of them. A paragraph's content model is "phrasing content." A section's is "flow content." An element can belong to several categories at once.
Read it as a type system. The categories are types, and a content model is a signature that says which types an element accepts as children. Phrasing content is a subtype of flow content: every phrasing element is also flow, so it can go anywhere flow is allowed, but not the reverse. Putting a div inside a p is a type error, exactly like passing a Shape where a Circle is expected. No compiler stops you; the browser runs the program anyway, after repairing it.
The standard defines seven main categories:
| Category | What it is | Examples |
|---|---|---|
| Metadata | Sets up the presentation or behavior of the rest of the document, or its relationship to other documents | title, base, link, meta, style, script, noscript, template |
| Flow | Most elements used in the body: the default category for content | p, div, ul, table, form, main, and every element in the categories below |
| Sectioning | Defines the scope of header and footer elements | article, aside, nav, section |
| Heading | Defines the heading of a section | h1 to h6, hgroup |
| Phrasing | The text of the document and the elements that mark it up within a paragraph | text, a, em, strong, span, code, img, button, input |
| Embedded | Imports another resource, or content from another vocabulary | img, picture, video, audio, iframe, canvas, svg, math |
| Interactive | Content specifically intended for user interaction | a with href, button, details, label, select, textarea, input unless hidden, audio and video with controls |
Two lessons from the diagram matter more than the rest. First, the gap between flow and phrasing is where most errors happen. A p, div, ul, table or heading is flow content and not phrasing, so none of them may go inside a paragraph, a span, an em or a button. Second, some elements belong to no category at all. A li, a tr, a td, an option, a dt and a dd exist only inside one kind of parent, and they are invalid anywhere else.
Palpable and script-supporting content
Two more categories cut across the seven. Palpable content is content a person can perceive: non-empty text, an image, a control. As "a general rule," an element that accepts flow or phrasing content should contain some, but the standard adds that this "is not a hard requirement," since an empty element may be a placeholder for a script to fill. The Nu HTML Checker says nothing about an empty <p></p> and only warns, "Empty heading," about an empty <h2></h2>.
Script-supporting elements are script and template. They render nothing, so the standard lets them appear in places that otherwise have strict models: a ul's content model is "zero or more li and script-supporting elements," so a template for a list item can sit inside the list it will be cloned into.
Reading an element's entry
You do not need to memorize the categories; you need to read an element's entry. Every element in the standard starts with the same block. Here is the one for p, slightly shortened:
Read the first three fields as a sentence about types. Categories says what type the element is: p is flow, not phrasing. Contexts says where it may appear: wherever flow content is expected. Content model says what type it accepts: phrasing content only. Put those together and the rule "no paragraph inside a paragraph" falls out without being stated anywhere: the inner p is flow, the outer one accepts phrasing, and flow is not phrasing.
Tag omission is the parser's side of the same rule. It lists the elements whose start tag ends an open paragraph, all of them flow content that is not phrasing, so leaving out </p> before them is legal. Note the exceptions at the end. Most of them, a, audio, del, ins, map and video, are the transparent elements of the next section.
Three more entries, read the same way. ul is flow, used where flow is expected, and its content model is "zero or more li and script-supporting elements," so text or any other element directly inside it is invalid. li's entry inverts the pattern. Its categories are "None," and its contexts are "inside ol elements, inside ul elements, inside menu elements." Its own content model is generous: flow content, so a list item may hold paragraphs, headings and whole nested lists.
button is flow, phrasing, interactive and palpable, plus several form categories, so it may sit inside a paragraph. Its content model is "phrasing content, but there must be no interactive content descendant and no descendant with the tabindex attribute specified." So a button's label may be marked up with em or an img, but not with a link, another button, or a div.
The entry for a adds something new. Its categories are flow, phrasing, palpable and, only "if the element has an href attribute," interactive. Its content model is "transparent, but there must be no interactive content descendant, a element descendant, or descendant with the tabindex attribute specified." Transparent needs a section of its own.
Transparent content models
A transparent element takes its content model from its parent. The standard's definition: the elements allowed in the transparent part are "the same elements as required in the part of the content model of the parent of the transparent element in which the transparent element finds itself." Put plainly, the content must be valid as if the transparent element were not there, apart from any extra rule of its own.
The Nu HTML Checker reports the second case as "Element div not allowed as child of element a in this context," and adds a note worth reading: "The a element has a transparent content model; its allowed content is inherited from its parent element." The a is not the problem. The heading around it is.
The transparent elements are a, ins, del, map, object, canvas and slot, plus audio and video, whose fallback content (what shows when the media cannot play) is transparent after any source and track children. Each exists to wrap content without changing what kind of content it is. ins and del mark an edit, and an edit may cover a few words or several paragraphs. map groups the clickable areas of an image map along with whatever content surrounds them. And an a may be a word inside a sentence or a whole card on a page.
That last point was a change. In HTML 4 the DTD declared A as inline only, so a link around a heading and a paragraph was invalid, though every browser rendered it. HTML5 made a transparent and so made the block link legal. The link, in the Linking section, follows the block link through its accessibility trade-offs and shows the nested-link repair in detail.
What the parser does with invalid markup
A content-model error never stops the browser, and the cases fall into two groups. In the first, the parser restructures the tree, so you get a tree you did not write. In the second, it builds the invalid structure exactly as written. Each claim below was checked in Chrome by parsing the snippet as a document; the demo after the list does the same live.
Errors the parser restructures
- A block inside a paragraph. In
<p>Intro <div>a block</div> after</p>, thedivstart tag closes the paragraph, so the paragraph holds only "Intro," the text " after" lands loose in the body, and the final</p>, with no paragraph open, creates a new, empty one. Aul, atableor a heading does the same (in no-quirks mode; quirks mode lets a table sit inside a paragraph). - A link inside a link. The parser never builds one. A second
<a>start tag while a link is open closes the first link. In a simple case the result is two links side by side, with any text after the inner link left outside both. - A button inside a button, a form inside a form. A second
<button>start tag closes the open button, giving two buttons in a row. A<form>start tag inside an open form is ignored altogether, and its fields join the outer form. - Content in the wrong place in a table. Text or an element such as
pthat appears directly in atable, outside any cell, is moved out and inserted just before the table. The standard calls this foster parenting. Its own example of an error with an "unintuitive" result is<table><hr>, which puts thehrbefore the table. Rows written directly in a table also get atbodythe source never mentioned.
The first case is worth seeing as a tree, because it is common and its repair changes three things at once.
p { margin-block: 1rem } now spaces two paragraphs, and p > div matches nothing.Errors the parser builds as written
- A
divor text inside aul. The tree holds thedivor the text as a child of the list, just as written. There is no rule that moves it. - A
buttoninside ana, or a link inside a button. Both elements stay nested in the tree, and the browser has to decide which one a click activates. - A
lioutside any list. The parser does not invent a list for it. Thelisits in its parent, styled as a list item by the browser's default style sheet, but it belongs to no list, and assistive technology has no list to announce. - A
divinside aspan, or inside anem. A block start tag closes an open paragraph, but not an openspanorem: those stay open and simply contain the block.
The second group is the more dangerous, because nothing looks wrong: DevTools shows the tree you wrote, and the page looks right. Only the validator objects.
Tag soup and the soup parse
"Tag soup" is the old name for markup like this: tags unclosed, overlapping or unknown, in pages that work anyway. The course slides ask whether that is HTML's strength or its weakness, and the answer is both. It is why a page from 1995 still renders. It is also why, for most of the web's history, the same broken page built different trees in different browsers, each with its own error handling, so a script could work in one browser and fail in the next.
HTML5's answer was not to forbid soup. It was to define it. The parsing chapter of the standard specifies, for every possible input, which tree comes out; in its own words, "the error handling for parse errors is well-defined." Today every major engine implements that one algorithm; the Nu HTML Checker's parser, by Henri Sivonen, is even the source Firefox's HTML parser is translated from. The course's author calls this consistent "soup parse" the single largest win of the HTML5 effort: "at least we have predictability in the face of junk markup." The snippets in the demo above build identical trees in Chrome and in WebKit, the engine behind Safari.
Predictable is not the same as correct, and it is not the same as strict. HTML5 also made a deliberate return to loose valid syntax:
The Nu HTML Checker reports no errors for it. Its one remark is a warning to add a lang attribute to the html start tag, which is a good reason to write that tag after all. Nothing here relies on error recovery: the omitted tags and missing quotes are part of the grammar. Writing this way is a style choice, and most teams close and quote everything because it reads and diffs better. Hold on to the difference between loose, which the grammar allows, and invalid, which only the error handling copes with. Both render. Only one means what it says.
The deeper problem is that the decision is rarely made on purpose. The slides picture a developer between two voices: "who cares, it will work in the browser," and HTML that "might be work, but it parses fast, helps SEO and is a11y friendly." The developer, keen to get to the JavaScript, never decides, and the soup accumulates. A validator in the workflow makes the decision once.
Why validity still matters
If every browser builds the same tree from the same soup, why bother being valid? The standard asks the question itself, and answers that "interoperability ... is not the only goal of document conformance requirements." Four reasons carry most of the weight.
The tree you get may not be the tree you meant. The repair follows the parser's rules, not your intent. The paragraph that became two and the text fostered out of a table are correct output for the input, and wrong output for the author.
Everything downstream reads the tree. CSS selectors match the tree, so p > div or table > tr silently match nothing. Scripts query the tree, so querySelector returns null for an element that is plainly in the source. The accessibility tree is computed from it, so a div in a list is something that is neither an item nor outside the list, and screen readers differ on how they announce a button inside a link. Search engines and AI agents read it too. The Accessibility page follows that thread through the course.
Invalid structures have no good behavior to rely on. The standard explains several of its rules this way. Interactive content cannot be nested because "the default behavior of such nesting interactive elements would be highly confusing to users"; a ul may contain only lis because if it contains anything else, "it's not clear what was meant." When the meaning is unclear, each browser, screen reader and crawler picks its own interpretation, and they need not agree.
Valid markup is easier to maintain. In a codebase that validates cleanly, a new error stands out the day it appears. A codebase with three hundred errors hides the three hundred and first.
The slides warn against jumping to conclusions, though. Famous home pages rarely validate. On the day this lesson was written, the version of Google's home page that the Nu HTML Checker received produced eight errors, among them the obsolete <center> element, and the HTTP Archive's Web Almanac 2024 found <center> and <font> each on 4.5% of the mobile pages it measured. Did those authors do something on purpose? Sometimes a team trades validity for bytes or an old browser, knowing the cost. The point is that the trade should be a decision, not an accident nobody looked at.
The validator as a linter
Treat the validator as a type checker or linter: a fast, automatic reader that runs before your code reaches anyone else. Two are worth knowing.
The Nu HTML Checker
The Nu HTML Checker is the W3C's validator for current HTML, at validator.w3.org/nu. It checks a page by address, by uploaded file or as pasted text. Its parser implements the standard's algorithm, so it reports parse errors exactly where a browser's error handling kicks in, then checks the tree against the content models and attribute rules, and checks the CSS too. The same checker runs offline as vnu.jar, as pre-built binaries, and as the npm package vnu-jar, for build scripts and continuous integration. (The older Markup Validation Service at validator.w3.org/check still checks the old DTDs; given an HTML5 document, it points to the Nu checker.)
An error is a conformance failure; a warning is advice, such as "Section lacks heading." A few errors are fatal: text directly inside a table stops the checker with "Cannot recover after last error. Any further errors will be ignored." The standard requires a checker to report at least one parse error, not all of them, so after a fatal error, fix it and check again.
html-validate in the editor and in CI
html-validate is a separate project, an offline validator written in JavaScript and installed from npm. Its own parser is strict and makes no corrections. It checks fragments as well as documents, including markup inside framework components, and runs in editors and CI with no network connection. Its recommended preset adds rules beyond the standard, such as a type on every button; its standard preset sticks to conformance.
The two overlap without agreeing. On the practice page below, both find the stray </p>, the button in a link, the duplicate id, the paragraph in a list and the missing alt; only Nu finds the bad datetime value and the skipped heading level; html-validate adds the button with no type, legal but a common bug, and a paragraph closed implicitly by a list, which is valid HTML that the recommended preset flags as a style choice. Given text directly in a table, html-validate's standard preset reports nothing.
| Nu HTML Checker | html-validate | |
|---|---|---|
| Rules | The standard: parse errors, content models, attribute values, obsolete features, plus CSS | Content models and many standard rules, plus optional style and accessibility rules |
| Parser | The standard's algorithm, the same one browsers use | Its own strict parser, no error recovery |
| Input | Whole documents, by address, file or paste | Documents or fragments, including component templates |
| Best place | Before you publish, and as the reference when tools disagree | In the editor and on every commit |
Neither tool checks meaning. A page built entirely of divs passes both, and so does a link whose text is "click here." Validation catches type errors, as a compiler does, and leaves meaning to you and the next lesson.
Reading the messages
A message names what the validator saw, at the point where it noticed, which is often after the mistake. Three habits help.
- Fix the first error first, then check again. One unclosed element can cause a cascade of later messages that all disappear together.
- Look one step back from the line it names. For
<p><div>…</div></p>, Nu says "Nopelement in scope but apend tag seen," and points at the</p>. The mistake is thedivthat closed the paragraph before it. - Translate the message into a content-model question. "Element
divnot allowed as child of elementulin this context" means: look upul's content model. Every content-model message is that lookup in disguise.
Code, validate, preview
The course's tooling advice describes a loop: write, validate, preview, repeat. Keep it fast enough to run constantly, using, in the slides' words, "rough sand paper as opposed to finishing paper" while you build:
- While writing, let an editor integration of html-validate underline errors as you type, the way a spelling checker does.
- On every commit, run a validator over the built pages in CI and fail the build on errors, not warnings. A failed build is the one reminder nobody forgets.
- Before publishing, check the real pages with the Nu HTML Checker, by address: it sees what the browser sees.
- Preview in more than one browser. The tree will match; the rendering, the fonts and the form controls may not.
A tool chain, the slides warn, "should be supportive and appropriate to the level you are at." For a course project, the Nu checker and a habit are enough.
Try it: find the seven errors
The page below looks fine in every browser. It holds seven deliberate errors, each a rule from this lesson.
- Before running anything, read the source of validator-practice.html (View Source, or the demo's own tab) and list every error you can find by eye, with the rule each one breaks.
- Check the page with the Nu HTML Checker at validator.w3.org/nu: choose "address" and paste
https://cse134.site/structure/html/demos/validator-practice, or paste the source as text. Compare its report with your list. Which did you miss, and which did you find that it does not report? - For the stray
</p>error, open DevTools on the practice page and find the empty paragraph the parser made. Then explain why the message points at the end tag and not at the list. - Fix every error in a copy of the page and check again until the report is empty. Then open "Show the answers" at the bottom of the page and compare your fixes with the suggested ones.
- Run your own most recent project through the checker. Pick one error, work out the content-model rule behind it, and predict what tree the browser built. Check the prediction in DevTools.
If you have Node installed, run npx html-validate validator-practice.html on a saved copy as well, and compare its list with Nu's. The differences are the subject of the table above.
Summary
| Concept | Key points |
|---|---|
| Well-formed and valid | Well-formed is the surface grammar; valid adds the rules for which element goes where, with which attributes. HTML's grammar allows omitted tags and unquoted values, so loose markup can be valid. The standard's word is conforming. |
| The DTD era | HTML 2.0 to 4.01 were SGML applications defined by DTDs: - O for an omissible end tag, (LI)+ for a list's content, -(A) to exclude nested links. Only validators parsed HTML as SGML; browsers never did. |
| The living standard | No DTD. Each element's entry states its categories, contexts and content model in prose, and the parsing chapter defines the tree for every input. The doctype remains for legacy reasons. |
| Content categories | Metadata, flow, sectioning, heading, phrasing, embedded, interactive; plus palpable and script-supporting. Overlapping sets, read like types: phrasing is a subtype of flow. li, tr, td, option, dt, dd have no category and live only in their parents. |
| Reading an entry | Categories: what it is. Contexts: where it goes. Content model: what it accepts. |
| Transparent | Content must be valid as if the element were not there. a, ins, del, map, object, canvas, slot, and the fallback in audio and video. a adds: no interactive content, no a, no tabindex inside. |
| Restructured errors | Block in p closes it; nested a or button closes the outer; nested form is ignored; misplaced table content is foster-parented before the table. |
| Errors built as written | div or text in ul, button in a, li outside a list, div in span. Nothing looks wrong; only a validator says so. |
| Tag soup | HTML5 defined error handling, so every browser builds the same tree from the same soup. Predictable is not correct. Loose valid syntax is a style choice; invalid markup is an accident. |
| Why bother | The repaired tree may not be the one you meant, and CSS, scripts, assistive technology and crawlers all read the tree. Invalid structures have no reliable meaning. |
| The validators | Nu HTML Checker: the reference, with the browsers' parser. html-validate: offline, strict, for the editor and CI. Neither checks meaning. Fix the first error first, and read each message as a content-model lookup. |
Next steps
This lesson ends the Principles group: HTML as a language, the document, its syntax, and now its type system and the tool that checks it. A valid document can still say very little, though. Lesson 05, Landmarks and sectioning, starts the Semantics group with the question validation cannot answer: of all the valid elements, which one says what this part of the page actually is? The parser's side of this lesson, the tokenizer and tree construction behind every repair shown here, is covered in depth in Parsing HTML into the DOM, and CSS's own, quite different, forgiving parser in CSS Basics.