Semantic HTML as the Foundation of Accessibility
AI generated
A11Y
WCAG
Accessibility · Semantic HTML · WCAG · Magento 2
Semantic HTML as the Foundation of Accessibility
Why the right element beats any ARIA attribute

Building interactive controls out of a generic div or span throws away keyboard support, focus order, and screen reader announcements, forcing you to rebuild all of it laboriously with ARIA. Native HTML elements like button, a, ul, and h2 deliver these properties for free. This article shows how to spot semantic mistakes in Magento and Hyvä templates and replace them with the right elements.

16 min read Semantic HTML · ARIA · Keyboard Accessibility WCAG 2.2 · Screen Readers · Hyvä Theme

1. Why semantic HTML is the foundation of accessibility

Semantic HTML tells the browser, and through it every assistive technology, what an element means, without developers having to rebuild that meaning themselves. Every native element such as button, a, or select is automatically translated by the browser into the so-called accessibility tree: a data structure carrying role, name, state, and value that screen readers, voice control software, and braille displays read directly. This translation happens invisibly during browser rendering and costs the developer not a single line of code.

div and span, by contrast, are meaningless containers with no role in the accessibility tree at all. Attach a click handler to a div and style it with CSS to look like a button, and you get an element that works fine for sighted mouse users but simply does not exist for keyboard users and screen reader users. This exact gap between visual appearance and actual semantics is the most common cause of WCAG violations in grown Magento and Hyvä templates, and it can almost always be fixed by switching to the correct native element.

2. The principle: no ARIA is better than bad ARIA

The W3C Accessibility API Mappings spell out what is known as the "First Rule of ARIA Use": if a native HTML element or attribute already exists with the semantics and behavior you want, use it instead of rebuilding it with a generic element plus ARIA. The reason is simple: ARIA attributes such as role or aria-expanded only tell assistive technology what role and state an element has. They add no behavior whatsoever. A div with role="button" is announced as a button to a screen reader, but without additional JavaScript it responds to neither Enter nor Space, and without tabindex it cannot even be reached by Tab.

Incorrect ARIA is even more dangerous: a role or aria-* attribute that does not match an element's actual behavior actively lies to assistive technology and produces a worse experience than no ARIA at all. An aria-expanded="false" that never switches to true after a click, or a role="dialog" without any focus management, are typical examples where "no ARIA" would genuinely have been the safer option.


<!-- WRONG: generic div with hand-rolled ARIA behavior -->
<div class="accordion-trigger" role="button" aria-expanded="false" onclick="toggle()">
  Show shipping costs
</div>
<div class="accordion-panel" hidden>...</div>
<!-- Keyboard support, focus ring, and Enter/Space handling are all missing -->

<!-- RIGHT: native button element, no extra ARIA needed -->
<button type="button" aria-expanded="false" aria-controls="shipping-panel" x-on:click="toggle()">
  Show shipping costs
</button>
<div id="shipping-panel" hidden>...</div>

<!-- EVEN SIMPLER: details/summary need no ARIA at all -->
<details>
  <summary>Show shipping costs</summary>
  <p>Shipping within Germany costs EUR 4.90.</p>
</details>

Almost every frontend audit surfaces the same pattern: a div with an onclick handler, styled with CSS to look like a button, but lacking tabindex, role, and any keyboard handling. To mouse users everything looks normal, while keyboard users simply cannot reach the function at all. The second classic is a span with a click handler that looks visually like a link: no href, so no real link target, no entry in the tab order, no status bar preview on hover, no opening in a new tab via middle click or context menu.

The decisive difference between a and button is semantic, not cosmetic: a link navigates to a new resource or anchor point, while a button triggers an action on the current page, such as submitting a form or opening a menu. A link without an href attribute is, incidentally, no longer an interactive element at all, just plain text, because it is the href that makes it keyboard focusable in the first place. Consistently observing this distinction resolves most div-as-button and span-as-link problems immediately, without a single ARIA attribute required.


<!-- WRONG: span disguised as a link -->
<span class="link-style" onclick="location.href='/cart'">Go to cart</span>

<!-- RIGHT: real a element with href -->
<a href="/cart" class="link-style">Go to cart</a>

<!-- WRONG: div disguised as an action button -->
<div class="btn-primary" onclick="addToCart(productId)">Add to cart</div>

<!-- RIGHT: button element for an action without navigation -->
<button type="button" class="btn-primary" x-on:click="addToCart(productId)">
  Add to cart
</button>

4. Keyboard support: what semantic HTML delivers automatically

A native button element is reachable by Tab without a single line of JavaScript, responds to both Enter and Space, shows a visible focus ring when focused, and reports its role and label to a screen reader automatically. An a element with href responds only to Enter, not Space, because links have traditionally never been activated with the space bar. These details sound minor, but they decide whether a component feels correct to keyboard users or not.

Anyone who makes a div interactive instead has to rebuild all of this by hand: tabindex="0" for focusability, a keydown listener that responds to both Enter and Space, an event.preventDefault() on Space so the page does not scroll down, and often manual focus styling as well, because developers like to strip the native focus ring with outline: none without providing an equivalent replacement. This exact removal of the focus ring without a replacement is one of the most common WCAG violations of all, whether the element in question is native or hand-rolled.


