Introduction
Run axe DevTools on your site right now. Seriously, right now -- I'll wait. If you got zero violations, close this tab. If you got 15+, keep reading.
Most of those violations will trace back to the same five or six markup mistakes. Wrong elements. Missing labels. Broken heading hierarchy. And the fixes take minutes, not days. The problem was never difficulty. It was that nobody on the team thought to check.
What follows is organized by impact. Highest-violation fixes first.
Why Semantic HTML Matters
Accessibility: The Web Is for Everyone
Screen readers read the DOM, not the pixels. A <nav> gets announced as navigation. Users jump straight to it. A <div class="nav">? Silent. Invisible. The user tabs through every element on the page hunting for links that the developer buried in meaningless containers.
About 15% of the world population has some form of disability. And in the US, EU, and Australia, accessibility is law, not suggestion. The lawsuits are real and getting more frequent, and "we didn't know" stopped being a defense years ago.
SEO: Search Engines Read Your Markup
Google's crawlers use your HTML structure to figure out what a page is about. Proper <h1> through <h6> headings, <article> elements, <nav> -- these are free ranking signals. No plugin required. Most developers ignore this and then pay for SEO tools that solve a problem the markup could have solved from the start.
Maintainability: Code That Explains Itself
Which codebase would you rather inherit?
<!-- Non-semantic: What is what? --><divclass="top-bar"><divclass="top-bar-links"><ahref="/">Home</a><ahref="/about">About</a></div></div><divclass="content-area"><divclass="main-text">...</div><divclass="side-stuff">...</div></div><divclass="bottom-bar">...</div><!-- Semantic: Self-documenting --><header><nav><ahref="/">Home</a><ahref="/about">About</a></nav></header><main><article>...</article><aside>...</aside></main><footer>...</footer>Shorter. Self-documenting. No comments needed. Someone touching this code six months later reads the element names and understands the page. That is the actual argument for semantic HTML -- not purity, not standards compliance. It is that <header> communicates what <div class="top-bar"> never will.
Landmark Elements
Landmarks are the single highest-impact fix for screen reader accessibility. They let users skip between page regions instantly. Without them, navigating your site means crawling through every element sequentially.
The skeleton:
<body><header><!-- Site logo, navigation, search --><navaria-label="Primary"><ul><li><ahref="/">Home</a></li><li><ahref="/blog">Blog</a></li><li><ahref="/contact">Contact</a></li></ul></nav></header><main><article><h1>Article Title</h1><sectionaria-labelledby="intro-heading"><h2id="intro-heading">Introduction</h2><p>Article content goes here...</p></section></article><asidearia-label="Sidebar"><!-- Related content, ads, table of contents --><navaria-label="Table of Contents"><ul>...</ul></nav></aside></main><footer><!-- Copyright, secondary nav, social links --><navaria-label="Footer"><ul>...</ul></nav></footer></body>Notice the aria-label attributes on the three <nav> elements. A screen reader announces "navigation" three times. Without labels the user cannot tell which is which. "Primary," "Table of Contents," "Footer" -- those are not decoration. They are the only way a non-sighted user distinguishes navigation regions. Most developers skip this. Most accessibility audits flag it.
One <main> per page. Never nested inside <header>, <footer>, or <nav>.
And the <article> versus <section> question that comes up constantly: if the content makes sense in an RSS feed on its own, it is an <article>. Blog posts, news stories, product cards. A <section> is a thematic grouping inside a page and should always have a heading. Not a generic wrapper. That is what <div> is for, and nobody seems to remember that <div> is not a dirty word -- it is the correct choice when no semantic element applies.
Headings, Lists and Text Semantics
Two-thirds of screen reader users navigate by headings. Not links. Not landmarks. Headings. This is from WebAIM's actual survey data.
Your heading structure is how most assistive technology users experience your site. Get it wrong and they are lost.
Heading Hierarchy
One <h1> per page. <h2> through <h6> in order. Never skip levels. Do not jump from <h2> to <h4> because you liked <h4>'s default font size. CSS controls styling. Heading levels control structure. Different jobs entirely.
<!-- Correct heading hierarchy --><h1>Semantic HTML and Web Accessibility</h1><h2>Why Semantic HTML Matters</h2><h3>Accessibility Benefits</h3><h3>SEO Benefits</h3><h2>Landmark Elements</h2><h3>The Header Element</h3><h3>The Nav Element</h3><!-- Incorrect: skips from h2 to h4 --><h2>Section Title</h2><h4>This should be an h3!</h4>Lists: More Than Bullet Points
Criminally underused. When a screen reader encounters a <ul> or <ol>, it announces the item count. The user immediately knows how much content lies ahead. Navigation menus should always be lists. That is literally what they are.
<ul> for unordered collections. <ol> for ordered sequences. <dl> for name-value pairs. Most developers forget <dl> exists.
Inline Text Semantics
<strong> and <b> look identical in a browser. They are not the same thing. <strong> signals importance -- screen readers may change tone of voice for it. <b> is cosmetic. No semantic weight. Same distinction between <em> and <i>. Matters more than most teams realize.
Accessible Forms
Forms break accessibility more than anything else. And forms are where accessibility failures cost actual money. An e-commerce checkout where every input uses placeholder text instead of labels, error messages float unlinked, and the submit button is a styled <div> -- screen reader users literally cannot buy anything. But the fix takes maybe twenty minutes.
Labels: The Non-Negotiable
An accessible form starts with one rule: every input gets a <label> connected via the for attribute. Not a placeholder. Not a nearby <span>. Placeholders vanish the moment you start typing. A real <label> element, linked programmatically. The autocomplete attribute helps browsers and password managers. The required attribute gives you native validation that works even when JavaScript fails. And aria-describedby links hint text to the input so screen readers announce it on focus. Here is a properly built email field:
<!-- Bad: placeholder as label --><inputtype="email"placeholder="Email address"><!-- Good: proper label association --><labelfor="user-email">Email address</label><inputtype="email"id="user-email"name="email"autocomplete="email"requiredaria-describedby="email-hint"><pid="email-hint"class="hint">
We will never share your email with third parties.
</p>Standard stuff.
Fieldsets and Legends: Grouping Related Inputs
Radio buttons without a <fieldset> and <legend>? A screen reader user hears each option in isolation. No context. No idea what the group of choices represents.
<formaction="/checkout"method="POST"novalidate><fieldset><legend>Shipping Method</legend><div><inputtype="radio"id="ship-standard"name="shipping"value="standard"checked><labelfor="ship-standard">
Standard (5-7 business days) - Free
</label></div><div><inputtype="radio"id="ship-express"name="shipping"value="express"><labelfor="ship-express">
Express (2-3 business days) - $12.99
</label></div><div><inputtype="radio"id="ship-overnight"name="shipping"value="overnight"><labelfor="ship-overnight">
Overnight (next business day) - $24.99
</label></div></fieldset><buttontype="submit">Continue to Payment</button></form>Error Messages: Be Specific and Programmatic
A red border is not an error message. Color alone conveys nothing to a screen reader and nothing to a colorblind user. Use aria-invalid="true" on the field, aria-describedby to link the error text, and aria-live="polite" on the error container so screen readers announce changes.
"Invalid input" tells the user nothing.
"Please enter an email in the format [email protected]" tells them exactly what to fix.
The difference between those two strings is the difference between a user completing the form and a user leaving your site.
ARIA Roles and Attributes
The first rule of ARIA is do not use ARIA.
Not a joke. A <div role="button" tabindex="0"> is strictly worse than a <button>. The div needs JavaScript for keyboard handling, ARIA for states, CSS for focus styles. The button gets all of that natively. Bad ARIA is worse than no ARIA. I have seen codebases where someone sprinkled ARIA attributes across everything and the result was less accessible than plain <div> soup.
When ARIA Is Appropriate
Custom widgets with no native HTML equivalent. That is basically it. The attributes worth knowing:
aria-labelprovides an accessible name when there is no visible text. Good for icon-only buttons:<button aria-label="Close dialog">.aria-labelledbypoints to another element as the label. Common on sections:<section aria-labelledby="section-heading">.aria-describedbylinks supplementary text to an element -- perfect for form hints and error messages.aria-expandedtells assistive tech whether a collapsible element is open or closed. Accordions, dropdowns, hamburger menus.aria-hidden="true"removes an element from the accessibility tree entirely. Decorative icons, duplicate content, elements visually hidden but still in the DOM.aria-liveannounces dynamic content changes. Use"polite"for non-urgent updates and"assertive"for critical alerts.
A Real-World ARIA Example: Accordion Component
This is what ARIA is actually for.
<divclass="accordion"><h3><buttontype="button"aria-expanded="false"aria-controls="faq-1-content"id="faq-1-trigger">
What is semantic HTML?
<svgaria-hidden="true"class="icon-chevron">...</svg></button></h3><divid="faq-1-content"role="region"aria-labelledby="faq-1-trigger"hidden><p>
Semantic HTML means using HTML elements for their
intended purpose. A <nav> for navigation,
a <button> for actions, a <table> for
tabular data, and so on.
</p></div></div>aria-expanded communicates open/closed state. aria-controls links the button to the panel. aria-hidden="true" on the chevron keeps screen readers from announcing a decorative SVG. And the hidden attribute on the panel natively removes it from both display and accessibility tree. No custom CSS hiding tricks needed.
Keyboard Navigation and Focus Management
Not everyone uses a mouse. Motor disabilities. Power users who refuse to touch one. Switch devices that simulate keyboard input. If your site cannot be fully operated by keyboard, those people are locked out.
The Basics: Tab Order and Focus Visibility
Links, buttons, and form inputs are natively focusable. DOM order determines tab order. Two mistakes come up constantly: removing the focus outline and setting tabindex values above zero.
Both are worse than doing nothing.
/* Remove default outline only for mouse users */:focus {
outline: none;
}
/* Restore visible focus for keyboard users */:focus-visible {
outline: 3px solid #4A90D9;
outline-offset: 2px;
border-radius: 4px;
}
/* Custom focus style for buttons */.btn:focus-visible {
outline: 3px solid #4A90D9;
outline-offset: 2px;
box-shadow: 0 0 0 6px rgba(74, 144, 217, 0.25);
}:focus-visible fires on keyboard navigation, not mouse clicks. Solves the entire "designers hate the focus ring" argument in one selector.
Focus Trapping in Modals
When a modal opens, focus stays inside it. Tab at the last element wraps to the first. Escape closes the modal and returns focus to the trigger.
The native <dialog> element does all of this for free when you call .showModal(). Still building modals with <div> elements and custom focus trap JavaScript? Stop.
<!-- Native dialog with built-in focus trapping --><buttontype="button"id="open-dialog"onclick="document.getElementById('my-dialog').showModal()">
Delete Account
</button><dialogid="my-dialog"aria-labelledby="dialog-title"><h2id="dialog-title">Confirm Deletion</h2><p>
Are you sure you want to delete your account?
This action cannot be undone.
</p><divclass="dialog-actions"><buttontype="button"onclick="this.closest('dialog').close()">
Cancel
</button><buttontype="button"class="btn-danger"onclick="deleteAccount()">
Yes, Delete My Account
</button></div></dialog>Skip Navigation Links
A "skip to main content" link. First focusable element in <body>. Visually hidden, visible on focus. Without it, keyboard users tab through your entire navigation on every single page load. Tiny fix. Huge difference.
Testing Accessibility
Writing the markup is half the work. The other half is proving it works.
Automated Testing Tools
Automated tools catch 30-50% of accessibility issues. Useful first pass. Not sufficient on its own.
axe DevTools is the one to install first. It scans the page and reports violations with severity and fix suggestions. Lighthouse has an accessibility audit built in. For React, eslint-plugin-jsx-a11y catches missing alt attributes and invalid ARIA at the code level. Pa11y runs from the command line against a URL -- put it in your CI pipeline and accessibility regressions fail the build automatically. But none of these tools will tell you that your tab order makes no sense or that your modal traps focus wrong. That requires a human.
Manual Testing: The Keyboard Test
Unplug the mouse. This one exercise finds more problems than most developers expect:
- Can you tab to every interactive element (links, buttons, inputs, selects)?
- Is the focus indicator always visible? Can you always tell where you are?
- Does the tab order follow a logical sequence that matches the visual layout?
- Can you activate buttons with Enter and Space?
- Can you open and close dropdowns, modals, and menus? Does pressing Escape close them?
- Can you navigate radio groups and select options with arrow keys?
- After a modal closes, does focus return to the element that opened it?
Screen Reader Testing
Spend an hour navigating websites with a screen reader. Just once. It permanently changes how you write markup.
Mac: VoiceOver, built in, Cmd+F5. Windows: NVDA, free, pairs best with Firefox. Android: TalkBack, built in.
Listen for whether landmarks get announced. Whether headings communicate structure. Whether form fields have labels. Whether error messages surface when they appear. If any of those fail, you know exactly what to fix. The gap between what you thought was accessible and what actually is can be embarrassing, and that discomfort is productive because it is the thing that makes you write better HTML going forward instead of just reading articles about it.
Checklist
- Run axe DevTools on every page before merging a PR. Fix anything flagged critical or serious.
- One
<h1>per page, no skipped heading levels, headings in order. - Every form input has a
<label>with aforattribute. No placeholder-only fields. - Every
<img>has analtattribute. Decorative images getalt="". - Tab through the entire page with the keyboard. Every interactive element reachable, focus always visible.
- Navigate the page with a screen reader once a quarter. VoiceOver or NVDA. No excuses.