March 26, 20267 min read

HTML Accessibility: Not Optional, Not Hard

Practical accessibility guide: alt text, heading hierarchy, form labels, keyboard navigation, focus management, ARIA, color contrast, and screen reader testing.

html accessibility a11y web-development aria
Ad 336x280

Web accessibility isn't a feature you tack on at the end. It's not a nice-to-have. In the EU, US, Canada, UK, and a growing list of other places, it's a legal requirement. Lawsuits over inaccessible websites have been climbing every year. But even if the law didn't exist, making your site usable by everyone is just good engineering. You don't ship a building without wheelchair ramps, and you shouldn't ship a website that only works with a mouse.

The good news: most accessibility work is basic HTML done correctly. You don't need a specialized library or expensive audit tool to cover the fundamentals.

Alt Text for Images

Every needs an alt attribute. But "needs alt text" and "writes good alt text" are different things.

<!-- Bad: describes the element, not the content -->
<img src="team.jpg" alt="image">
<img src="team.jpg" alt="photo of team">

<!-- Good: describes what the image communicates -->
<img src="team.jpg" alt="Five team members standing in front of the office building">

<!-- Decorative image: empty alt, not missing alt -->
<img src="decorative-border.png" alt="">

The purpose of alt text is to convey the same information a sighted user gets from the image. If the image is a chart, describe the data point it's making. If it's a photo of your team, describe who's there. If it's purely decorative, use alt="" (empty string) so screen readers skip it entirely. Missing the alt attribute is different -- the screen reader will read the filename instead, which is useless.

Heading Hierarchy

Headings create an outline of your page. Screen reader users navigate by heading level the way sighted users scan visually. The rules are simple:

<!-- Correct: logical hierarchy -->
<h1>Python Tutorial</h1>
  <h2>Variables</h2>
    <h3>Strings</h3>
    <h3>Numbers</h3>
  <h2>Control Flow</h2>
    <h3>If Statements</h3>
    <h3>Loops</h3>

<!-- Wrong: skipping levels for styling -->
<h1>Python Tutorial</h1>
<h4>Variables</h4> <!-- jumped from h1 to h4! -->

One

per page. Don't skip levels (h1 to h3 without h2). And never choose a heading level based on how it looks -- that's what CSS is for. An

styled to look bigger than an

is still an

to a screen reader, and the resulting page outline makes no sense.

Form Labels

Every form input needs a programmatically associated label. "Programmatically associated" means the browser knows which label goes with which input -- not just that they're visually next to each other.

<!-- Method 1: explicit label with 'for' -->
<label for="email">Email address</label>
<input type="email" id="email" name="email">

<!-- Method 2: wrapping (implicit association) -->
<label>
Email address
<input type="email" name="email">
</label>

<!-- Wrong: no association, screen reader doesn't know the label -->
<span>Email address</span>
<input type="email" name="email">

Also: placeholder text is not a label. It disappears when users start typing, it has poor contrast in most browsers, and screen readers handle it inconsistently. Use it for examples (placeholder="jane@example.com"), but always have a real label.

For groups of related inputs, use

and :

<fieldset>
  <legend>Shipping address</legend>
  <label for="street">Street</label>
  <input type="text" id="street" name="street">
  <label for="city">City</label>
  <input type="text" id="city" name="city">
</fieldset>

Keyboard Navigation

Not everyone uses a mouse. Some users rely on keyboards, switch devices, or other input methods. Your entire site should be operable with Tab, Shift+Tab, Enter, Space, and arrow keys.

Test it yourself: put your mouse in a drawer and try to use your site with only the keyboard.

Things that break keyboard navigation:

<!-- Bad: div with click handler, not focusable or activatable by keyboard -->
<div onclick="doSomething()">Click me</div>

<!-- Good: button is focusable and responds to Enter/Space -->
<button onclick="doSomething()">Click me</button>

<!-- If you MUST use a non-interactive element (you usually shouldn't): -->
<div role="button" tabindex="0" onclick="doSomething()"
onkeydown="if(event.key==='Enter')doSomething()">Click me</div>

The second example is significantly more code to replicate what

Ad 728x90