/* WRONG: focus ring removed, no replacement present */
.custom-widget:focus {
  outline: none;
}

/* RIGHT: focus ring visible only for keyboard users, with a clear replacement */
.custom-widget:focus {
  outline: none;
}
.custom-widget:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
  border-radius: 4px;
}

/* Native elements like button and a already ship with sensible defaults
   that are usually best left alone */
button,
a {
  outline-offset: 2px;
}

5. Forms: using label, fieldset, and native inputs correctly

A <label for="..."> tied via the for attribute to an input's id gives that field a programmatically determinable name, enlarges the clickable area to the entire label, and is read aloud automatically by a screen reader when the field receives focus. Without this connection, for instance when only a visually adjacent span or div serves as the caption, the field remains nameless for assistive technology, even though it looks clearly labeled to sighted users.

fieldset and legend group related form controls, such as a set of radio buttons for shipping method, and give that group a shared heading that the screen reader announces alongside every single group member. Custom checkboxes built from divs that mimic state via JavaScript may avoid the hard-to-style native checkbox, but they lose native validation, aria-checked synchronization, and form submission via name/value, all of which work automatically with a real input[type=checkbox].


<form>
  <div class="mb-4">
    <label for="email" class="block font-semibold mb-1">Email address</label>
    <input
      type="email"
      id="email"
      name="email"
      required
      aria-describedby="email-error"
      class="border border-gray-300 rounded px-3 py-2 w-full"
    >
    <p id="email-error" class="text-red-600 text-sm mt-1" x-show="errors.email">
      Please enter a valid email address.
    </p>
  </div>

  <fieldset class="border border-gray-300 rounded p-4">
    <legend class="font-semibold px-2">Shipping method</legend>
    <label class="flex items-center gap-2 mb-2">
      <input type="radio" name="shipping" value="standard" checked>
      Standard shipping (free)
    </label>
    <label class="flex items-center gap-2">
      <input type="radio" name="shipping" value="express">
      Express shipping (EUR 9.90)
    </label>
  </fieldset>
</form>

6. Heading hierarchy and landmark elements

Screen reader users rarely navigate a page linearly from top to bottom. Instead they jump through a list of all headings straight to the section they want, similar to a table of contents. This list only works if h1 through h6 actually reflect the content structure and are not skipped, or chosen purely for their visual font size. An h4 used only because it renders smaller instead of an h2 with matching CSS classes destroys this navigation aid, without sighted users ever noticing the difference.

On top of that, landmark elements such as header, nav, main, aside, and footer divide the page into named regions that screen reader users can jump between with a keyboard shortcut, instead of tabbing through every single menu item. One main element per page is mandatory, and multiple nav regions should be distinguishable via aria-label, for example "Main navigation" and "Breadcrumb", so the list of landmarks stays meaningful to the user instead of consisting of several identically named entries.

7. Lists and tables: structure instead of visual approximation

A real ul with li elements is announced by a screen reader as a list with position information, such as "item 3 of 7". If several div elements with a CSS bullet icon are used to visually mimic a list instead, that information is lost entirely: the user learns neither that it is a list, nor how many items it contains. For navigations, product attributes, and cart line items, the rule is therefore: the semantically correct list first, the CSS styling second.

For data tables, tying th cells to the scope attribute is decisive: scope="col" for column headers and scope="row" for row headers ensure that a screen reader automatically announces the associated column and row heading as the user navigates each cell. Without this mapping, a larger table becomes practically unreadable with a screen reader, because the relationship between a cell's value and its meaning is lost. A caption element additionally describes the purpose of the entire table.


<table>
  <caption>Available shipping options and delivery times</caption>
  <thead>
    <tr>
      <th scope="col">Shipping method</th>
      <th scope="col">Delivery time</th>
      <th scope="col">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Standard</th>
      <td>3 to 5 business days</td>
      <td>free</td>
    </tr>
    <tr>
      <th scope="row">Express</th>
      <td>1 business day</td>
      <td>EUR 9.90</td>
    </tr>
  </tbody>
</table>

8. When ARIA is actually needed

ARIA is not fundamentally wrong, it is designed for exactly the cases where no native element exists with the right semantics. A cookie notice that gets injected via JavaScript while the page is loading, without the user reloading anything, needs aria-live="polite" so the screen reader announces the new information automatically. A hand-built modal needs role="dialog", aria-modal="true", and a focus-trap script, because HTML has no native modal element with this behavior, with the exception of the native dialog element combined with showModal(), which now handles this work itself.

Complex widgets like tabs, comboboxes, or tree views strictly require ARIA roles such as role="tablist", role="tab", and role="tabpanel", because the browser provides no built-in semantics for these patterns. It is important to follow the W3C's ARIA Authoring Practices Guide (APG) closely here, since every one of these roles demands an exact keyboard interaction pattern, for example arrow keys to move between tabs and a so-called roving tabindex technique, where only a single element in the group ever sits in the tab focus while arrow keys move the internal focus.

