In CSS, how do the hover, any-hover, pointer and any-pointer media features differ, and how would you use them to adapt an interface to touch versus mouse input?
answer
- ask about input, not about width
- primary device versus any device
- none, coarse, fine
- the touchscreen laptop breaks naive rules
- fail toward the visible state
basics
~20 shover and pointer describe the primary input mechanism; any-hover and any-pointer match if any available input qualifies. pointer reports none, coarse, or fine. Use them to gate hover-only affordances and to enlarge hit targets for coarse pointers, instead of inferring touch from viewport width.
solid answer
~50 s`hover` and `pointer` describe the *primary* input mechanism, while `any-hover` and `any-pointer` match if *any* attached input qualifies. `pointer` reports `none`, `coarse` (a fingertip — imprecise, large contact area) or `fine` (a mouse, trackpad or stylus); `hover` reports whether that primary input can hover without committing to a click. So a phone is `(hover: none) and (pointer: coarse)`, a desktop is `(hover: hover) and (pointer: fine)`, and a touchscreen laptop is usually primary-fine-with-hover but also matches `(any-pointer: coarse)`. In practice I gate hover-only affordances behind `@media (hover: hover)` so a touch user is never asked to hover to discover something, and I size hit targets up under `(pointer: coarse)`. The key point is that these describe input capability, not screen size — a narrow desktop window still has a mouse, and a large tablet still has fingers, so viewport width is the wrong proxy.
code
css · 11 lines.card__actions { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
.card__actions { opacity: 0; transition: opacity 150ms ease; }
.card:hover .card__actions,
.card:focus-within .card__actions { opacity: 1; }
}
@media (any-pointer: coarse) {
.card__actions button { min-block-size: 2.75rem; min-inline-size: 2.75rem; }
}go deeper
Know that (hover: hover) asks whether the device can hover and (pointer: coarse) whether the primary pointer is a fingertip, and that these are separate from how wide the screen is.
Explain the none/coarse/fine values, the difference between the primary features and the any- variants, and why a hover-revealed control must default to visible rather than hidden.
Demonstrate having shipped this on hybrid hardware: which affordances you gate on hover versus any-pointer, target sizing driven by pointer rather than breakpoint, and killing sticky hover at the source.
Own the interaction policy across the product — that no feature is reachable only by hovering, that touch targets have a single enforced minimum, and that these rules live in shared primitives instead of per-component guesses.
## Capability, not device class The long-standing bad habit in responsive CSS is inferring input from dimensions: narrow viewport, therefore touch, therefore no hover and bigger buttons. Both halves of that inference are wrong in common cases. A desktop browser window dragged to phone width still has a mouse; a 12-inch tablet has an enormous viewport and no mouse at all; a touchscreen laptop has both. The `hover` and `pointer` interaction media features exist so you can ask about the input directly. ## The four features **`pointer`** describes the accuracy of the *primary* pointing device: - `none` — no pointing device at all (a TV with only a remote, a keyboard-only kiosk). - `coarse` — present but imprecise, with a large contact area: a fingertip on a touchscreen. - `fine` — precise: a mouse, a trackpad, a stylus. **`hover`** describes whether that primary device can hover over an element without activating it: `hover` or `none`. Touch cannot — the first contact is already a tap. **`any-pointer`** and **`any-hover`** ask the same questions of *every* available input mechanism, matching if at least one qualifies. `any-pointer` can therefore match several values at once conceptually: on a touchscreen laptop both `(any-pointer: fine)` and `(any-pointer: coarse)` match. Typical devices resolve like this: | Device | `pointer` | `hover` | `any-pointer: coarse` | |---|---|---|---| | Phone / tablet | `coarse` | `none` | matches | | Desktop with mouse | `fine` | `hover` | no | | Touchscreen laptop | `fine` | `hover` | matches | | TV with a remote | `none` | `none` | no | ## Gating hover-only affordances The canonical use is anything that only reveals itself on hover — a row's action buttons, a card's overlay caption, a tooltip: ```css .row .actions { opacity: 1; } @media (hover: hover) { .row .actions { opacity: 0; transition: opacity 150ms; } .row:hover .actions, .row:focus-within .actions { opacity: 1; } } ``` Note the shape: the *accessible* state is the default, and hiding is the enhancement applied only where hovering is possible. Written the other way round — hide by default, reveal inside a `(hover: none)` block — a browser that doesn't match either branch leaves the controls invisible forever. Fail toward visible. ## Sizing for a coarse pointer The second use is hit-target size. A fingertip's contact patch is roughly 8–10mm, which is why touch guidance lands around a 44px minimum target. Rather than tying that to a breakpoint, tie it to the pointer: ```css @media (pointer: coarse) { .icon-button { min-block-size: 2.75rem; min-inline-size: 2.75rem; } } ``` This is exactly the case where viewport width misleads: the tablet that most needs large targets has the widest viewport in the room. ## Hybrid devices and the `any-` variants Hybrids are where naive queries break. A touchscreen laptop reports `pointer: fine` and `hover: hover` because the trackpad is primary — so a `(pointer: coarse)` block never fires even though the user may be tapping the screen. Two defensible policies: - **Keep hover enhancements on `hover: hover`.** Hybrid users get them, and because the affordance is an enhancement over an already-usable default, touching the same screen still works. - **Use `any-pointer: coarse` for anything about safety or reach** — destructive buttons, drag handles, close targets — so a device that *can* be touched gets forgiving targets regardless of which input is primary. What you should not do is treat these queries as a device-detection oracle. The user can plug in a mouse mid-session, or pick up a detachable tablet's keyboard; browsers do re-evaluate when the primary mechanism changes, but the transition is not something to build critical behaviour on. Design so that either input works and the query only tunes the experience. ## The sticky-hover trap A related production issue: on touch, tapping an element that has a `:hover` rule frequently leaves the hover style applied until the user taps elsewhere, because the browser synthesises a hover state around the tap. Buttons that turn a different colour on hover appear stuck in that colour after being pressed. Wrapping hover styling in `@media (hover: hover)` removes the problem at the source rather than patching it with script. ## What these features do not tell you They do not report the operating system, the browser, the screen size, or whether a keyboard is present. Keyboard accessibility is orthogonal and must be handled with `:focus-visible` regardless of what the pointer queries say — a hover-revealed control that is not also focus-revealed is inaccessible on *every* device, which is why the example above pairs `:hover` with `:focus-within`.
- A touchscreen laptop never matches your (pointer: coarse) rules. Why, and what would you use instead?Because `pointer` reports the *primary* mechanism, and on a hybrid that is the trackpad — `fine`, with hover. `any-pointer: coarse` is the query that matches, since it is satisfied if any attached input is coarse. Use `hover: hover` to gate hover enhancements (hybrids should get them) and `any-pointer: coarse` for anything where a fingertip needs a forgiving target, such as destructive actions and drag handles.
- Why does a button sometimes stay in its hover colour after being tapped on a phone?Mobile browsers synthesise a hover state around a tap, and with no pointer to move away, it lingers until the user taps elsewhere. The fix is not script that strips a class but scoping the hover styling itself: put `:hover` rules inside `@media (hover: hover)` so devices that cannot truly hover never receive them, and rely on `:active` for touch feedback.
- Should hover queries replace focus styles for keyboard users?No — they are orthogonal. A keyboard user may be on a device that reports `hover: hover`, and hiding a control until hover makes it unreachable by keyboard entirely. Any hover-revealed affordance must also reveal on `:focus-within`, and focus indication belongs to `:focus-visible` regardless of what the pointer queries report.
saying these in an interview costs you the question
- Uses viewport width as a proxy for touch input
- Thinks pointer: coarse matches any device with a touchscreen
- Hides controls by default and reveals them only under hover: none
- Treats these queries as reliable device detection
- Adds hover-reveal without a matching focus-within rule