Skip to main content

Accessible React Components

How to build interactive React components while preserving semantics, focus, and native behavior.

Andrews Ribeiro

Andrews Ribeiro

Founder & Engineer

The problem

There is a very common mistake in modern frontend work:

the team builds a nice, flexible React component with lots of props, but destroys the interaction model in the process.

Then you start seeing things like:

  • a div pretending to be a button
  • a clickable span with onClick
  • tabIndex={0} used to compensate for bad structure
  • role="button" trying to rescue something that already started crooked

On screen, that can still look acceptable.

But the component stops inheriting behavior that the browser already handled well.

And the worst part is this: when that mistake enters a design system or internal library, it spreads across the whole product.

Mental model

Think about it like this:

An accessible component is not a visual shell with extra attributes. It is an abstraction that preserves the intent and behavior of the right element.

If the abstraction forces you to rebuild in JavaScript what native HTML already gave you for free, there is a good chance it is making the foundation worse.

In React, that matters even more because a component becomes part of the team’s API.

So the better question is not only:

  • “does this component work here?”

The better question is:

  • “does this component stay safe when other people start using it in different contexts?”

Breaking it down

Start with intent, not appearance

Before styling, animation, or prop API, it helps to answer:

  • does this perform an action on the current screen?
  • does this navigate somewhere else?
  • does this open or close some context?

If it performs an action, the natural starting point is usually <button>.

If it navigates, the natural starting point is usually <a>.

When you start from appearance instead of intent, semantics become an accident.

A native element buys a lot of behavior at once

A real button already gives you:

  • focus
  • keyboard activation
  • a clear semantic role
  • better integration with assistive technologies

When you swap that for a div, you have to rebuild almost all of it. And most teams rebuild only half of it.

An API that is too flexible often hides cost

This matters a lot in React.

Some components accept:

  • asChild
  • as
  • any tag
  • any prop

That can be useful.

But it can also make the wrong usage too easy without anyone noticing early.

If the component can become almost anything without protecting essential behavior, it looks powerful and becomes dangerous at the same time.

aria-* does not repair a broken foundation

ARIA is useful when you need to describe something that HTML alone cannot express well.

But ARIA does not turn a bad foundation into a good one.

If a button became a div, the first problem is not a missing aria-label.

The first problem is that the abstraction abandoned the right element.

Stateful components need extra clarity

When the component changes state, the user needs to be able to perceive that clearly.

Common examples:

  • a toggle button
  • an accordion
  • an expandable menu
  • a modal trigger

In those cases, it is not enough to click and hope the visual layer explains everything.

You need to think about:

  • state exposed in a coherent way
  • predictable focus
  • a clear accessible name
  • the relationship between trigger and content

Simple example

Imagine an IconButton component that opens a search modal.

Weak version:

function IconButton({ onClick, children }) {
  return <div onClick={onClick}>{children}</div>;
}

It looks flexible.

But it already started in debt:

  • it is not a real button
  • it does not inherit keyboard behavior correctly
  • it does not communicate its role clearly
  • it will need patch after patch whenever the team wants to use it properly

Better version:

function IconButton({ onClick, label, children }) {
  return (
    <button type="button" onClick={onClick} aria-label={label}>
      {children}
    </button>
  );
}

Now the abstraction preserves the right foundation.

After that, you can talk about style, variant, size, loading, disabled state, and the rest of the API. But the base is no longer wrong.

Common mistakes

  • building a base component with div just because it is convenient
  • assuming role="button" solves everything
  • testing the abstraction only with mouse clicks and ignoring keyboard behavior
  • exposing such a generic API that wrong usage becomes easy
  • reaching for aria-* too early instead of fixing semantics and behavior
  • treating accessibility as a final adaptation instead of a modeling requirement

How a senior thinks

More experienced engineers are suspicious of their own abstractions.

Especially shared components.

The question stops being:

  • “did we manage to encapsulate it?”

and becomes:

  • “does this encapsulation preserve the native contract, or is it hiding debt the team will pay for later?”

Seniority here shows up in two places:

  • choosing the base element with more discipline
  • limiting the API so the correct usage becomes easier than the wrong one

That matters because reusable components multiply quality when they start well and multiply problems when they start crooked.

What the interviewer wants to see

When this topic shows up in interviews, the interviewer usually wants to see whether you:

  • understand that UI abstraction cannot erase semantics
  • choose the element based on functional role
  • think about focus and keyboard as part of the component contract
  • can tell the difference between legitimate ARIA usage and late patching

A strong answer often sounds like this:

In React components I try to preserve as much native behavior as possible. If the abstraction forces me to rebuild keyboard, focus, and semantic role manually, I already suspect I started from the wrong element. The API should help the team use the right component in the right way.

An accessible component is not the one patched at the end. It is the one that already starts hard to misuse.

If the abstraction breaks native behavior, it is not simplifying. It is outsourcing the problem.

Quick summary

What to keep in your head

Practice checklist

Use this when you answer

You finished this article

Next article How to Think About Tickets and Tasks Previous article Keyboard Navigation and Focus

Keep exploring

Related articles