9. Practical audit: spotting and fixing common mistakes

Automated tools such as axe DevTools or Lighthouse, by their own documentation, reliably cover only around 30 to 40 percent of all WCAG success criteria, because semantic correctness frequently requires contextual knowledge a tool cannot verify automatically. A solid manual audit therefore always starts with the keyboard: operate the entire page using only Tab, Shift+Tab, Enter, and arrow keys, and check whether every interactive element is reachable and shows a visible focus.

The Chrome DevTools "Accessibility" tab shows the actual role, name, and state in the accessibility tree for any selected element, regardless of how it looks visually. If the role shown there differs from the visual impression, for example "generic" instead of "button", that is a reliable sign of a div-as-button problem. A short screen reader spot check with VoiceOver or NVDA, specifically walking through the lists of headings and landmarks, helps confirm the findings. The table below summarizes the most common anti-patterns from audits like these.

Anti-Pattern Wrong Right Advantage of the native element
Clickable button <div onclick> <button> Focus, keyboard, and role automatically
Linked text <span onclick> <a href> Status bar, context menu, tab focus
Section title <div class="heading"> <h2> Appears in heading navigation
Bullet list <div> with bullet icon <ul><li> List length and position are announced
Form field <div contenteditable> <input> + <label> Native validation and label association
Dropdown selection Custom <div> popup <select> Keyboard, mobile picker, no ARIA needed

Mironsoft

Accessibility audits and WCAG-compliant implementation for Magento and Hyvä stores

A semantically clean, accessible frontend?

We review your Magento and Hyvä templates for div-as-button patterns, missing form labels, and heading errors, and replace them with semantically correct, WCAG-compliant markup.

Semantics audit

Accessibility tree analysis and manual keyboard testing of every component

Template refactoring

Fixing div-as-button, missing labels, and heading jumps in phtml

WCAG documentation

Traceable audit reports for BFSG and WCAG 2.2 compliance

10. Summary

Semantic HTML is not a matter of style, it is the foundation of every accessible application: native elements like button, a, ul, h2, and select deliver focusability, keyboard support, focus rings, and screen reader semantics automatically, for free and without ARIA. The "First Rule of ARIA Use" sums it up neatly: no ARIA is better than bad ARIA, because a div with a wrong or incomplete ARIA role actively lies to assistive technology. Div-as-button and span-as-link are the most common anti-patterns in grown templates and can almost always be fixed by switching to the right element.

ARIA still remains indispensable for cases without a native equivalent: live regions, modals, and complex widgets like tabs or comboboxes need explicit roles and a full keyboard interaction pattern as specified by the APG. A solid audit combines automated tools, which cover only part of the WCAG criteria, with manual keyboard testing and a look at the accessibility tree in the browser's DevTools. Anyone who consistently follows this order, the right native element first, ARIA only where necessary afterward, builds interfaces that work for all user groups without extra effort.

Semantic HTML as the Foundation of Accessibility: the essentials at a glance

No ARIA is better than bad ARIA

Check for a native element first. Wrong or incomplete ARIA is worse than none at all.

div/span are not buttons or links

button for actions, a href for navigation. Both deliver keyboard support and focus for free.

Structure first, styling second

Real headings, lists, and tables with scope instead of a purely visual CSS approximation.

Check manually, not only automated

Keyboard testing, the accessibility tree in DevTools, and screen reader spot checks complement axe/Lighthouse.

11. FAQ: Semantic HTML as the Foundation of Accessibility

1What does no ARIA is better than bad ARIA mean?
The W3C's First Rule of ARIA Use: native element before ARIA rebuild. Wrong ARIA lies to assistive technology and is worse than having no markup at all.
2Why is a button better than a div with onclick?
button is focusable without extra code, responds to Enter/Space, shows a focus ring, and reports role and name automatically. A div with onclick has none of that by default.
3What happens with role=button on a div?
The screen reader announces a button, but without JavaScript the element does not respond to keyboard input. ARIA only conveys semantics, it adds no behavior.
4Keyboard support without ARIA?
Use the matching native element: button, a with href, input/select/textarea. All are keyboard operable by default, with no ARIA attributes needed.
5When is ARIA actually necessary?
Without a native equivalent: live regions, hand-built modals, complex widgets like tabs or comboboxes, each with the full keyboard pattern per the APG.
6Difference between a and button?
a navigates to a new resource, button triggers an action on the current page. This meaning should drive the choice, not the appearance.
7Why do heading levels matter?
Screen reader users jump directly to sections via a heading list. Choosing levels purely for font size destroys this navigation aid unnoticed.
8How do I audit my HTML for mistakes?
First test with keyboard only, then check the Accessibility tab of DevTools. Automated tools like axe complement this, but cover only part of the WCAG criteria.
9What is roving tabindex?
For tabs or listboxes, only one element sits in the tab focus, all others get tabindex=-1. Arrow keys move the internal focus, per the ARIA Authoring Practices Guide.
10Is semantic HTML enough for WCAG compliance?
It is the most important foundation, but not everything. Contrast, alternative text, and, where needed, correct ARIA are also part of full WCAG 2.2 compliance.