Fix Nested Interactive Elements in React with Sibling Buttons
Nested interactive elements are usually a structure problem, not a keyboard-handler problem.
A common React component starts as one clickable chip or card. Later it gains a remove button, menu button, or secondary action. The quick implementation nests another interactive element inside the first one. That creates invalid HTML and confusing accessibility behavior.
The fix is usually to make the container non-interactive and place sibling buttons inside it.
The symptom: one component has two click targets
Typical examples:
- recent project chip: select and remove
- file tab: activate and close
- card: open and favorite
- list row: select and show menu
The broken shape often looks like this:
export function RecentChip({ path, onSelect, onRemove }) {
return (
<button onClick={onSelect}>
<span
role="button"
tabIndex={0}
onClick={onRemove}
>
x
</span>
{path}
</button>
);
}
This creates an interactive descendant inside an interactive ancestor.
Refactor to a non-interactive wrapper
Use a list item or div as the wrapper, then put two native buttons next to each other:
export function RecentChip({ path, onSelect, onRemove }) {
return (
<li className="recent-chip">
<button className="chip-main" onClick={onSelect}>
{path}
</button>
<button
className="chip-remove"
aria-label={`Remove ${path}`}
onClick={onRemove}
>
x
</button>
</li>
);
}
Now each action has its own native semantics, focus target, and keyboard behavior.
Do not patch the nested span with more handlers
This is usually the wrong direction:
<span
role="button"
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") onRemove();
}}
>
x
</span>
That may improve one symptom, but it keeps the nested interactive structure. Native buttons already handle Enter, Space, disabled state, focus, and role.
Preserve layout with CSS, not invalid HTML
The sibling structure can still look like one chip:
.recent-chip {
display: inline-flex;
align-items: center;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
}
.chip-main,
.chip-remove {
border: 0;
background: transparent;
}
Visual grouping does not require semantic nesting.
Verification checklist
Check that:
- no
buttoncontains anotherbutton, link, input, orrole="button" - both actions are reachable by Tab
- both actions have visible focus
- Enter and Space work without custom keyboard code
- remove buttons have useful accessible names
- axe or Accessibility Insights no longer reports nested interactive controls