Back to blog
Tutorial

Web accessibility: the basics every dev ignores

By Flávio Emanuel · · 9 min read

Accessibility isn’t a favor. It’s a requirement.

Most devs treat accessibility as an extra. Something to do “if there’s time left.” In practice, there never is. The site goes live without alt text on images, without adequate contrast, without keyboard navigation, and without correct semantic HTML.

The result: 15% of the world population has some type of disability. That’s over 1 billion people. Part of this audience uses screen readers, navigates by keyboard, or depends on high contrast to read content. If the site isn’t accessible, these people can’t use it.

Beyond the ethical argument, there’s the legal one. In the US, web accessibility lawsuits surpass 4,000 per year. In Brazil, the Brazilian Inclusion Law (Law 13.146/2015) requires sites to be accessible. In the EU, the European Accessibility Act takes effect in 2025. Enforcement is increasing everywhere.

And there’s the SEO argument. Google uses accessibility signals for ranking. Image alt text, correct heading hierarchy, semantic HTML, adequate contrast. Everything that improves accessibility improves SEO. They’re the same practices.

Semantic HTML: the foundation nobody respects

Semantic HTML means using the right tags for the right content. <nav> for navigation, <main> for main content, <article> for articles, <button> for buttons. Seems obvious, but the number of sites using <div> for everything is large.

The problem with <div> for everything: screen readers don’t know what that element is. A <div onclick="..."> looks like a button visually, but the screen reader doesn’t announce it as a button, doesn’t respond to the Enter key, and doesn’t appear in the list of interactive elements.

<!-- Wrong -->
<div class="btn" onclick="submit()">Submit</div>

<!-- Right -->
<button type="submit">Submit</button>

The difference seems small in the code. For the experience of someone using a screen reader, it’s the difference between being able to use the site or not.

Minimum semantic structure every site needs:

<header>
  <nav>...</nav>
</header>
<main>
  <h1>Unique page title</h1>
  <section>
    <h2>Section</h2>
    ...
  </section>
</main>
<footer>...</footer>

This costs zero extra development time. It’s the difference between HTML that makes sense and HTML that’s just visual.

With GPM2, Family Pilates, and all my Astro projects, semantic structure comes by default. <header>, <nav>, <main>, <footer>, headings in correct hierarchy. It’s not an extra feature. It’s how HTML should be written from the start.

Alt text: the most ignored tag on the web

Every image needs an alt attribute. No exception. Alt text is what the screen reader reads when it encounters an image. Without it, the person hears “image” and doesn’t know what it is.

Practical rules for alt text:

Informative images: describe what the image shows. “Pilates studio with reformer equipment and mirror” is better than “studio photo.”

Decorative images: use alt="" (empty alt, not missing alt). This tells the screen reader to skip the image. Decorative icons, backgrounds, and visual separators go here.

Images with text: include the image text in the alt. If the image says “30% discount,” the alt needs to say “30% discount.”

Logos: “Logo of [company]” is sufficient.

Alt text also helps SEO. Google reads alt to understand image content. Natural keyword in alt (when it makes sense) improves ranking in Google Images. I detailed character limits for alt text in the post about technical SEO for developers: between 80 and 125 characters, descriptive and with keyword when possible.

Contrast: if you can’t read it, it doesn’t exist

WCAG (Web Content Accessibility Guidelines) defines minimum contrast ratios between text and background:

  • Normal text: minimum ratio of 4.5:1
  • Large text (18px+ bold or 24px+ regular): minimum ratio of 3:1
  • Interactive elements (buttons, links, inputs): minimum ratio of 3:1

