DEV Community

Priya Nair
Priya Nair

Posted on

Beyond the Div: Leveraging Semantic HTML to Solve Complex ARIA Failures in Modern React Applications

Meta: Stop over-using ARIA. Learn how semantic HTML solves complex accessibility failures in React and how to meet WCAG 2.1 AA without the ARIA overhead.

Beyond the Div: Leveraging Semantic HTML to Solve Complex ARIA Failures in Modern React Applications

I remember a project I worked on a few years ago for a fintech client in Amsterdam. The team had built a beautiful, high-performance dashboard in React. Visually, it was stunning. But when I opened VoiceOver (a screen reader for macOS), the experience was a nightmare.

The entire application was essentially a "div soup." Every button was a div with an onClick handler; every navigation link was a span with a cursor: pointer. To "fix" the accessibility, the developers had layered on a massive amount of ARIA (Accessible Rich Internet Applications) attributes. They had role="button", tabIndex="0", and complex aria-labelledby relationships everywhere.

On paper, the code looked "compliant." In practice, it was fragile. A single typo in an ID reference broke the relationship between a label and an input, and the keyboard navigation was inconsistent because they had to manually manage focus with useEffect hooks. They were working ten times harder to simulate behavior that the browser provides for free.

The most important lesson I learned from that project—and one I share in my "Accessibility Office Hours" every month—is this: ARIA is a polyfill for missing semantics. The best ARIA is no ARIA.

If you can use a native HTML element to achieve your goal, do it. Every time you use a div where a button or nav should be, you are opting out of decades of browser and assistive technology optimization.

The "ARIA Paradox" in Modern Frameworks

In the React ecosystem, we often get caught up in the "componentization" of everything. We create <CustomButton /> or <CustomInput /> components. Because these are often wrapped in multiple layers of abstraction, developers tend to reach for ARIA roles to "tell" the screen reader what the component is.

The paradox is that the more ARIA you add to a non-semantic element, the more likely you are to introduce a bug. ARIA doesn't change the behavior of an element; it only changes how it is announced.

If you give a div a role="button", the screen reader says "Button," but the browser still doesn't know that the Enter key or the Space bar should trigger a click event. You then have to write a custom onKeyDown handler to handle those keys. If you forget one, you've just failed WCAG 2.1 Success Criterion 2.1.1 (Keyboard).

Compare that to a native <button>. It is focusable by default, it triggers on Enter and Space by default, and it is announced as a button by default. Zero lines of extra JavaScript.

The Cost of "Div Soup"

Let's look at a common failure pattern. Imagine a custom dropdown menu built with div elements.

The "Wrong" Way (The ARIA Overload)

