Keyboard Inaccessible Elements
Some parts of your site can only be reached with a mouse. Keyboard users are locked out. Here's how to open the door.
What is this?
Imagine a building where some doors can only be opened with a very specific key — and if you do not have that exact key, you cannot get through. That is what keyboard-inaccessible elements are like. Parts of your site — buttons, menus, modals, custom controls — that can only be activated with a mouse click. If someone navigates with a keyboard (using Tab, Enter, and Arrow keys), they simply cannot reach or use those elements.
Why it matters
- For your visitors: Keyboard navigation is not a niche concern. People with motor disabilities, repetitive strain injuries, tremors, or temporary injuries (broken arm, anyone?) all rely on keyboards. Power users who prefer keyboard shortcuts also benefit. If your custom dropdown, modal, or interactive widget cannot be reached with Tab and activated with Enter, those visitors are stuck.
- For your business: Keyboard accessibility is a legal requirement in many jurisdictions. Beyond compliance, inaccessible controls mean lost conversions. If someone cannot tab to your "Sign Up" button, they cannot sign up.
- The standard: WCAG 2.1.1 (Keyboard) requires that all functionality is operable through a keyboard interface. No exceptions for "but it works with a mouse."
<!-- Native button: keyboard-accessible by default -->
<button onclick="doSomething()">Click me</button>
<!-- If you MUST use a div, add role + tabindex + keyboard handler -->
<div
role="button"
tabindex="0"
onclick="doSomething()"
onkeydown="if(event.key === 'Enter' || event.key === ' ') doSomething()"
>
Click me
</div><!-- div with click handler: invisible to keyboard users -->
<div onclick="doSomething()">Click me</div>
<!-- span pretending to be a link: not focusable -->
<span onclick="navigate('/about')" style="color: blue; cursor: pointer;">
About us
</span>How to fix it
React / Next.js
The golden rule: use native HTML elements whenever possible. <button>, <a>, <input>, <select> are all keyboard-accessible by default. You only need extra work when building custom components.
// src/components/card.tsx
// GOOD: Use a button for actions
export function ActionCard() {
return (
<div className="border p-4">
<h3>Premium Plan</h3>
<p>Everything you need.</p>
{/* Native button — keyboard accessible automatically */}
<button
onClick={() => subscribe("premium")}
className="mt-4 rounded bg-blue-600 px-4 py-2 text-white"
>
Subscribe
</button>
</div>
);
}
// BAD: Making the entire card clickable with a div
// If you need a clickable card, use an anchor or button inside it:
export function ClickableCard() {
return (
<div className="border p-4">
<h3>
{/* The link inside makes the card content accessible */}
<a href="/premium" className="after:absolute after:inset-0">
Premium Plan
</a>
</h3>
<p>Everything you need.</p>
</div>
);
}For custom interactive components, handle keyboard events:
// src/components/custom-toggle.tsx
export function CustomToggle({ isOn, onToggle }: { isOn: boolean; onToggle: () => void }) {
return (
<div
role="switch"
aria-checked={isOn}
tabIndex={0}
onClick={onToggle}
onKeyDown={(e) => {
// Space and Enter should activate it, just like a real button
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onToggle();
}
}}
className="h-6 w-11 cursor-pointer rounded-full bg-neutral-300"
>
<span
className={`block h-5 w-5 rounded-full bg-white transition ${isOn ? "translate-x-5" : "translate-x-0.5"}`}
/>
</div>
);
}Plain HTML
<!-- Use native elements whenever possible -->
<button type="button" onclick="toggleMenu()">Menu</button>
<a href="/about">About us</a>
<!-- For custom elements, add these three things: -->
<!-- 1. role (what it is) -->
<!-- 2. tabindex="0" (makes it focusable) -->
<!-- 3. keyboard event handler (makes it activatable) -->
<div
role="button"
tabindex="0"
onclick="toggleMenu()"
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleMenu()}"
>
Custom menu button
</div>The simplest fix is almost always: replace your <div> or <span> with a <button> or <a>. Native elements get keyboard support, focus styles, and screen reader announcements for free.
Keyboard access is non-negotiable. If the cat cannot tab to it, your visitors cannot use it. This is one of the most impactful accessibility fixes you can make.
How the cat scores this
The cat identifies all interactive elements on the page — anything with a click handler, links, buttons, and custom controls. It then checks whether each one is reachable via keyboard (has a tabindex or is a native focusable element) and whether it responds to keyboard events (Enter and Space for buttons, Enter for links). Elements with onclick but no tabindex and no keyboard event handler get flagged.
Further reading
- WebAIM: Keyboard Accessibility — comprehensive guide to keyboard testing and fixes
- WCAG 2.1.1: Keyboard — the standard
- MDN: Keyboard-navigable JavaScript widgets — deep dive into making custom widgets accessible