In practice: light gray text (#999) on white background (#FFF) has a ratio of 2.85:1. Fails. Dark gray text (#595959) on white background has a ratio of 7:1. Passes easily.

Minimalist design with pastel tones and low contrast looks good on Dribbble and is unusable for people with low vision. And “low vision” isn’t just a diagnosed condition. It’s anyone over 45 using their phone in sunlight.

Free tool to test: WebAIM Contrast Checker (webaim.org/resources/contrastchecker). Enter the text and background colors and it shows whether it passes or not. Takes 10 seconds per color pair.

In Lighthouse, the accessibility audit already checks contrast automatically. If the accessibility score is below 90, there’s probably a contrast issue.

Keyboard navigation: the 30-second test

Open your site and try navigating using only the keyboard. Tab to move forward between elements, Shift+Tab to go back, Enter to activate. If you can’t access all links, buttons, and forms, keyboard users can’t either.

Common problems:

Invisible focus. The browser shows a blue outline on the focused element. Many devs remove it with outline: none because “it’s ugly.” This removes the visual indicator for keyboard users. Never remove outline without replacing it with another visible indicator.

/* Wrong */
*:focus { outline: none; }

/* Right */
*:focus-visible {
  outline: 2px solid #0F8A6B;
  outline-offset: 2px;
}

focus-visible shows the outline only when the user is navigating by keyboard, not when clicking with mouse. Best of both worlds.

Incoherent tab order. Focus jumps from one corner to another because the HTML isn’t in visual order. Fix by organizing HTML in logical reading order. Don’t use positive tabindex (tabindex=“1”, tabindex=“2”). Use the natural DOM order.

Non-focusable interactive elements. <div> with click handler doesn’t receive keyboard focus. Use <button> or <a> for interactive elements. If you need div for some reason, add tabindex="0" and role="button".

Modal that traps focus incorrectly. Opened a modal? Focus needs to stay inside it until it closes. If the user presses Tab and focus goes to the page behind the modal, the experience breaks. Use inert on content behind the modal or implement focus trap.

Accessible forms

A form without labels is an inaccessible form. The screen reader needs the <label> associated with the <input> to announce what each field expects.

<!-- Wrong: placeholder doesn't replace label -->
<input type="email" placeholder="Your email">

<!-- Right -->
<label for="email">Your email</label>
<input type="email" id="email" name="email">

Placeholder disappears when the user starts typing. If it was the only indication of what the field asks for, the user forgets what they should fill in. Label is permanent.

Error messages need to be associated with the field. Use aria-describedby:

<label for="email">Email</label>
<input type="email" id="email" aria-describedby="email-error">
<span id="email-error" role="alert">Invalid email</span>

The screen reader announces the error linked to the field. Without this association, the person knows there’s an error on the page but doesn’t know in which field.

With AutoPars, registration and checkout forms follow these practices. Visible labels, associated error messages, and full keyboard navigation. It’s a marketplace where sellers list parts from their phone, often with one hand. Accessibility here isn’t about disability, it’s about usability in real context.

ARIA: use sparingly

ARIA (Accessible Rich Internet Applications) are attributes that add accessibility information to HTML elements. The problem: misused ARIA is worse than no ARIA.

The first rule of ARIA: if you can use native semantic HTML, don’t use ARIA. <button> is already accessible. <div role="button"> is a worse version of the same thing.

Where ARIA makes sense:

  • aria-label for elements without visible text (hamburger menu icon: aria-label="Open menu")
  • aria-expanded for dropdown menus (indicates whether open or closed)
  • aria-live for dynamically updating content (notifications, counters)
  • role="alert" for error messages

Where ARIA doesn’t make sense:

  • role="button" on <div> (use <button>)
  • role="navigation" on <div> (use <nav>)
  • aria-label repeating the visible text of the element (redundant)

How to test accessibility

You don’t need expensive tools. The basics are covered with:

Lighthouse (Chrome DevTools). Runs automatic accessibility audit. Catches contrast issues, missing alt text, heading hierarchy problems, and elements without labels.

Keyboard navigation. Open the site and navigate only with Tab and Enter for 2 minutes. If you can’t access everything, there’s a problem.

Screen reader. On Mac, VoiceOver comes installed (Cmd+F5). On Windows, NVDA is free. Turn on the reader and try to use the site. The experience is revealing.

axe DevTools (Chrome extension). Runs a more detailed audit than Lighthouse. Shows exactly which element has a problem and how to fix it.

The ideal is to test with at least two of these methods on every project before delivery. In my workflow, Lighthouse accessibility above 90 is the minimum target, along with manual keyboard testing.

Minimum accessibility checklist

  • Semantic HTML (header, nav, main, footer, section)
  • Single H1 per page, H2 > H3 hierarchy without skipping levels
  • Alt text on all images (descriptive or empty for decorative)
  • Minimum contrast 4.5:1 for normal text
  • Full keyboard navigation (Tab, Enter, Escape)
  • Visible focus on all interactive elements
  • Labels on all form fields
  • Error messages associated with field via aria-describedby
  • Page language defined (<html lang="en">)
  • Lighthouse Accessibility above 90

None of these items add significant development time. Most are correct HTML from the start, not retroactive fixes. The cost of implementing is nearly zero. The cost of not implementing is excluding millions of people from your site.

Next step

Need a dev who truly delivers?

Whether it's a one-time project, team reinforcement, or a long-term partnership. Let's talk.

Chat on WhatsApp

I reply within 2 hours during business hours.