skip to content

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?

level: seniorimportance: should knowfreq 40%

answer

  1. ask about input, not about width
  2. primary device versus any device
  3. none, coarse, fine
  4. the touchscreen laptop breaks naive rules
  5. fail toward the visible state

basics

~20 s

hover 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
css
.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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context