// ❌ Fragile and over-engineered
const CustomDropdown = ({ options }) => {
  return (
    <div 
      role="combobox" 
      aria-expanded="false" 
      aria-haspopup="listbox" 
      className="dropdown-container"
      tabIndex="0"
      onKeyDown={handleKeyDown} // Manually managing Enter/Space/Arrows
    >
      Select an option...
      <div role="listbox" className="options-list">
        {options.map(opt => (
          <div 
            role="option" 
            tabIndex="-1" 
            className="option-item"
            onClick={() => selectOption(opt)}
          >
            {opt.label}
          </div>
        ))}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In the example above, we are fighting the browser. We have to manually manage tabIndex, manually handle keyboard events, and manually track the aria-expanded state. If the state logic fails, the screen reader user is left in a vacuum.

The "Right" Way (Semantic HTML)

Now, let's look at how we achieve the same result using semantic HTML. While some complex patterns truly require ARIA, many "custom" components can be simplified.

// ✅ Robust, accessible, and lightweight
const SemanticDropdown = ({ options }) => {
  return (
    <div className="field-wrapper">
      <label htmlFor="category-select">Choose a category</label>
      <select id="category-select" name="category">
        {options.map(opt => (
          <option key={opt.id} value={opt.id}>
            {opt.label}
          </option>
        ))}
      </select>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

By using <select> and <option>, we satisfy WCAG 2.1 SC 4.1.2 (Name, Role, Value) automatically. The browser handles the keyboard navigation, the focus management, and the screen reader announcement without a single line of custom ARIA.

Solving Common ARIA Failures with Semantics

Let's dive into three common areas where developers over-use ARIA and how to fix them using a semantic-first approach.

1. Clickable Elements

The Mistake: Using div or span for buttons because "it's easier to style."
The Failure: Failure of WCAG 2.1 SC 2.1.1 (Keyboard). Users who rely on keyboards cannot tab to your "button," and users with screen readers may not realize the element is interactive.

The Fix: Use a <button>. If you hate the default browser styling, use CSS to reset it.

/* Resetting button styles to make it a blank canvas */
.btn-reset {
  background: none;
  color: inherit;
  border: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
  outline: inherit;
}
Enter fullscreen mode Exit fullscreen mode
// Use the reset class to get the look you want with the functionality you need
<button className="btn-reset" onClick={doSomething}>
  Click Me
</button>
Enter fullscreen mode Exit fullscreen mode

2. Navigation Landmarks

The Mistake: Wrapping your site navigation in a div and adding role="navigation".
The Failure: While technically compliant, it's redundant. If you forget the role on one page, you break the landmark structure.

The Fix: Use the <nav> element. It provides the landmark role implicitly.

// Instead of <div role="navigation">
<nav aria-label="Main Navigation">
  <ul>
    <li><a href="/home">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>
Enter fullscreen mode Exit fullscreen mode

Note: Adding aria-label to the <nav> is important if you have multiple navigation blocks (e.g., Header and Footer) so users can distinguish between them.

3. Form Labels and Associations

The Mistake: Using a div as a label and trying to connect it using aria-labelledby.
The Failure: If the ID is duplicated or missing, the association is lost. This violates WCAG 2.1 SC 1.3.1 (Info and Relationships).

The Fix: Use the <label> element with the htmlFor attribute.

// ❌ Fragile
<div id="label-1">Email Address</div>
<input aria-labelledby="label-1" type="email" />

// ✅ Robust
<label htmlFor="email-field">Email Address</label>
<input id="email-field" type="email" />
Enter fullscreen mode Exit fullscreen mode

When is ARIA Actually Necessary?

I don't want you to think ARIA is "evil." It is an incredibly powerful tool. However, it should be your last resort, not your first step.

You should use ARIA when:

  1. You are building a complex widget that HTML doesn't have a native equivalent for (e.g., a Tab panel, a Tree view, or a Modal dialog).
  2. You need to communicate dynamic changes that aren't visually obvious (e.g., using aria-live to announce a successful form submission without moving the user's focus).
  3. You are enhancing a native element (e.g., adding aria-invalid="true" to an input when a validation error occurs).

Even then, follow the First Rule of ARIA: If you can use a native HTML element or attribute with the semantics and behavior you require already built-in, do so.

A Practical Workflow for React Developers

If you're working in a fast-paced startup environment, you don't have time to read the entire WAI-ARIA specification every morning. Here is the workflow I use when building new components:

  1. The Semantic Search: Ask, "Is there an HTML element that does this?" (e.g., Instead of a div with a click handler $\rightarrow$ <button>. Instead of a div for a list $\rightarrow$ <ul> and <li>).
  2. The Keyboard Test: Unplug your mouse. Can you reach the element? Can you trigger it? Does the focus indicator (the outline) clearly show where you are?
  3. The Screen Reader Audit: Use VoiceOver (Mac) or NVDA (Windows). Does the element announce its purpose and state?
  4. The ARIA Layer: Only now, if the behavior is missing or the announcement is vague, add the minimum amount of ARIA required to bridge the gap.

Accessibility Essentials Checklist

To make this actionable, here is a quick reference for your next PR review:

Instead of... Use... Why? WCAG Criteria
<div onClick={...}> <button> Native focus & keyboard support 2.1.1
<div className="header"> <header> Structural landmark for screen readers 1.3.1
<span className="link"> <a> Proper navigation and SEO 2.1.1 / 4.1.2
<div role="main"> <main> Implicit landmark 1.3.1
<div> (as a list) <ul> / <li> Announces number of items in the list 1.3.1

Final Thoughts: Accessibility is a Business Requirement

Some developers argue that using native elements limits their design flexibility. This is a myth. CSS is powerful enough to make a <button> look like anything—a pill, a square, a ghost button, or a complex icon. What you cannot "style" is the underlying accessibility tree that a screen reader relies on.

When we prioritize semantic HTML, we aren't just "checking a box" for compliance. We are ensuring that a user with a motor impairment can navigate our site, a blind user can understand our layout, and a user with a cognitive disability can predict how our interface behaves.

Building inclusive software isn't about adding features; it's about removing barriers. Start with the basics. Stop the "div soup." Let the browser do the heavy lifting.


Let's Discuss

What is the most frustrating "custom" component you've encountered that should have just been a native HTML element? Or, have you found a specific case where ARIA was the only way to solve a problem? Let's talk in the comments.

About the Author:
Priya Nair is a Senior Frontend Developer and Accessibility Consultant based in Amsterdam. She specializes in WCAG 2.1 compliance and inclusive design patterns for React applications. She is a frequent speaker at a11y conferences and maintains several open-source accessibility checklists on GitHub.

Top comments (0)