← ./articles

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 button contains another button, link, input, or role="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

References