Pointer and Hover Media Queries: Gating Affordances by Input Capability
A card component reveals its "edit" and "delete" buttons when you hover it. On a laptop this reads as clean and uncluttered. On a phone it reads as a card with no controls at all, because there is no hover state to enter and the buttons never appear. The narrow problem this page solves is deciding, in CSS, whether the current input device can hover and how precise it is — and structuring the stylesheet so that a device which cannot hover never loses functionality. This page is part of Hover & Focus State Design, and it is the capability-detection layer underneath the effects covered in Smooth Hover Effects Without JavaScript.
What this page settles:
- What each of the four interaction media features reports
- Why
hover: hoveris the correct test and touch detection is not - The default-visible pattern that cannot strand a touch user
- Where
any-hoverandany-pointerare genuinely the right question
Four features, two axes
Media Queries Level 4 defines four interaction media features, on two axes — can it hover, and how accurate is it:
| Feature | Values | Reports on |
|---|---|---|
hover | none, hover | the primary pointing device |
pointer | none, coarse, fine | the primary pointing device |
any-hover | none, hover | any available pointing device |
any-pointer | none, coarse, fine | any available pointing device |
hover: hover means the primary pointing device can put the cursor over an element without activating it and keep it there. pointer: fine means that device can point at a small area accurately — a mouse, a trackpad, a precise stylus. pointer: coarse means it cannot, which covers fingers but also TV remotes, D-pads and game controllers. pointer: none means there is no pointing device at all, which is the case for a keyboard-only or screen-reader-driven environment on some platforms.
The any- variants are queried against the union of all available devices, and any-pointer in particular can match more than one value at once: a touchscreen laptop with a mouse matches both any-pointer: coarse and any-pointer: fine. That is the whole reason the pair exists — the plain features force a single answer about the primary device, and the any- features describe the full set.
Your own device is the most convincing example. This readout styles four rows, each highlighted by its own media query:
Why capability beats detection
The instinct is to ask "is this a touch device?" — usually via 'ontouchstart' in window, a navigator.maxTouchPoints check, or user-agent sniffing. All three answer a question that is not the one you have. The presence of a touchscreen tells you nothing about how the person is currently interacting: a convertible laptop has a touchscreen and a trackpad, a desktop monitor may be a touchscreen nobody touches, and a tablet may have a keyboard and mouse attached. A boolean about hardware cannot describe a session where the user picks up the mouse, puts it down, and taps the screen.
The media features describe capability rather than hardware category, and they are live. When the user attaches a mouse to a tablet the primary pointing device changes, the media query re-evaluates, and the styles follow — no listener, no reflow of your own code, no stale flag captured at page load. That is a different class of correctness from feature detection performed once in an entry script.
The second, subtler argument is about failure direction. A hover: hover query fails closed: an engine that does not support interaction media features, or a device it cannot classify, simply does not match the block. If you write your stylesheet so the block inside @media (hover: hover) only ever removes a permanently visible affordance, the unmatched case leaves the control visible — the safe outcome. Touch detection in script fails open: a mis-detection hides the control and the user is stuck.
Complete working implementation
A file list where each row has a delete action. On hovering devices the action fades in on hover; everywhere else it is simply always there. Note the order: the visible state is the default, and the hiding lives inside the query.
<ul class="list">
<li class="item">
<span class="item__name">Q3-forecast.numbers</span>
<button class="item__action" type="button">Delete</button>
</li>
<li class="item">
<span class="item__name">brand-guidelines.pdf</span>
<button class="item__action" type="button">Delete</button>
</li>
</ul>
.list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.7rem 0.8rem;
border: 1px solid #cbd5e1;
border-radius: 8px;
}
.item__action {
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 0.35rem 0.7rem;
background: transparent;
font: 400 0.8rem/1 system-ui, sans-serif;
cursor: pointer;
}
/* The default is VISIBLE. Nothing is hidden until we have proof the device
can hover, so an unclassified device or an old engine keeps the control. */
@media (hover: hover) {
.item__action {
opacity: 0;
transition: opacity 0.18s ease;
}
/* Two reveal conditions: pointer hover, and focus anywhere inside the row.
Without the second, a keyboard user would tab to an invisible button. */
.item:hover .item__action,
.item:focus-within .item__action {
opacity: 1;
}
}
/* Precision, not hover: a coarse pointer needs a bigger target even on a
device that can hover, such as a stylus-driven tablet. */
@media (pointer: coarse) {
.item__action {
min-height: 44px;
padding-inline: 1rem;
}
}
/* Honour the motion preference for the fade itself. */
@media (prefers-reduced-motion: reduce) {
.item__action { transition-duration: 0.01ms; }
}
The technique that makes it work
The load-bearing decision is not which media feature to use — it is which side of the query the hiding goes on. Written the other way round, as a permanently hidden control revealed inside @media (hover: none), every failure mode strands the user: an engine that does not implement the feature, a device the platform reports as unknown, a stylesheet loaded with the media attribute stripped. Written as visible by default, hidden only under (hover: hover), every one of those failures produces a cluttered but fully functional interface.
This is progressive enhancement applied to input capability rather than to feature support, and it composes with @supports reasoning in the same way: assume the least capable environment, then subtract. It also has a practical consequence for review — you can audit a stylesheet for this pattern by grepping for hover: none, since a well-structured sheet rarely needs it.
The opacity-based hide matters too. display: none inside the hover block would remove the button from the accessibility tree and from the tab order, so :focus-within could never fire — there would be nothing to focus. opacity: 0 keeps the element focusable, which is what lets the keyboard reveal work at all. If you need the control to be non-interactive while hidden, visibility: hidden is the correct middle ground, but then you must restore visibility in the same rules that restore opacity.
Variation: an any-hover fallback for hybrid devices
hover describes only the primary device, and platforms do not always designate the one the user is holding. On a convertible where the OS reports touch as primary, a user with a mouse plugged in matches hover: none and gets the always-visible layout — correct but slightly cluttered. If you would rather trust the presence of any hovering device:
/* Hide only when a hovering device exists somewhere on the system, even if
it is not the primary one. More generous, slightly riskier. */
@media (any-hover: hover) {
.item__action { opacity: 0; }
.item:hover .item__action,
.item:focus-within .item__action { opacity: 1; }
}
The tradeoff is real: any-hover will hide the control on a tablet whose only hovering device is a stylus the user rarely picks up. As a rule, use hover for hiding functionality and any-hover only for decorative refinements. Where the control is genuinely important, do not hide it on either — and where it is small, check it against the rules in Target Size and Pointer Accessibility before deciding a coarse pointer can reach it.
Browser support
The interaction media features have very broad support: hover, pointer, any-hover and any-pointer are all available across every current engine and have been for years, so anything evergreen handles all four. Because an unsupporting engine simply fails to match, the default-visible structure described above is itself the fallback and no @supports guard is needed. One caveat worth testing for: a small number of older Android browsers reported hover: hover incorrectly, which is another reason not to put functionality behind the query.
FAQ
Why use (hover: hover) instead of detecting touch support?
Touch detection answers whether a touchscreen exists, not whether the person is using one. Hybrid laptops have both, and users switch input mid-session. The hover media feature describes the primary pointing device's capability and re-evaluates live, so the styling follows the actual input.
What is the difference between hover and any-hover?hover reports the capability of the primary pointing device, while any-hover reports whether any available pointing device can hover. A touchscreen laptop with a mouse attached typically matches both, but a tablet with an occasional stylus may match any-hover without matching hover.
Does pointer: coarse always mean a touchscreen?
No. It means the primary pointing device has limited accuracy, which covers fingers, some styluses, TV remotes and game controllers. It is a statement about precision, not about the hardware category, which is exactly why it is the right thing to test.
Should hover-revealed controls be hidden on desktop?
Only if they are also revealed on focus. Hiding a control behind hover makes it unreachable by keyboard, so pair every hover reveal with :focus-within on the same container, and keep the control permanently visible wherever hover is unavailable.
Related
- Hover & Focus State Design — the parent guide on interactive state styling.
Form Patterns — the keyboard half of every hover reveal. - Smooth Hover Effects Without JavaScript — the effects these queries gate.
- Target Size and Pointer Accessibility — sizing targets once you know the pointer is coarse.
- Responsive Navigation Without Media Queries — where capability queries and size queries divide the work.
Related articles
More pages in the same section.