WCAG 2.2 Success Criterion 1.3.1 requires that information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. The normative text reads:
“Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.”
In plain language: if you communicate something visually (a heading, a list, a column relationship), the same meaning must reach a screen reader, a search engine, and a future redesign without depending on CSS surviving.
The failing patterns
<!-- "headings" that are just bold paragraphs -->
<p style="font-weight: 700; font-size: 1.25rem;">Frequently asked</p>
<!-- "lists" assembled from <br> tags -->
WCAG 2.2 covers:<br>
- Perceivable<br>
- Operable<br>
- Understandable<br>
- Robust<br>
<!-- table layout for tabular data without <th> or scope -->
<table>
<tr><td>Name</td><td>Role</td></tr>
<tr><td>Maria</td><td>Engineer</td></tr>
</table>
<!-- everything wrapped in <div> -->
<div class="header">...</div>
<div class="nav">...</div>
<div class="main">...</div>
A sighted user perceives structure from typography, spacing, and alignment. A screen-reader user, a Reader Mode parser, a search-engine crawler, and a print stylesheet all consume the DOM. If the DOM is structureless, they cannot reconstruct meaning.
The fix — encode structure in markup
<!-- real headings -->
<h2>Frequently asked</h2>
<!-- real list -->
<p>WCAG 2.2 covers:</p>
<ul>
<li>Perceivable</li>
<li>Operable</li>
<li>Understandable</li>
<li>Robust</li>
</ul>
<!-- real table with header cells -->
<table>
<caption>Editorial team</caption>
<thead>
<tr><th scope="col">Name</th><th scope="col">Role</th></tr>
</thead>
<tbody>
<tr><td>Maria</td><td>Engineer</td></tr>
</tbody>
</table>
<!-- landmarks -->
<header>...</header>
<nav aria-label="Primary">...</nav>
<main id="main">...</main>
<aside aria-label="Sponsorship">...</aside>
<footer>...</footer>
Landmarks let screen-reader users jump between regions with a single key.
NVDA’s D and VoiceOver’s rotor both surface landmarks first.
Heading order
<h1> once per page; never skip a level. A page with <h1> → <h2>
→ <h4> reads “level four heading” with no context for the missing
three. axe-core’s heading-order rule catches the most common cases.
<h1>Article title</h1>
<h2>Background</h2>
<h3>Earlier work</h3> <!-- valid: h2 then h3 -->
<h2>Method</h2>
<h2>Results</h2>
<h3>Tables</h3>
<h4>Per-region</h4> <!-- valid: h3 then h4 -->
For component libraries, expose a level prop:
function Section({ level = 2, children, title }) {
const Heading = `h${Math.max(2, Math.min(6, level))}`;
return (
<section>
<Heading>{title}</Heading>
{children}
</section>
);
}
Lists
If two or more items share semantic relationship (siblings of equal weight,
ordered steps, definitions), use <ul>, <ol>, or <dl>. Visual bullet
points in CSS without underlying list markup are invisible to screen
readers; conversely, screen readers count list items, so a list of one
should usually be a paragraph.
Tables
<th scope="col"> and <th scope="row"> make the relationship between
data and header explicit. Avoid <table> for layout — CSS Grid is
universally supported and exposes no false semantics.
For tables with merged headers, use <th scope="colgroup"> and headers="..."
attributes; or, when complexity grows, refactor into multiple simpler tables.
Forms
Form controls require explicit labels. <label for> is the cleanest
mechanism. aria-labelledby works for non-form widgets. Grouping related
fields uses <fieldset> and <legend> — for example, a “shipping
address” block.
How to test
Automated — the following axe-core rules cover most 1.3.1 failures:
heading-orderlandmark-one-main,region,landmark-banner-is-top-levellist,listitemtable-fake-caption,td-headers-attr,scope-attr-validlabel,form-field-multiple-labelsdefinition-list
Manual screen-reader landmark navigation — NVDA’s D key cycles
through landmarks. VoiceOver’s rotor — VO+U — offers the same
view. A page with a single landmark tagged main and no others is harder
to navigate than one with header, nav, main, aside, footer.
When this is hard
- Component libraries that wrap children in
<div>. Some libraries emit<div>wrappers around your<h2>. This usually does not break semantics but can break heading order if the wrapper is itself rendered inside an unexpected context. - CMS-authored content. Authors paste from Word and produce styled
paragraphs that look like headings. Configure the editor to expose
heading levels as toolbar buttons; reject content with no
<h2>. - SPAs and route changes. When the route changes, screen readers do
not automatically announce the new page. Move focus to the new
<h1>on route change, or announce the page title viaaria-liveon a dedicated container.
Cross-links
- Reference: WCAG 1.3.1 explained on w3c.wiki.
- Adjacent: How to fix WCAG 4.1.2 name role value for accessible names on custom components.
- Testing: How to test with NVDA for landmark navigation.
- ARIA recipes: WAI-ARIA Authoring Practices for canonical patterns.
A useful self-test: hide all CSS (DevTools “Disable styles” or Reader Mode), and read the page top-to-bottom. If the result is intelligible, your 1.3.1 posture is sound. If it reads as one wall of text, your meaning lives in CSS, and a screen-reader user is missing it.