HTML Guides for heading
Learn how to identify and fix common HTML validation errors flagged by the W3C Validator — so your pages are standards-compliant and render correctly across every browser. Also check our Accessibility Guides.
The ARIA in HTML specification defines which roles are allowed on each HTML element. Heading elements (<h1>–<h6>) have an implicit role of heading, and the set of roles they can be explicitly assigned is limited. The button role is not among them, so applying role="button" directly to a heading element is invalid.
This matters for several reasons. First, headings play a critical role in document structure and accessibility — screen reader users rely on headings to navigate and understand the page hierarchy. Assigning role="button" to a heading overrides its semantic meaning, which confuses assistive technologies. Second, browsers and screen readers may handle conflicting semantics unpredictably, leading to an inconsistent experience for users. Third, it violates the W3C HTML specification, which means your markup won't pass validation.
Why This Combination Is Problematic
When you apply role="button" to an element, assistive technologies treat it as an interactive button. This completely replaces the element's native heading semantics. Users who navigate by headings would no longer find that heading in their list, and users who navigate by interactive controls would encounter a "button" that may not behave like one (lacking keyboard support, focus management, etc.).
If you genuinely need something that looks like a heading but acts as a button, there are valid approaches to achieve this without breaking semantics.
How to Fix It
There are several strategies depending on your intent:
Use a
<button>element styled as a heading. This is often the cleanest approach when the primary purpose is interactivity. You can style the button with CSS to match your heading appearance.Wrap the heading in a
<div>withrole="button". This preserves the heading in the document outline while making the wrapper interactive. However, be aware that thebuttonrole appliesrole="presentation"to all descendant elements, meaning assistive technologies will strip the heading semantics from the<h2>inside it. The text content remains accessible, but it won't be recognized as a heading.Place a
<button>inside the heading. This keeps the heading semantics intact for document structure while making the text inside it interactive. This pattern is commonly used for accordion-style components and is the approach recommended by the WAI-ARIA Authoring Practices.
Examples
❌ Invalid: role="button" on a heading
<h2 role="button">Toggle Section</h2>
This triggers the validation error because button is not an allowed role for heading elements.
✅ Fix: Use a <button> styled as a heading
<button type="button" class="heading-style">Toggle Section</button>
.heading-style {
font-size: 1.5em;
font-weight: bold;
background: none;
border: none;
cursor: pointer;
}
✅ Fix: Wrap the heading in a container with role="button"
<div role="button" tabindex="0">
<h2>Toggle Section</h2>
</div>
Note that this approach causes the <h2> to lose its heading semantics for assistive technologies, since the button role does not support semantic children. Also remember to add tabindex="0" so the element is keyboard-focusable, and implement keydown handlers for Enter and Space to replicate native button behavior.
✅ Fix: Place a <button> inside the heading (recommended for accordions)
<h2>
<button type="button" aria-expanded="false">
Toggle Section
</button>
</h2>
This is the most robust pattern. The heading remains in the document outline, and the button inside it is fully interactive with built-in keyboard support. Screen reader users can find it both when navigating by headings and when navigating by interactive elements. The aria-expanded attribute communicates whether the associated section is open or closed.
Headings play a critical role in structuring a web page. They create an outline of the document that both users and machines rely on. Screen readers, for example, allow users to navigate a page by jumping between headings, making them one of the most important tools for accessible navigation. When a heading is empty, screen readers may announce "heading level 2" (or similar) with no accompanying text, leaving users confused about what section they've entered.
Empty headings also signal a structural problem. They often appear when developers use heading elements purely for spacing or styling purposes, when content is meant to be injected dynamically via JavaScript but the script fails, or when headings are left as placeholders during development and never filled in.
The W3C validator flags this as a warning because the HTML specification states that headings represent the topic of their section. An empty heading cannot fulfill this purpose. While it is technically parseable HTML, it is semantically meaningless and degrades the quality of the document.
How to fix it
- Add descriptive text to the heading that accurately represents the content of its section.
- Remove the empty heading if it serves no purpose. Don't keep empty headings as spacers — use CSS margins or padding instead.
- If content is loaded dynamically, consider adding the heading element via JavaScript at the same time as its content, rather than leaving an empty shell in the markup.
- If you're hiding the heading visually but still want it available for screen readers, use a visually-hidden CSS technique rather than leaving it empty.
Examples
❌ Empty headings (triggers the warning)
<h1></h1>
<h2> </h2>
<h3>
</h3>
All three of these are considered empty — even the ones containing whitespace or a newline.
❌ Using an empty heading for spacing
<h2></h2>
<p>This paragraph needs some space above it.</p>
This misuses a heading element for visual layout purposes.
✅ Heading with meaningful content
<h1>Welcome to Our Store</h1>
<h2>Featured Products</h2>
<p>Check out our latest arrivals.</p>
✅ Using CSS for spacing instead of empty headings
<p class="section-intro">This paragraph needs some space above it.</p>
.section-intro {
margin-top: 2rem;
}
✅ Visually hidden heading for screen readers
If you need a heading that is available to assistive technologies but not visible on screen, include text and hide it with CSS:
<h2 class="visually-hidden">Navigation Menu</h2>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
✅ Adding headings dynamically with their content
Instead of placing an empty heading in the HTML and populating it later, create the heading along with its content:
<div id="results"></div>
<script>
const container = document.getElementById("results");
const heading = document.createElement("h2");
heading.textContent = "Search Results";
container.appendChild(heading);
</script>
This approach avoids any moment where an empty heading exists in the DOM, ensuring validity and accessibility at all times.
According to the HTML specification, heading elements (h1–h6) have a content model of "phrasing content," which means they can only contain inline-level elements like span, strong, em, a, and text nodes. Other heading elements are not phrasing content — they are flow content — so placing one heading inside another is invalid HTML.
This matters for several reasons. Screen readers and other assistive technologies rely on a well-formed heading hierarchy to help users navigate a page. When headings are nested inside each other, the document outline becomes broken and confusing, making it harder for users to understand the structure of the content. Browsers may also attempt to "fix" the invalid markup by auto-closing the outer heading before starting the inner one, which can produce unexpected rendering and DOM structures that differ from what you intended.
There are two common causes of this error:
Intentionally nesting headings for styling. Developers sometimes nest an
h2inside anh1hoping to create a visual "main heading + subtitle" pattern. This is invalid. Instead, use separate heading elements or use aspanorpelement for the subtitle.A missing or malformed closing tag. If you accidentally write
<h3>instead of</h3>for a closing tag, the browser sees two openingh3tags in a row. The first heading is never properly closed, and the second heading appears to be nested inside it.
Examples
❌ Heading nested inside another heading
<h1>Main heading
<h2>Sub heading</h2>
</h1>
The h2 is a child of the h1, which is not allowed. To create a heading with a subtitle, use separate elements:
✅ Headings as siblings
<h1>Main heading</h1>
<h2>Sub heading</h2>
Or, if the subtitle should be part of a sectioned document structure:
<main>
<h1>Main heading</h1>
<section>
<h2>Section heading</h2>
<p>Paragraph content</p>
</section>
</main>
❌ Missing closing slash causes nesting
A very common typo is forgetting the / in the closing tag:
<h3>Meet the Feebles<h3>
<h3>Bad Taste<h3>
Here, <h3>Meet the Feebles<h3> opens a second h3 instead of closing the first one. The validator sees the second h3 as a child of the first. The same problem cascades to subsequent headings.
✅ Properly closed heading tags
<h3>Meet the Feebles</h3>
<h3>Bad Taste</h3>
❌ Using nested headings for visual hierarchy inside a heading
<h1>
Our Company
<h3>Established 1999</h3>
</h1>
✅ Using a span for supplementary text within a heading
<h1>
Our Company
<span class="subtitle">Established 1999</span>
</h1>
You can then style the .subtitle class with CSS to achieve the desired visual appearance — for example, displaying it on a new line with a smaller font size:
.subtitle {
display: block;
font-size: 0.5em;
font-weight: normal;
}
✅ Using the hgroup element
The hgroup element is specifically designed for grouping a heading with related content like subtitles:
<hgroup>
<h1>Our Company</h1>
<p>Established 1999</p>
</hgroup>
This keeps the heading hierarchy clean while semantically associating the subtitle with the heading. The hgroup element is supported in the current HTML living standard and works well with assistive technologies.
A heading, from <h1> to <h6>, that comes after a </body> or </html> end tag is stray, because the HTML parser accepts no more elements once the body has ended. The message names the heading level the validator found, and the cause and fix are the same for every level.
Once the parser reads </body>, only whitespace, comments and the closing </html> tag may follow. Any element after that point is a parse error. Browsers recover by treating the heading as part of the body again, so the page usually looks right and the mistake goes unnoticed. After </html> the validator also gives up: it reports "Cannot recover after last error" and checks nothing further down the page, so other errors stay hidden until you fix this one.
The </body> that ends the body is often not the one at the bottom of the page. A block of HTML included from a standalone file, such as a widget or a newsletter form, can bring its own <html>, <body>, </body> and </html> tags. The parser ignores the extra start tags, which the validator reports a few lines earlier, but it obeys the end tags, so the real body ends in the middle of the page. The heading that opens the next section is then the first element after that point. A template that closes the body before printing the rest of the content has the same effect.
Remove the <html>, <head> and <body> tags from the included block so only its content reaches the page, and keep one </body> and one </html> at the very end of the document. If the heading sits after the page's own </body>, move it inside the body.
Invalid example
<!DOCTYPE html>
<html lang="en">
<head>
<title>News</title>
</head>
<body>
<main>
<h1>News</h1>
<!-- Included newsletter block -->
<html>
<body>
<p>Subscribe to our newsletter.</p>
</body>
</html>
<h2>Latest articles</h2>
<p>New articles appear here every week.</p>
</main>
</body>
</html>
Valid example
<!DOCTYPE html>
<html lang="en">
<head>
<title>News</title>
</head>
<body>
<main>
<h1>News</h1>
<!-- Included newsletter block -->
<p>Subscribe to our newsletter.</p>
<h2>Latest articles</h2>
<p>New articles appear here every week.</p>
</main>
</body>
</html>
Heading elements <h1> through <h6> must not appear inside an element with role="button". ARIA exposes a button as a single, flat control and treats its contents as presentational, so a heading placed inside it is no longer announced as a heading. Move the heading outside the control, or use a native <button> for the part that is actually clickable.
The WAI-ARIA specification marks button as a role whose children are presentational. When the browser maps a role="button" element into the accessibility tree, it drops the roles of everything inside it, so the heading level is lost. The W3C validator reports the conflict instead of letting the heading semantics disappear silently.
This usually happens when a clickable card or toggle is built from a <div role="button"> that wraps a heading. Headings are navigation points that screen reader users jump between, and they form the document outline. Burying one inside a button removes it from heading navigation and flattens the outline.
Keep the heading as a sibling of the control rather than a descendant. When the whole region needs to be activatable, use a native <button> (or a link) for the action and leave the heading outside it.
Invalid example
<div role="button">
<h6>Save changes</h6>
</div>
Valid example
<h6>Settings</h6>
<button type="button">Save changes</button>
Heading elements <h1> through <h6> must not appear inside an element with role="img", because that role makes the element and everything in it a single image for assistive technologies.
WAI-ARIA lists img among the roles whose children are presentational. A screen reader announces the element once, by the name in its aria-label or aria-labelledby, and skips the semantics of its descendants. Any heading in there drops out of the heading list that screen reader users navigate by, and out of the document outline. The heading still renders, so sighted testers rarely notice anything wrong.
Two patterns usually cause it. One is a banner or card whose picture is a CSS background image: role="img" and aria-label go on the wrapper to give the background a text alternative, and the headline inside gets swallowed with it. The other is an illustration built from HTML, such as a mockup of an app screen, that is labelled as an image while its fake interface still uses real heading elements.
For a banner, give the picture its own element and keep the heading outside it: an <img> with alt text, or an empty child element that carries both the background and role="img". For an HTML mockup that is only a picture, keep role="img" and its label, and replace the headings inside with <p> or <span> elements styled the same way. If the content inside is real and readers need it, remove role="img" altogether.
Invalid example
<div class="hero" role="img" aria-label="Mountains at sunset">
<h1>Plan your next trip</h1>
</div>
Valid example
Put the text alternative on the image itself and leave the heading outside it:
<div class="hero">
<img src="mountains.jpg" alt="Mountains at sunset">
<h1>Plan your next trip</h1>
</div>
When the whole block is an illustration, keep role="img" and use plain text elements instead of headings:
<div class="app-preview" role="img" aria-label="Preview of the dashboard showing this week's sales">
<p class="preview-title">Weekly sales</p>
<p>Revenue is up 12% on last week.</p>
</div>
Heading elements <h1> through <h6> must not appear inside an element with role="separator", because a separator's contents are presentational and the heading would lose its meaning.
WAI-ARIA marks separator as a role whose children are presentational. Assistive technology gets the separator as a single divider, and the browser drops the roles of everything inside it. The heading stops being a heading: screen reader users can't jump to it, and it disappears from the document outline. The rule covers a plain divider and a focusable separator, such as the handle of a resizable panel.
This usually comes from a "divider with a title" pattern, where one wrapper with role="separator" holds both the decorative lines and the section heading. Put the heading next to the separator instead of inside it. A native <hr> already has the separator role, so it doesn't need the attribute.
Invalid example
<div class="divider" role="separator">
<h2>Our services</h2>
</div>
Valid example
<hr class="divider">
<h2>Our services</h2>
A th element can't contain a heading, from h1 to h6, at any depth. Put the text straight in the th, style it with CSS, or title the table with a caption or a heading before it.
The HTML standard defines the content of a th as flow content with no header, footer, sectioning content or heading content (h1 to h6 and hgroup) among its descendants. The rule applies at any depth, so a heading wrapped in a div inside the th still triggers this error. The message names the heading level the validator found, and the fix is the same for every level. A td doesn't have this restriction: headings are allowed in data cells.
A th is already a header. Screen readers announce it when they read the cells of its row or column, so a heading inside it adds nothing to the table. It does add an entry to the page's heading outline, and users who move through the page by headings land inside the table as if a new section started there.
Headings usually end up in header cells to make the text bigger or bolder. Browsers already show th text in bold, and CSS can change the size, weight or letter case.
How to fix it
- Remove the heading element and keep its text directly inside the
th. - Style the
thwith CSS if the header cells need a particular look. - Use a
captionif the heading was meant as a title for the whole table. Acaptionis tied to the table, and screen readers announce it with it. - Put a heading before the
tableif the table needs an entry in the page's outline, at a level that fits the surrounding headings.
Examples
❌ Invalid: heading inside a th
<table>
<tr>
<th><h2>Product</h2></th>
<th><h2>Price</h2></th>
</tr>
<tr>
<td>Widget</td>
<td>$25</td>
</tr>
</table>
Each h2 is a descendant of a th, which the th content model doesn't allow.
✅ Valid: plain text in the th, styled with CSS
<table class="price-table">
<tr>
<th>Product</th>
<th>Price</th>
</tr>
<tr>
<td>Widget</td>
<td>$25</td>
</tr>
</table>
.price-table th {
font-size: 1.2em;
text-transform: uppercase;
}
✅ Valid: a caption as the table title
<table>
<caption>Product pricing</caption>
<tr>
<th>Product</th>
<th>Price</th>
</tr>
<tr>
<td>Widget</td>
<td>$25</td>
</tr>
</table>
✅ Valid: a heading before the table
<h2>Product pricing</h2>
<table>
<tr>
<th>Product</th>
<th>Price</th>
</tr>
<tr>
<td>Widget</td>
<td>$25</td>
</tr>
</table>
HTML heading elements (<h1> through <h6>) have built-in semantic meaning that browsers and assistive technologies already understand. According to the WAI-ARIA specification, each of these elements carries an implicit heading role with a corresponding aria-level — <h1> has aria-level="1", <h2> has aria-level="2", and so on. When you explicitly add role="heading" to one of these elements, you're telling the browser something it already knows, which clutters your markup without providing any benefit.
This pattern is part of a broader principle in ARIA authoring known as the first rule of ARIA: don't use ARIA when a native HTML element already provides the semantics you need. Redundant ARIA roles can cause confusion for developers maintaining the code, as it suggests that the role might be necessary or that the element might not otherwise be recognized as a heading. In some edge cases, adding an explicit aria-level that doesn't match the heading level (e.g., aria-level="3" on an <h1>) can create conflicting information for screen readers, leading to an inconsistent experience for users of assistive technologies.
The role="heading" attribute is designed for situations where you need to give heading semantics to a non-heading element, such as a <div> or <span>. In those cases, you must also include the aria-level attribute to specify the heading's level. However, whenever possible, using native heading elements is always preferred over this ARIA-based approach.
How to fix it
- Remove
role="heading"from any<h1>through<h6>element. - Remove
aria-levelif it was added alongside the redundant role and matches the heading's native level. - If you genuinely need a non-standard element to act as a heading, use
role="heading"witharia-levelon that element instead — but prefer native heading elements whenever possible.
Examples
❌ Redundant role on a native heading
<h1 role="heading" aria-level="1">Welcome to My Site</h1>
<h2 role="heading">About Us</h2>
<h3 role="heading" aria-level="3">Our Mission</h3>
All three headings will trigger the validator warning. The role="heading" and aria-level attributes are completely unnecessary here because the elements already convey this information natively.
✅ Native headings without redundant roles
<h1>Welcome to My Site</h1>
<h2>About Us</h2>
<h3>Our Mission</h3>
Simply removing the redundant attributes resolves the issue while preserving full accessibility.
✅ Correct use of the heading role on a non-heading element
In rare cases where you cannot use a native heading element, the heading role is appropriate on a generic element:
<div role="heading" aria-level="2">Section Title</div>
This tells assistive technologies to treat the <div> as a level-2 heading. Note that aria-level is required here since a <div> has no implicit heading level. That said, using a native <h2> is always the better choice:
<h2>Section Title</h2>
❌ Conflicting aria-level on a native heading
Be especially careful with this anti-pattern, where the explicit level contradicts the element:
<h1 role="heading" aria-level="3">Page Title</h1>
This sends mixed signals — the element is an <h1> but claims to be level 3. Screen readers may behave unpredictably. If you need a level-3 heading, use <h3>:
<h3>Page Title</h3>
A heading level has been skipped in the document's heading hierarchy, such as jumping from an <h2> directly to an <h4>.
HTML headings (<h1> through <h6>) form a hierarchical outline of the document. Screen readers and other assistive technologies use this hierarchy to help users navigate content. When a level is skipped, the outline becomes ambiguous: does the <h4> after an <h2> belong to a missing <h3> section, or is it a direct subsection of the <h2>? Users who navigate by heading level may assume content is missing.
The rule is straightforward: after an <h1>, the next heading should be <h2>. After an <h2>, use <h3>, and so on. You can go back up the hierarchy at any time (an <h2> after an <h4> is fine, because it starts a new section), but you should not skip down.
If you are using a heading level purely for its visual size, use CSS instead. Apply the correct semantic heading level and style it however you want.
Example with skipped heading level
<h1>Recipe book</h1>
<h2>Desserts</h2>
<h4>Chocolate cake</h4>
The jump from <h2> to <h4> skips the <h3> level.
Fixed heading hierarchy
<h1>Recipe book</h1>
<h2>Desserts</h2>
<h3>Chocolate cake</h3>
If the <h4> was chosen for its smaller font size, apply CSS to the <h3> instead:
<style>
.small-heading {
font-size: 1rem;
}
</style>
<h1>Recipe book</h1>
<h2>Desserts</h2>
<h3 class="small-heading">Chocolate cake</h3>
A document lacks a heading with a computed level of 1, which means there is no <h1> element (or an element whose final heading level resolves to 1) on the page.
The W3C validator raises this as a warning because every document should have at least one <h1> to identify its main topic. Screen readers and search engines rely on the heading hierarchy to understand page structure, and <h1> is the expected entry point of that hierarchy.
A "computed heading level" differs from the literal tag name only when the now-obsolete <hgroup> algorithm or the abandoned outline algorithm would adjust levels. In practice, for nearly all documents, a computed level of 1 simply means an <h1> tag.
This warning appears when a page uses <h2>, <h3>, or other headings but skips <h1> entirely. Adding a single <h1> that describes the page content fixes the warning.
Example with the warning
<!DOCTYPE html>
<html lang="en">
<head>
<title>My page</title>
</head>
<body>
<h2>About us</h2>
<p>Welcome to our site.</p>
<h3>Our team</h3>
<p>We are a small group.</p>
</body>
</html>
Fixed example
<!DOCTYPE html>
<html lang="en">
<head>
<title>My page</title>
</head>
<body>
<h1>My page</h1>
<h2>About us</h2>
<p>Welcome to our site.</p>
<h3>Our team</h3>
<p>We are a small group.</p>
</body>
</html>
Each page should have exactly one <h1>. If the design calls for the <h1> to be visually hidden (for example, when a logo replaces the text), use a CSS technique to hide it from sighted users while keeping it accessible to screen readers, rather than removing it from the markup.
Validate at scale.
Ship accessible websites, faster.
Automated HTML & accessibility validation for large sites. Check thousands of pages against WCAG guidelines and W3C standards in minutes, not days.
1K Weekly
1,000 monthly checks. Cancel anytime.
Get 1K Weekly →Join teams across 40+ countries