HTML Validation: W3C Standards, Common Errors and Fixes
Learn what HTML validation checks, how browsers repair bad markup, and how to fix the most common W3C and WHATWG conformance errors.

Your page looks fine in Chrome, Firefox and Safari, so the HTML must be fine, right? Not necessarily. Browsers are built to forgive. They quietly repair missing tags, reorder misplaced content and shrug at attributes they do not recognise. That forgiveness hides real defects: a broken heading outline, a form that submits twice, a duplicate ID that breaks a label, a table that renders in an order you did not write. This guide explains what HTML validation is, what it is not, how the standard and the checkers fit together, and how to read and fix the most common messages. It is written for front-end developers, CMS editors, QA engineers and anyone who maintains templates. You will learn how browsers recover from bad markup, how to read validator output, a catalogue of frequent errors with before and after fixes, how to validate fragments and server-rendered output, and how to add validation to a build pipeline.
What is HTML validation?
HTML validation is the automated checking of a document against the rules of the HTML language. A validator reads your markup, builds a model of it and reports every place where the markup breaks a rule: an element in a position where it is not allowed, an attribute with a value outside its permitted set, a required attribute that is missing, an end tag with no matching start tag.
The proper term for what a validator checks is conformance. The HTML specification defines two audiences with different rules. Conformance requirements for authors say what a document may contain. Requirements for user agents, meaning browsers, say how to parse any input at all, including broken input, and what tree to build from it. A document that follows the author rules is conforming. A document that breaks them is non-conforming, and a browser will still render it, because the parsing rules are defined for every possible byte sequence.
That split is the key idea of this whole article. Validation answers the question "does my markup follow the author rules?" It does not answer "does it render?", "is it accessible?" or "does it rank well?". Those are separate questions, and we will come back to each of them.
What a validator does not do
A validator is a syntax and structure checker. It generally does not:
- judge whether your element choices are meaningful, for example whether a
<div>should have been a<button>; - test colour contrast, keyboard behaviour or screen reader output;
- fetch your CSS or JavaScript and check them (though some tools lint these separately);
- confirm that links point to real pages or that images exist;
- tell you whether the page works in an older browser.
Treat a clean validation result as evidence that the markup is well-formed and rule-abiding, not as a quality certificate.
A short history: SGML, HTML 4, XHTML and the Living Standard
Early HTML was described as an application of SGML, a general markup meta-language. HTML 4 shipped with formal document type definitions, and a validator could check a page against the DTD named in its doctype declaration. That is why older pages carry long doctype lines naming a public identifier: the doctype told the validator which DTD to load.
XHTML later reformulated HTML as XML. XML parsers must stop at the first well-formedness error, so XHTML promised strict, machine-friendly markup. In practice most XHTML pages were served as regular HTML anyway, where browsers applied their usual error recovery, so the strictness was mostly theoretical. The slash in <br /> is a leftover from that era.
HTML5 changed the approach. Instead of being defined by a DTD, HTML became defined by a detailed parsing algorithm plus a set of content rules written in prose. Today the specification is maintained as a continuously updated document called the HTML Living Standard, published by the WHATWG, and you can read it at html.spec.whatwg.org. The W3C also published HTML specifications over the years, and the relationship between the two organisations has changed more than once. I will not try to give dates or recount the politics here, because details differ between sources. The practical point is that the WHATWG Living Standard is the document that browser vendors implement and track, so it is the one to consult when a rule is in doubt.
Validators evolved alongside the language. The long-running W3C Markup Validation Service, at validator.w3.org, is the best-known public checker. The tool behind its HTML checking today is the Nu Html Checker, often abbreviated "vnu", a validator that implements the rules of the living standard rather than loading DTDs. The general idea of such tools is described in the Wikipedia article on markup validators. You can run the Nu checker as a Java program locally, which is useful for continuous integration, and several community tools reimplement subsets of the rules in other languages.
How browsers parse HTML: forgiveness by design
To understand why "it renders" is not "it is valid", you need to see what a browser does with bad input. The HTML parsing algorithm is not a loose set of guesses. It is a specified state machine, and the specification lists named parse errors with defined recoveries. Every conforming browser follows the same recovery, which is why most invalid pages still look the same everywhere.
A few recovery behaviours matter most in practice.
Implied tags and omitted end tags
Some elements can legally omit their start or end tags, and the parser inserts them. You never need to write <html>, <head> or <body> for the parser to create them. A <p> is closed automatically when the next block-level element begins. List items close when the next <li> appears. These omissions are conforming in the right places, which is a trap: because omission is sometimes legal, it is easy to omit tags where it is not.
The paragraph that closes itself
A <p> element cannot contain a <div>. When the parser meets a <div> start tag while a <p> is open, it closes the paragraph first. Here is what that does, measured with the parse5 library, an implementation of the specification's parsing algorithm. The input was:
<p>One<div>Two</div></p>
The serialised tree came out as:
<p>One</p><div>Two</div><p></p>
The <div> ended up as a sibling, and the trailing </p> produced a new, empty paragraph, because a stray end tag for p creates an empty p element. You wrote one paragraph and the browser built two, one of them empty. If CSS gives paragraphs margin, you now have an unexplained gap.
Foster parenting in tables
Table content has strict rules. Text or elements that do not belong between table parts cannot stay there, so the parser moves them out. The specification calls this foster parenting: misplaced nodes are inserted before the table instead. The same parse5 run on <table><tr><td>cell</td></tr><div>stray</div></table> produced:
<div>stray</div><table><tbody><tr><td>cell</td></tr></tbody></table>
Two things happened. The <div> jumped out in front of the table, so the text you wrote after the row now appears above it. The parser also inserted a <tbody> that you never wrote. The tbody insertion is harmless and fully legal, but the foster-parented <div> is a visible bug that no browser warns you about.
Misnested formatting elements
Inline formatting elements such as <b> and <i> get special treatment. When tags overlap, as in <b>bold<i>both</b></i>, the parser runs what the specification calls the adoption agency algorithm, which restructures the tree so that the elements nest properly. The output from parse5 was <b>bold<i>both</i></b>. That is the result you probably wanted, which is exactly why this kind of error survives for years. In more complicated cases, the algorithm reopens formatting elements in later content, so bold or italic styling can leak into paragraphs you did not intend.
Why browsers do this
The web has always contained broken pages, and early browsers rendered them anyway. Once authors depended on that tolerance, removing it would have broken old content. The specification therefore captured the common recovery behaviours so that every browser produces the same tree. This is good for users and a hazard for authors. Nothing in the rendered page tells you that the parser worked around a mistake.
Rendering is not conformance
Putting these together:
- Renders correctly means the tree the parser built produced the visuals you expected, on the browsers you tried.
- Valid means the source text you wrote follows the author rules, so the parser had nothing to repair.
A page can render correctly and be invalid, as the misnested <b> and <i> above. A page can also be valid and render badly, because validity says nothing about your CSS. The two checks are complementary, and the rest of this article shows how to use validation to find the problems that rendering hides.
How validators work and how to read their output
Validators run in roughly three phases. First they parse the input using the same tokenizer and tree-construction rules a browser uses, noting every place where the parser had to recover. Second they walk the resulting tree and check content models: what each element may contain, which parents it may have, which attributes it allows and which values are legal. Third they add cross-cutting checks, such as duplicate IDs, references to IDs that do not exist, and some ARIA rules.
Errors, warnings and info
Most validators separate messages into levels, and the vocabulary is similar across tools.
| Level | Meaning | Typical examples | What to do |
|---|---|---|---|
| Error | The markup breaks a conformance rule | Stray end tag, duplicate ID, element not allowed here | Fix it. |
| Warning | Allowed by the rules but likely to cause trouble | Missing lang, a section without a heading, a questionable construct |
Read it and decide. Fix unless you have a reason. |
| Info | A note about the result or the checking process | Notices about the document type or encoding | Read for context. |
The Nu checker, for example, reports errors, warnings and informational messages, and the severity of a particular message can differ between tools. Do not assume a rule that one tool calls a warning is optional in another. Always check the message text and the rule it comes from.
Cascading errors
One real mistake often produces many messages. The first error changes the parser state, so everything after it is read in a different context. Fix errors from the top of the file downward, and re-run after each batch. In our sample below, a single unclosed tag generated several unrelated-looking messages further down.
Line and column numbers
Messages carry a position. Treat it as a pointer to where the tool noticed the problem, which is not always where the mistake is. For a missing end tag, the report usually points at the place where the tool realised the element must have ended, perhaps lines later.
The most common errors and how to fix them
The examples below come from sample files I validated with the html-validate package, version 11.18.0, using its recommended preset. Message wording and rule names are specific to that tool. The Nu checker words the same problems differently, so use the examples as a map of the problem, not as a script of the text you will see. Every before and after fix was validated.
First, the sample document that produced the output. Here is the bad file, with line numbers matching the messages that follow.
<html>
<head>
</head>
<body>
<img src="a.png">
<div id="x"><p>One<div>Two</div></p></div>
<div id="x"></div>
<a href="/a"><a href="/b">nested</a></a>
<center>Old</center>
<input type="text" disabled="false">
<ul><li>ok</li></ul>
<button type="button"><a href="/c">Go</a></button>
<br/>
</body>
</html>
And the output of npx html-validate bad.html:
1:2 error <html> is missing required "lang" attribute element-required-attributes
2:2 error <head> element must have <title> as content element-required-content
5:2 error <img> is missing required "alt" attribute wcag/h37
6:14 error Element <p> is implicitly closed by adjacent <div> no-implicit-close
6:34 error Stray end tag '</p>' close-order
7:10 error Duplicate ID "x" no-dup-id
8:15 error <a> element is not permitted as a descendant of <a> element-permitted-content
9:2 error <center> is deprecated: use CSS instead deprecated
9:2 error <center> element is not permitted as content under <body> element-permitted-content
10:20 error Attribute "disabled" should omit value attribute-boolean-style
10:30 error Attribute "disabled" has invalid value "false" attribute-allowed-values
12:24 error <a> element is not permitted as a descendant of <button> element-permitted-content
13:4 error Expected omitted end tag <br> instead of self-closing element <br/> void-style
Thirteen messages, one run. The sections below take them in groups.
1. Missing doctype
Problem. Without a doctype, browsers switch to quirks mode, an emulation of old, inconsistent layout behaviour. Box sizing, table inheritance and some other details differ from standards mode.
Before:
<html lang="en">
<head><title>Page</title></head>
<body>...</body>
</html>
After:
<!DOCTYPE html>
<html lang="en">
<head><title>Page</title></head>
<body>...</body>
</html>
The HTML doctype is short and case-insensitive for the parser. In my run, the html-validate recommended preset did not flag the missing doctype in bad.html, while it did complain about a lowercase <!doctype html> through a style rule that wants uppercase. That is a style preference of that tool, not a conformance rule. The Nu checker reports a missing doctype as an error, as far as I know, and a browser reports nothing, so check for it yourself. If a page looks subtly different from its siblings, a missing doctype is the first thing to rule out.
2. Missing title and missing lang
Every document needs a <title> inside <head>, and a lang attribute on <html> is strongly recommended. In my sample, both were reported:
1:2 error <html> is missing required "lang" attribute
2:2 error <head> element must have <title> as content
Before:
<html>
<head></head>
After:
<html lang="en">
<head>
<title>Pricing | Example Co</title>
</head>
The title appears in browser tabs, bookmarks and search results, and assistive technology announces it when a page loads. The lang attribute lets screen readers pick the right pronunciation rules, helps browsers choose hyphenation and quotation marks, and supports translation features. Use a valid language tag, such as en, en-GB or pt-BR.
3. Unclosed and misnested tags
This is the most common structural error, and it generates cascades. Consider:
<p>Tom <b>bold<i>both</b></i></p>
In my second sample, a single misnested <b> and <i> pair produced messages that included Unclosed element '<i>' and End tag '</b>' seen but there were open elements, and further messages for the closing tags of enclosing elements on later lines. One typo, a pile of output. Fix:
<p>Tom <b>bold<i>both</i></b></p>
The rule is simple. Close elements in the reverse order you opened them. When a validator reports an end tag far from the real mistake, scan upward for the first element that was never closed.
4. Implicitly closed elements and stray end tags
Our <p>One<div>Two</div></p> example gave two messages: the paragraph was implicitly closed by the adjacent <div>, and the final </p> was a stray end tag. This matches the parser behaviour shown earlier.
Before:
<div id="x"><p>One<div>Two</div></p></div>
After:
<div id="x"><p>One</p><div>Two</div></div>
If you wanted the second block inside the paragraph visually, use <span> with CSS display rules, or restructure so that the paragraph holds only phrasing content. Phrasing content means text and inline elements such as <span>, <a>, <em> and <img>.
5. Duplicate IDs
An id must be unique within a document. Duplicates cause getElementById to return the first match only, make <label for> bind to the wrong control, and break fragment links and ARIA references that point to an ID.
Before:
<div id="x"></div>
<div id="x"></div>
After:
<div id="x"></div>
<div id="y"></div>
Duplicates mostly come from reused components. A card component with a hard-coded id="title" repeated in a list is typical. Generate IDs from record keys or counters, or use classes when you do not need a unique reference.
6. Invalid nesting: block in inline, anchor in anchor, form in form
The content model of each element says what it may contain. Several violations come up constantly.
Interactive content inside interactive content. An <a> cannot contain another <a>, and an <a> cannot sit inside a <button>. The validator said:
8:15 error <a> element is not permitted as a descendant of <a>
12:24 error <a> element is not permitted as a descendant of <button>
Browsers handle nested anchors by closing the first one when the second starts, so the layout you intended does not appear. Nested interactive controls also confuse keyboards and screen readers, since the focus order and the accessible name become ambiguous.
Before:
<button type="button"><a href="/c">Go</a></button>
After, choose one meaning:
<a href="/c" class="button">Go</a>
or, if it triggers an action instead of navigation:
<button type="button">Go</button>
Form in form. My second sample had one form inside another. The validator reported <form> element is not permitted as a descendant of <form>. The parser ignores a nested form start tag, so the inner fields silently join the outer form. Typical cause: a template that includes a search form inside a page-wide form wrapper, such as the ASP.NET-style single form around the whole page. Fix by separating the forms, or by using the form attribute on a control to associate it with a form elsewhere in the document.
Block elements inside inline ones. <span><div>block</div></span> was reported as <div> element is not permitted as content under <span>. Change the outer element to a <div>, or the inner one to a <span>. One exception worth knowing: an <a> element may wrap block-level content when its parent allows flow content, so a clickable card can be a single <a>, as long as no interactive elements are placed inside it.
List items outside a list. <li> requires a <ul>, <ol> or <menu> parent. The messages were <li> element is not permitted as content under <body> and <li> element requires a <ul>, <ol>, <menu> or <template> element as parent.
Before: <li>orphan</li>
After: <ul><li>orphan</li></ul>
7. Obsolete and deprecated elements and attributes
Elements such as <center> and <font> are obsolete. Browsers still render them, so they survive in old CMS content and email templates, but they are not conforming. Our run reported <center> is deprecated: use CSS instead and the same for <font>.
Before:
<center>Old</center>
<font color="red">old</font>
After:
<div class="centered">New</div>
<span class="warning">new</span>
.centered { text-align: center; }
.warning { color: red; }
The same goes for presentation attributes such as align, bgcolor, border on images, cellpadding and valign. Move them into CSS. In my test, html-validate with its recommended preset did not flag align="center" on an anchor, while the specification lists align on most elements as obsolete, and I would expect a specification-based checker such as the Nu checker to report it. Different tools cover different subsets of the rules, which is a good reason to understand the rule rather than rely on a single tool's silence.
8. Unescaped ampersands and angle brackets
A raw & in text can be read as the start of a character reference, and a raw < can be read as the start of a tag. The rule is to write & and <. The tool reported:
10:8 error Raw "&" must be encoded as "&"
8:6 error Raw "<" must be encoded as "<"
Before:
<p>Tom & Jerry</p>
<p>5 < 6</p>
<a href="/search?q=a&b=c">Search</a>
After:
<p>Tom & Jerry</p>
<p>5 < 6</p>
<a href="/search?q=a&b=c">Search</a>
The third case is the one that bites most often. In an attribute value, &b looks like the start of a named character reference. The parsing rules are lenient in attributes when the text after the name is = or a letter or digit, so such a URL usually survives, but the same text in element content, such as ©=1, can turn into a copyright symbol. Always encode the ampersand in URLs inside HTML attributes, so you never have to remember which case applies. Anything you print from user input into HTML must be escaped as a matter of security as well as validity, a point we return to later.
9. Bad attribute values
Attributes with enumerated or typed values must hold legal values. Our sample showed:
15:32 error Attribute "width" has invalid value "10px"
10:30 error Attribute "disabled" has invalid value "false"
width on an image accepts a number of CSS pixels without a unit, so width="10" is right and width="10px" is not. For other sizing, use CSS.
Before:
<img src="a.png" alt="" width="10px">
After:
<img src="a.png" alt="" width="10" height="10">
Including both dimensions also lets the browser reserve space before the image loads, which reduces layout shift.
10. Invalid boolean attributes
A boolean attribute is true when it is present and false when absent. Its value, if any, must be empty or the attribute's own name. disabled="false" does not disable nothing, it still disables the control, because the attribute is present. Validators flag it twice, as the sample shows.
Before:
<input type="text" disabled="false">
<input type="checkbox" checked="checked">
After:
<input type="text">
<input type="checkbox" checked>
In the second line, checked="checked" is technically conforming, and the tool reported Attribute "checked" should omit value because its style rule prefers the short form. Treat that as a style message. The first line is a genuine bug: to enable the field, remove the attribute entirely, and in JavaScript set the property instead.
11. Missing alt text
Every <img> needs an alt attribute. The validator message was <img> is missing required "alt" attribute. The attribute can legitimately be empty for decorative images, as alt="" marks the image as presentational so screen readers skip it. A missing attribute is different: some screen readers then read out the file name.
Before:
<img src="a.png">
After, informative image:
<img src="a.png" alt="Company logo: a blue compass">
After, decorative image:
<img src="divider.png" alt="">
Writing good alt text is a skill that a validator cannot check. It confirms the attribute exists, not that the description is useful.
12. Void elements and the trailing slash
Void elements such as <br>, <img>, <input>, <meta> and <link> have no end tag and no content. In HTML a trailing slash on them is permitted but meaningless. The slash does not make <div /> self-closing, a frequent source of bugs for people who come from XML or JSX. A self-closing <div /> is parsed as an opening tag, so everything after it ends up inside it.
My run reported Expected omitted end tag <br> instead of self-closing element <br/> under a style rule. As far as I know, the Nu checker also notes that the slash on a void element has no effect, so this is largely a house-style decision. Pick one style and keep it consistent.
Before:
<br/>
<div />
After:
<br>
<div></div>
13. Charset declaration position
The character encoding declaration, <meta charset="utf-8">, must appear within the first 1024 bytes of the document, because browsers sniff the encoding before they parse the page. It is conventionally the first element in <head>, ahead of the title and any long scripts or inline styles. If it appears too late, a browser may guess a different encoding and show garbled characters, such as é for é.
Before:
<head>
<title>Café menu</title>
<link rel="stylesheet" href="a.css">
<!-- many lines of other content -->
<meta charset="utf-8">
</head>
After:
<head>
<meta charset="utf-8">
<title>Café menu</title>
<link rel="stylesheet" href="a.css">
</head>
In a short test file where the meta element came after a stylesheet link but well inside the first kilobyte, html-validate reported nothing, which is correct, as the rule concerns bytes, not order. The real check needs a long head to trigger it. Also make sure your server sends a Content-Type header with a matching charset, since the header takes precedence over the meta element.
14. Table structure errors
Tables have a strict content model: <caption>, then column groups, then <thead>, <tbody> and <tfoot> or direct <tr> rows, and rows contain only <td> and <th>. Anything else is non-conforming, and the parser relocates it via foster parenting, as shown earlier.
Before:
<table>
<tr><td>cell</td></tr>
<div>stray</div>
</table>
The tool reported <div> element is not permitted as content under <table> and a style message, Prefer to wrap <tr> elements in <tbody>.
After:
<table>
<caption>Quarterly totals</caption>
<thead><tr><th scope="col">Quarter</th><th scope="col">Total</th></tr></thead>
<tbody><tr><td>Q1</td><td>120</td></tr></tbody>
</table>
The stray <div> is gone because the content belongs either in a cell or outside the table. Use tables for tabular data, with <th> cells and scope to describe headers, and use CSS grid or flexbox for layout.
15. Invalid ARIA usage
ARIA attributes adjust what assistive technology is told about an element. Misusing them is worse than omitting them. The first rule of ARIA is that a native element with the right semantics beats an ARIA role on a generic one. Typical errors:
- Redundant roles.
<button role="button">was reported asRedundant role "button" on <button>. - Attributes on the wrong element.
<div aria-labelledby="nothere">was reported as"aria-labelledby" cannot be used on this element, since a bare<div>with no role does not support naming. - Invalid role names. A role such as
role="banana"is not in the ARIA specification. My test of that with the recommended preset produced no message, so do not assume every tool catches it. A specification-based checker is expected to flag unknown roles, though I did not verify that here. - References to IDs that do not exist.
aria-labelledby="nothere"points nowhere. Even when no message appears, the label does not work.
Before:
<button role="button">Save</button>
<div aria-labelledby="nothere">Content</div>
After:
<button type="button">Save</button>
<section aria-labelledby="sec-title">
<h2 id="sec-title">Settings</h2>
...
</section>
Note also the second message in the button line: <button> is missing recommended "type" attribute. A <button> inside a form defaults to type="submit", so a button you meant as an action will submit the form. Always state the type.
16. Everything fixed
After applying the fixes to the first sample, the final file looked like this and passed with the tool's default settings:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sample</title>
</head>
<body>
<img src="a.png" alt="Logo">
<div id="x"><p>One</p><div>Two</div></div>
<a href="/a">link</a>
<ul><li>ok</li></ul>
</body>
</html>
Running npx html-validate good.html on this file with an uppercase doctype gives no output and exit code 0. When I first wrote the doctype as lowercase <!doctype html>, the tool objected with DOCTYPE should be uppercase, a style preference. Both spellings are conforming.
Real-world use cases
Validation earns its keep in specific situations. These are the ones where I see teams get the most value.
Template and component libraries. A bad partial is included on every page. One unclosed <div> in a header template shifts the layout of the entire site, and one hard-coded ID in a card component breaks every list. Validating the rendered output of the template catches defects that a code review of the template source often misses, since the template language hides the final markup.
CMS and rich text content. Editors paste from word processors, email clients and other sites. The result is frequently full of <font> tags, empty paragraphs, nested spans and unescaped characters. Validating a sample of live pages tells you what the editor actually produces and whether you need a sanitising step.
Email-adjacent and legacy markup. Legacy pages, and HTML email in particular, rely on old table-based layout and presentation attributes that are not conforming. In those contexts, validation is a lens for finding accidents, not a goal, and you will accept a list of known exceptions.
Migrations and redesigns. Moving from one framework to another, or from server-rendered pages to a static generator, changes the output in ways no one reads line by line. A validator run before and after, compared with a diff, shows exactly what changed in structure. Our guide to comparing files and code diffs explains how to read such a comparison.
Accessibility work. Many accessibility defects are markup defects: missing labels, duplicate IDs referenced by ARIA, headings used for styling and tables without header cells. A validator will not find all of them, but it finds the mechanical ones early and cheaply.
Teaching and code review. A validator gives objective, repeatable feedback. It ends arguments about whether a closing tag is "really needed".
Before minifying. Minifiers assume the input is sound. If you remove optional tags or collapse whitespace on top of broken markup, the result can be hard to debug. Validate first, then minify. Our HTML minification guide covers what a minifier may safely remove.
Validating fragments, templates and server-rendered output
A validator checks what it is given, and what you are given is often not the page a user sees.
Validate the rendered HTML, not the template
Template files contain placeholders and control flow, for example {{ item.name }} or <% if user %>. Those are not HTML, so a validator pointed at a template file reports nonsense or nothing useful. The reliable approach is to render real pages, then validate the output. Fetch the page from a running dev server, a static build folder or a test response, and feed that text to the validator.
Rendering also surfaces bugs that exist only with real data. Here is a small Node script I ran. It renders a list from data, and I deliberately forgot to escape the item names and reused an ID:
const { HtmlValidate } = require("html-validate");
const items = [{ id: "a", name: "Tom & Jerry" }, { id: "a", name: "Second" }];
const page = (rows) => `<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>List</title></head>
<body>
<ul>
${rows}
</ul>
</body>
</html>`;
// Bug: no escaping and a repeated id taken from the data
const rows = items.map((i) => `<li id="${i.id}">${i.name}</li>`).join("\n");
(async () => {
const hv = new HtmlValidate({ extends: ["html-validate:recommended"] });
const report = await hv.validateString(page(rows));
report.results
.flatMap((r) => r.messages)
.forEach((m) => console.log(`${m.line}:${m.column} ${m.ruleId} ${m.message}`));
})();
The output was:
6:16 no-raw-characters Raw "&" must be encoded as "&"
7:9 no-dup-id Duplicate ID "a"
Neither problem is visible in the template source, because the template looks correct. Both appear only when particular data flows through it. The fix is to escape text on output and to build IDs that cannot collide, for example by combining a prefix with the record key:
const esc = (s) =>
String(s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
const rows = items
.map((i, n) => `<li id="item-${esc(i.id)}-${n}">${esc(i.name)}</li>`)
.join("\n");
After this change the same validation returned valid: true. Real template engines escape for you by default; the usual way this bug appears is when someone turns escaping off for a "trusted" value or builds strings by hand.
Fragments
Some content is never a full document: a snippet in a CMS field, an HTML partial returned from an AJAX endpoint, or one component. The full-document rules do not apply to a fragment, since it has no doctype, <html> or <title>. Tools handle this differently. Some accept fragments as they are. Others expect a document and complain about the missing parts, in which case you wrap the fragment in a minimal valid page before checking.
I validated a menu fragment with a missing </li> using html-validate, and it reported only the real defect, without complaining about the absent doctype or title:
3:4 error Element <li> is implicitly closed by parent </ul>
An important caveat: whether a fragment is valid also depends on where it will be inserted. A <tr> is valid inside a <tbody> and invalid inside a <div>. If you validate fragments in isolation, remember the context, or wrap them in a stand-in parent that matches where they will land. The <template> element, which holds inert content, has its own parsing rules for the same reason.
Content built by JavaScript
Client-side rendering creates HTML after the page loads. A validator that fetches the URL sees only the initial document, which for a single-page application is often an empty shell. To check the final DOM, serialise it in a test browser, for example by reading document.documentElement.outerHTML in an end-to-end test, and validate that string. Keep in mind that the DOM is the parser's result, not your source, so errors that the parser repaired will already be gone. A DOM dump can tell you the structure is valid, but it cannot tell you which mistakes the parser fixed along the way. For that, validate the server's response too.
Step-by-step: validate HTML with DevUtilX
For a quick check without installing anything, use the DevUtilX HTML Validator. It runs in your browser, and your markup is processed locally and is not uploaded, which matters when the page contains unpublished content or customer data.
- Open the HTML Validator.
- Paste your markup into the input editor. For a full page, copy the rendered source from your browser's "view source", not the template file.
- Run the validation.
- Read the messages from the top down. Note the line and column for each one.
- Fix the first group of errors in your source, paste again and re-run. Because errors cascade, do not try to fix thirty messages in one pass.
- When the result is clean, copy the corrected markup back into your project.
If a message does not make sense, find the line it points to and look at the lines above it. The most common cause of a mysterious error is an element that was never closed earlier in the file. If you are validating a snippet, remember the context caveat above. When the markup is a tangled single line, re-indent it first: consistent indentation makes unclosed and misnested tags visible, and our guide to HTML formatting and semantic markup explains the conventions.
Validation in code and CI
A manual check finds today's bugs. A check in your pipeline stops tomorrow's from shipping.
The Nu Html Checker
The Nu Html Checker is the reference implementation of conformance checking for the living standard. It is a Java program distributed as a jar file, commonly called the vnu jar, and it can check local files as well as run as a server. A typical local invocation looks like this:
java -jar vnu.jar --errors-only build/index.html
I did not run this command in the environment used for this article, which had no access to download the jar, so check the project's documentation for the current flags and release downloads. The checker's output is the most authoritative for specification conformance, and it is the right choice when you need the closest match to the standard.
html-validate for Node projects
html-validate is an npm package that runs offline, needs no Java, and can be configured per rule. Every output in this article came from version 11.18.0, installed in an isolated folder:
npm install --save-dev html-validate
npx html-validate "dist/**/*.html"
It exits with a non-zero status when it finds problems, so a CI job fails automatically. Run against my deliberately bad sample, the exit code was 1, and against the corrected file the exit code was 0.
A small configuration file, .htmlvalidate.json, selects a preset and lets you tune rules:
{
"extends": ["html-validate:recommended"],
"rules": {
"doctype-style": "off",
"void-style": "off"
}
}
Turning off style rules that your team does not care about makes real errors stand out. Resist the urge to disable rules that find genuine bugs, such as duplicate IDs or invalid nesting. A better route is to fix the cause, and to ignore a single known false positive in a specific file with an inline comment or a targeted override.
Using the API from a test
If you already run tests in Node, call the validator directly. This script validates any file and sets the process exit code, which I ran against the bad sample file:
const { HtmlValidate } = require("html-validate");
const fs = require("fs");
(async () => {
const hv = new HtmlValidate({ extends: ["html-validate:recommended"] });
const report = await hv.validateString(fs.readFileSync(process.argv[2], "utf8"));
for (const r of report.results)
for (const m of r.messages) console.log(`${m.line}:${m.column} ${m.ruleId} ${m.message}`);
console.log("valid:", report.valid);
process.exit(report.valid ? 0 : 1);
})();
On the bad file it began:
1:2 element-required-attributes <html> is missing required "lang" attribute
2:2 element-required-content <head> element must have <title> as content
5:2 wcag/h37 <img> is missing required "alt" attribute
6:14 no-implicit-close Element <p> is implicitly closed by adjacent <div>
6:34 close-order Stray end tag '</p>'
A tiny checker in Python, and why not to rely on it
You may be tempted to write your own check with the Python standard library. This version uses html.parser to catch duplicate IDs, images without alt and unbalanced tags:
import sys
from html.parser import HTMLParser
VOID = {"area", "base", "br", "col", "embed", "hr", "img", "input",
"link", "meta", "source", "track", "wbr"}
class Checker(HTMLParser):
def __init__(self):
super().__init__()
self.stack, self.ids, self.problems = [], set(), []
def handle_starttag(self, tag, attrs):
d = dict(attrs)
line = self.getpos()[0]
if "id" in d:
if d["id"] in self.ids:
self.problems.append(f"duplicate id {d['id']!r} at line {line}")
self.ids.add(d["id"])
if tag == "img" and "alt" not in d:
self.problems.append(f"img without alt at line {line}")
if tag not in VOID:
self.stack.append(tag)
def handle_endtag(self, tag):
if tag in VOID:
return
if self.stack and self.stack[-1] == tag:
self.stack.pop()
else:
self.problems.append(
f"unexpected </{tag}> at line {self.getpos()[0]}, open: {self.stack}")
c = Checker()
c.feed(open(sys.argv[1]).read())
c.close()
print("\n".join(c.problems) or "no problems")
sys.exit(1 if c.problems else 0)
Run on my bad sample, it printed only:
img without alt at line 5
duplicate id 'x' at line 7
Two findings against thirteen from the real validator. It missed the nested anchors, the <p> containing a <div>, the obsolete element and the bad boolean attribute, because it knows nothing about content models. It also did not report the unclosed paragraph, since the <p> start and end tags happened to balance. This is a useful lesson. A hand-written check finds the failures you thought to look for, and that is all. A real validator carries the whole specification. Use scripts like this for project-specific rules, such as "every page has a canonical link", and leave conformance to a proper tool.
A CI job
In most CI systems the pipeline is: build the site, then validate the output, then fail on errors. A minimal package.json script and workflow step might look like this:
{
"scripts": {
"build": "your-build-command",
"validate:html": "html-validate \"dist/**/*.html\"",
"ci": "npm run build && npm run validate:html"
}
}
- run: npm ci
- run: npm run ci
Keep the job fast by validating generated pages for each template type, not every page of a large site. One page per layout, plus a few with the heaviest content, catches most template bugs.
If you adopt validation in an existing project, expect a large initial report. Do not block builds on day one. Fix the categories that cause real problems first, record the rest as a baseline, and tighten over time so that new errors fail the build while old ones are tracked.
Common mistakes and pitfalls
- Validating the template source. Placeholders and loops are not HTML. Validate rendered output.
- Believing "it renders" means "it is valid". The parser repairs mistakes silently, as the paragraph and table examples showed.
- Fixing errors out of order. Errors cascade, so start at the top and re-run.
- Chasing every warning blindly. Some messages are style preferences of the tool. Understand each rule before you obey or disable it.
- Disabling rules to get a green build. A rule turned off because it was noisy is a bug you chose not to see.
- Using
<div />and other self-closing tags on non-void elements. The slash is ignored in HTML, so the element stays open. - Treating a validator as an accessibility test. It checks parts of accessibility, such as missing
alt, and misses most of it. - Hard-coding IDs in reusable components. They collide as soon as the component appears twice.
- Relying on one tool. Tools implement different subsets of the rules. In my tests, the recommended preset of
html-validatestayed silent about an unknown ARIA role and an obsoletealignattribute, which a specification-based checker is likely to flag. - Putting
<meta charset>far down the head. The declaration must be reachable early. - Forgetting
typeon buttons. The defaultsubmittype causes accidental form submissions. - Validating once and never again. Markup changes with every release, so automate the check.
Accessibility, SEO, performance and security
Accessibility
Valid markup is the base that accessibility builds on. Assistive technology reads the parser's tree. When the tree differs from what you intended, because of a foster-parented element or a duplicate ID, the experience can break for screen reader users while looking fine to sighted ones. Several accessibility basics are also validity rules: alt on images, a lang attribute, a title, unique IDs for for and ARIA references, and correct element nesting for interactive controls.
Still, validity does not equal accessibility. A page can pass every conformance check and be impossible to use with a keyboard, with unlabelled form controls built from generic elements, poor contrast and a meaningless heading order. In my test, a heading jump from <h1> to <h3> and a label whose for pointed to a non-existent ID both passed the recommended preset without a message. Treat the validator as one layer. Add automated accessibility tests and, importantly, manual testing with a keyboard and a screen reader.
SEO
Search engines parse HTML with tolerant parsers, as browsers do, so validity is not a ranking factor that I can point to with evidence. What validation does protect against are real structural problems that can affect how a page is understood: a missing or duplicated title, an unclosed element that swallows content, structured data embedded in a broken script tag, or a head that is closed early by a misplaced element so that meta tags after it are ignored. Fix the validity problems for the sake of correctness and users, and treat any SEO gain as a possible side effect, not a promise.
One specific pitfall is worth knowing. Elements that do not belong in <head>, such as an <img> or a <div>, make the parser end the head and start the body. Tags placed after that point, including <meta> and <link rel="canonical">, are then in the body, where search engines and browsers may not treat them as head metadata. A validator that reports "element not allowed in head" is pointing at exactly this.
Performance
Valid, tidy markup is cheaper to parse than a document full of error recovery, though the difference is small compared with network and script costs. The more useful performance link is indirect: validation catches the missing width and height attributes with invalid values that cause layout shifts, and the duplicate or malformed resources that waste requests. For delivery, validate first and then minify, as our HTML minification guide recommends.
Security
Validity is not security, but the two overlap in output escaping. The unescaped & and < errors that a validator reports are the same defects, in mild form, that cross-site scripting attacks exploit. If a stray < from user input breaks your markup, an attacker's <script> could be inserted the same way. A validator that complains about raw characters in rendered pages is a cheap early warning that something is not being escaped. Escape on output, use your template engine's automatic escaping, and add a Content Security Policy as defence in depth. Do not claim a page is secure because it validates.
Troubleshooting guide
Work from symptom to cause to fix.
Symptom: dozens of errors after one small change
Cause. Cascading errors from a single unclosed or misnested tag.
Fix. Fix only the first error, re-run, and repeat. Look above the reported line for an element that was never closed.
Symptom: "Stray end tag" with no obvious extra tag
Cause. An earlier start tag was implicitly closed by the parser, for example a <p> closed by an adjacent <div>, so the later end tag has nothing to match.
Fix. Find the earlier element, as in the <p>One<div>Two</div></p> example, and restructure the nesting.
Symptom: the layout is wrong, but the page validates
Cause. Validity says nothing about CSS. The problem is in your styles, or in a mistake the validator's rule set does not cover.
Fix. Use the browser's element inspector to compare the DOM tree with your source. Differences between them show where the parser intervened.
Symptom: an element appears in the wrong place on the page
Cause. Foster parenting. Content that is not allowed inside a table, such as a <div> or text between rows, was moved in front of the table.
Fix. Put the content inside a cell or outside the table. Wrap rows in <tbody> and check for stray tags.
Symptom: the validator reports errors on a template file
Cause. The file contains template syntax, not HTML.
Fix. Render the template with real data, and validate the output.
Symptom: the validator complains about the missing doctype or title on a snippet
Cause. The tool treats the input as a complete document.
Fix. Wrap the fragment in a minimal valid page, or switch the tool into fragment mode if it has one.
Symptom: two tools disagree
Cause. They implement different subsets of the rules, and some of their messages are style preferences, not conformance requirements.
Fix. Read the rule documentation, and check against the specification text at html.spec.whatwg.org when it matters. Choose one tool as the CI authority and document the choice.
Symptom: a "duplicate ID" error appears only in production
Cause. A component repeats on the page, or a plugin injects an element with the same ID as one of yours.
Fix. Validate the fully rendered page, rename one of the IDs, and generate IDs from record keys.
Symptom: special characters turn into garbage such as é
Cause. The encoding was misdeclared, or the declaration comes too late, or the server header says something else.
Fix. Save the file as UTF-8, put <meta charset="utf-8"> first in the head, and make the Content-Type header agree.
Symptom: a form submits when you click a button that should not submit
Cause. A <button> without a type attribute defaults to submit.
Fix. Add type="button" for actions and type="submit" for submission.
Symptom: a "disabled" field is still disabled after setting disabled="false"
Cause. A boolean attribute is true whenever it is present, whatever the value.
Fix. Remove the attribute, or set the property element.disabled = false in script.
Symptom: a self-closing <div /> swallows the rest of the page
Cause. HTML ignores the slash on non-void elements, so the div stays open.
Fix. Write <div></div>. If the markup came from a JSX or XML habit, search for other self-closing tags on non-void elements.
Best practices and checklist
- Validate rendered pages, not templates, and include real data.
- Make validation part of CI, and fail the build on errors.
- Start with errors, then tune the warning level with intent.
- Always declare
<!DOCTYPE html>,<html lang>,<meta charset="utf-8">early in the head, and a<title>. - Prefer native elements over ARIA roles on generic ones.
- Give every image an
alt, empty for decorative images. - Keep IDs unique, generated from data keys in components.
- Escape on output. Encode
&,<,>and quotes in text and attributes. - Keep presentation in CSS, which also avoids obsolete attributes. Our CSS formatting and architecture guide covers that side.
- Pick one void-element style and a consistent boolean attribute style.
- Format before you debug. Consistent indentation exposes misnesting.
- Combine tools: a conformance checker, an accessibility checker and a manual test.
- Document accepted exceptions with a comment and a reason, so they can be revisited.
- Re-validate after minification or any build step that rewrites markup.
A short pre-release checklist:
- Does every page have a doctype, a language, a charset and a unique title?
- Do all rendered pages pass the conformance checker with zero errors?
- Are IDs unique on every page, including pages with repeated components?
- Do all images have an
alt, and are decorative ones empty? - Is there no interactive element nested inside another?
- Are all user-supplied values escaped on output?
- Is the validation job in CI, and does it fail the build?
- Have you tested with a keyboard and a screen reader?
Comparison: ways to check HTML
| Approach | Strengths | Limits | Best for |
|---|---|---|---|
| DevUtilX HTML Validator in the browser | No setup, local processing, instant feedback | Manual, one document at a time | Quick checks and learning |
| W3C Markup Validation Service | Well known, public, follows the standard | Needs the page to be reachable or pasted, and you send the markup to a service | Public pages and second opinions |
Nu Html Checker (vnu jar) |
Closest to the specification, can run locally | Needs Java, and output can be verbose | Authoritative CI checks |
html-validate (npm) |
Offline, configurable, Node-friendly, fast | Different rule coverage from the standard | JavaScript projects and component tests |
| Browser developer tools | Shows the real DOM the parser built | Does not report conformance errors | Seeing parser repairs |
| Accessibility checkers | Find contrast, label and role problems | Do not check full conformance | Accessibility audits |
| Linters in the editor | Instant feedback as you type | Partial rules | Everyday authoring |
| Hand-written scripts | Enforce project rules | Miss everything you did not code | Custom conventions |
No single approach covers everything. A common pairing is an editor linter for instant feedback, a conformance checker in CI and an accessibility scan plus manual testing before release.
FAQ
Is HTML validation required?
No. Nothing forces a page to be valid, and browsers render invalid pages. Validation is a quality practice that catches defects the parser would otherwise hide.
Does invalid HTML hurt SEO?
I know of no evidence that conformance by itself is a ranking factor, and search engines use tolerant parsers. But structural errors, such as a head closed early or a missing title, can cause real problems, so fixing them is still worthwhile.
What is the difference between the W3C validator and the Nu Html Checker?
The W3C Markup Validation Service is a public web service, and its HTML checking is powered by the Nu Html Checker, which you can also download and run yourself. The details of the history have changed over time, so check the project pages for the current picture.
Is the HTML Living Standard the same as HTML5?
The WHATWG maintains HTML as a continuously updated Living Standard, and "HTML5" is the name widely used for the generation of the language that introduced the parsing algorithm and many new elements. For day-to-day rules, consult the Living Standard.
Should I write <br /> or <br>?
Both are accepted in HTML. The slash has no effect. Choose one style and apply it consistently, and let your tool enforce it.
Do I need to close every tag?
Not always. Some end tags, such as those of <li>, <p> and <td>, may be omitted in specific contexts. Void elements have no end tag. Omitting tags where it is not allowed is an error, so unless you know the rules, close your tags.
Why does my validator accept role="banana" but another rejects it?
Tools implement different rule subsets. The ARIA specification defines the valid role names, and a checker that implements those rules should flag unknown ones. In my test, the recommended preset of html-validate did not report it.
Can I validate a page behind a login?
Yes, if you paste the rendered source or run a local checker against saved output. A public online service cannot reach pages behind authentication. The DevUtilX validator works on pasted markup in your browser, so nothing needs to be exposed.
Should I validate before or after minifying?
Before, so that errors map to readable source, and after as a sanity check that the minifier did not break anything.
Do I need to fix every warning?
Not blindly. Read each one, understand the rule and decide. Warnings often flag real risk, but some are style preferences that you can turn off.
Why does the validator report an error far from my mistake?
Because the parser recovers and keeps going. The tool reports where it noticed the inconsistency, which can be many lines after the real defect, such as an unclosed element.
Does valid HTML guarantee accessibility?
No. It guarantees none of contrast, keyboard operation or meaningful labels. It helps with some basics, so combine it with accessibility testing.
Is it safe to paste my HTML into an online validator?
Only if you trust the service. DevUtilX processes your input locally in the browser. Never paste secrets, tokens or personal data into any tool unless you know where the data goes.
Conclusion
HTML validation separates "the browser managed to display it" from "the markup follows the rules". Browsers repair mistakes silently, through implied tags, foster parenting and the adoption agency algorithm, so a page can look fine while hiding duplicate IDs, nested interactive elements and broken structure. A validator shows you those hidden repairs. Validate the rendered output, fix errors from the top down, add the check to CI, and remember that it complements accessibility testing and does not replace it.
To check a page right now, open the DevUtilX HTML Validator and paste your markup. To keep going, read our guides on HTML formatting and semantic markup and HTML minification pros and cons, and see how the same discipline applies to data formats in our guide to XML formatting and structure.


