Device dependent event handlers
What does device-independent mean?
If an interaction only fires on mouseover or onclick with no keyboard equivalent, it doesn’t matter how good the rest of the page is - keyboard and switch-device users simply can’t trigger it. Running an automated scan like WAVE will often surface this as “device-dependent event handler,” which just means: something on the page only responds to a mouse.
The usual offender is a hover effect wired up with inline JavaScript:
onmouseover = 'this.style.opacity=0.8;return false;';
onmouseleave = 'this.style.opacity=1;return false;';The fix isn’t really about inline JavaScript versus external files - a keyboard-inaccessible handler is just as broken in a .js file. What actually matters is whether the interaction is reachable without a mouse. For a simple hover effect like this one, the cleanest fix is to let CSS handle it, since :hover and :focus can both be targeted together:
img {
opacity: 1;
&:hover,
&:focus-visible {
opacity: 0.8;
}
}Adding :focus-visible alongside :hover is what actually makes this keyboard-accessible - :hover alone only ever fires for a mouse.
Scenarios and Edge Cases
Touch screens and hover states
On touch devices, hover states can cause unexpected behavior:
Some mobile browsers trigger
:hoveron first tap and the action on second tap, creating a confusing “double-tap to activate” pattern.Solution: Avoid relying on hover as the only way to reveal content (e.g., tooltips, dropdown menus). Ensure all hover-revealed content is also accessible via tap/click and keyboard focus.
// Accessible hover/focus pattern
.dropdown-menu {
display: none;
.dropdown:hover &,
.dropdown:focus-within & {
display: block;
}
}Dismissible menus and motor impairments
Users with motor impairments may overshoot a dropdown menu while moving the mouse. Design menus that:
Stay open when the mouse briefly exits the menu area (add a short delay with
setTimeoutbefore closing).Close with
Escapekey for keyboard users.Use
focus-withinso the menu remains open while any child element has focus.
let closeTimer;
menu.addEventListener('mouseleave', () => {
closeTimer = setTimeout(() => closeMenu(), 300); // 300ms grace period
});
menu.addEventListener('mouseenter', () => {
clearTimeout(closeTimer);
});
menu.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMenu();
});Drag-and-drop without keyboard alternatives
Drag-and-drop interfaces are inherently mouse-dependent. To make them accessible:
Provide keyboard-operated controls (e.g., “Move up” / “Move down” buttons) alongside drag handles.
Use
aria-grabbedandaria-dropeffectto communicate drag state to assistive technologies.Announce reorder changes via
aria-liveregions.
<li draggable="true" aria-grabbed="false">
<span>Item 1</span>
<button aria-label="Move Item 1 up">Up</button>
<button aria-label="Move Item 1 down">Down</button>
</li>Using Pointer Events as a modern alternative
The Pointer Events API unifies mouse, touch, and pen input into a single API, reducing the need for separate mouse* and touch* handlers:
element.addEventListener('pointerdown', handleInteraction);
element.addEventListener('pointerup', handleInteraction);Note: Pointer events do not replace the need for keyboard event handlers. Always pair pointer events with
keydown/keyuphandlers for full accessibility.
Resources
For keyboard interaction patterns beyond input-device parity, see using keyboard for content accessibility.
If you found this helpful, share it with someone who's building for the web.