UI/UX law
// Interaction & Speed;
Hover-Reveal Actions
An action revealed only by :hover has to be reachable at least two other ways: on keyboard focus, so tabbing to it makes it visible, and independent of pointer type, so a touchscreen, which never fires :hover and doesn't set focus from a plain tap, gets the same access as a mouse.
Mechanism
Why it happens
:hover is a state a mouse or trackpad can sustain by resting the pointer over an element. A touchscreen has no pointer that lingers, so it never enters :hover at all, and a keyboard user tabbing through the page never touches it either. Hover is one input method's signal, not a shared one.
Adding :focus-within reveals the actions when something inside the row is focused, and that's a real fix for keyboard users. But a plain tap on a non-interactive row doesn't move DOM focus, only a focusable element receiving focus does, so on a touchscreen with no attached keyboard, :focus-within fires for exactly nobody. It's frequently shipped as "the accessible fix" on its own, when it only ever covered keyboard access.
The reliable touch fix is a pointer-capability query, @media (hover: none) or (pointer: coarse), that forces the actions permanently visible on any device that can't hover, independent of hover or focus state entirely.
None of the three substitutes for another: hover serves the mouse, :focus-within serves the keyboard, and the coarse-pointer query serves touch. Dropping any one of them silently locks out exactly the input method it was covering, while the other two keep working and hide the gap.
Impact
Why it matters
- A hover-only reveal is invisible to touchscreen users and keyboard users at the same time, not a small edge case but most non-desktop-mouse traffic
- :focus-within is often shipped as the whole accessible fix; it solves keyboard tabbing but does nothing for a touchscreen user, since tapping a row doesn't focus it without an element inside built to receive that focus
- The action itself still works once it's visible, so the bug never shows up in a functional test, only in who can ever trigger it
- Reviewing the UI with a mouse hides the bug by construction: the reviewer's own input method is the one case that already works
Example
Without vs. with
β³ hover-only actions: invisible on Tab, invisible on tap
A file list reveals its edit and delete icons with only `.file-row:hover .file-row__actions { opacity: 1 }`. Tabbing to the row with a keyboard leaves the icons invisible and unreachable, and every tap on a phone leaves them invisible too, since a touchscreen never fires :hover.
β² hover, Tab, or a touchscreen β the icons are always reachable
The same rule adds :focus-within, so tabbing into the row reveals the icons for keyboard users, and @media (hover: none), so touchscreens, which can satisfy neither :hover nor a tap-triggered :focus-within, get the icons permanently visible with a full 44px tap target instead.
Checklist
How to apply it
Reveal actions on :hover and :focus-within together, so a mouse resting over the row and a keyboard user tabbing into it see the same controls
Add @media (hover: none), or (pointer: coarse), that forces the actions permanently visible, since that's the only rule a touchscreen actually satisfies
Give the always-visible touch actions a real 44Γ44px hit target (Fitts's Law) instead of shrinking icons to fit next to the row text
For dense tables where always-visible icons crowd every row, use one per-row "more" trigger instead of several hidden actions; the trigger stays visible on every input type and opens the same menu everywhere
Recipe
Code example
Where it shows up