January 17 2025
Accessible React Components
How to build interactive React components while preserving semantics, focus, and native behavior.
Andrews Ribeiro
Founder & Engineer
5 min Intermediate Frontend
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
divpretending to be a button - a clickable
spanwithonClick tabIndex={0}used to compensate for bad structurerole="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:
asChildas- 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
divjust 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
- An accessible component starts with the right element, not with a pile of `role`, `tabIndex`, and `aria-*` patches.
- A good abstraction preserves native behavior instead of rebuilding it badly with JavaScript.
- A component API is also an accessibility responsibility, because one bad choice there scales across the team.
- In interviews, strong answers show component modeling judgment, not just attribute knowledge.
Practice checklist
Use this when you answer
- Can I explain when my component should render a `<button>`, an `<a>`, or another native element?
- Can I spot when a React abstraction is erasing important native behavior?
- Can I review a component API while thinking about keyboard, focus, and announced state?
- Can I tell when `aria-*` is describing something real and when it is trying to hide a broken foundation?
You finished this article
Share this page
Copy the link manually from the field below.