Missing main heading (h1)
headings/h1-missing
- Outcome class
- Hint only. An automated check that by design reports review hints only: its findings are marked for manual review, never as confirmed violations.
- Area
- Accessibility, Structure & Semantics
- Default severity
- medium
- axe-core rule
headings/h1-missing
Why we flag this
Screen reader users cannot find the start of the main content via heading navigation.
No <h1> element in the document.
What the rule checks
Scope. Documents with at least one rendered heading.
Expectation. Flags a document that has rendered headings but no rendered level-1 heading: neither an h1 nor role="heading" aria-level="1". A hidden h1 (hidden attribute) does not count. A level-1 heading inside an open shadow root counts. In each viewport the rendered h1 may be a different element. Not a WCAG failure (no criterion requires a level); a best-practice violation for the whole document, without an element.
Source. Best practice, no WCAG failure: axe page-has-heading-one; WAI tutorial Page Structure: Headings
Detection: shared; needs dom, computed_style.
Failing example
The check flags this for review. Corpus case structure_content_model. The page may contain further findings for other rules.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Index of tool specifications – Acme Hardware</title>
<style>
body { font: 16px/1.5 sans-serif; margin: 0 16px; }
a { display: inline-block; min-width: 44px; min-height: 44px; line-height: 44px; }
input, button { min-width: 44px; min-height: 44px; }
</style>
</head>
<body>
<!-- Product data from a page builder (#783). The feature list wraps its
items in a div, which the <ul> content model does not allow; the
browser repairs the tree, the markup stays wrong. The definition list
groups name-value pairs in div elements, which <dl> does allow, and
the table is well-formed. -->
<a href="#main" class="skip-link">Skip to content</a>
<header>
<p>Acme Hardware</p>
</header>
<nav aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/tools/">Tools</a></li>
</ul>
</nav>
<main id="main">
<!-- The page's h1 is hidden; the visible outline starts at h2. -->
<h1 hidden>Cordless drill CD-18</h1>
<p class="product-name">Cordless drill CD-18</p>
<h2>Features</h2>
<ul id="features">
<div class="feature-row">
<li>Two speeds</li>
<li>LED work light</li>
</div>
</ul>
<h2>Specifications</h2>
<dl id="specs">
<div><dt>Voltage</dt><dd>18 V</dd></div>
<div><dt>Weight</dt><dd>1.4 kg</dd></div>
</dl>
<h2>Spare parts</h2>
<table id="parts">
<caption>Spare parts and prices</caption>
<thead>
<tr><th scope="col">Part</th><th scope="col">Price</th></tr>
</thead>
<tbody>
<tr><td>Battery 2 Ah</td><td>39 EUR</td></tr>
<tr><td>Charger</td><td>29 EUR</td></tr>
</tbody>
</table>
</main>
<footer>
<p>Acme Hardware, Berlin</p>
</footer>
</body>
</html>Passing example
The check passes here and reports nothing. Corpus case landmark_main_in_shadow_root. The page may contain further findings for other rules.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Main landmark inside a shadow root corpus case</title>
</head>
<body>
<!-- The page's landmarks live in an open shadow root, as in web-component
app shells. Chrome's accessibility tree exposes the main landmark; a
document.querySelectorAll('main') check does not see it and used to
report a missing main. -->
<app-shell></app-shell>
<script>
const root = document.querySelector('app-shell').attachShadow({ mode: 'open' });
root.innerHTML =
'<header><nav aria-label="Site"><a href="/">Home</a></nav></header>' +
'<main><h1>Content in a shadow root</h1><p>Text.</p></main>' +
'<footer>Footer</footer>';
</script>
</body>
</html>This page is generated from the rule catalogue and the rule contracts; see therule overview for how the outcome classes are derived.