Problem Solved

CSS · hover on touch

Home › Courses › CSS › Problems › :hover on touch
🎨 Concept · 3 min read

:hover on touch doesn't work.
The sticky-hover fix.

You tested the site on desktop — hover works. You test on mobile — hover works once, then stays stuck. The button stays highlighted after you tap it. This is the sticky hover problem, and it's caused by browsers trying to simulate hover on touch. The fix: use @media (hover: hover) to apply hover styles only on devices that actually support it. This page covers why hover fails, the media query that detects it, and the patterns that feel right on both desktop and touch.

Short answer: Wrap :hover rules in @media (hover: hover) and (pointer: fine) to apply them only on devices with a real pointer. Touch devices have no cursor — :hover either doesn't fire or fires sticky after a tap. Use :active for immediate tap feedback on all devices. Don't detect touch with user-agent sniffing — the media queries are accurate.

✅ The problem

Sticky hover on touch devices.

sticky-hover.css
.button {
background: white;
color: #2965f1;
padding: 0.5rem 1rem;
border: 1px solid #2965f1;
}

.button:hover {
background: #2965f1;
color: white;
}
Desktop:
Mouse enters → blue background, white text.
Mouse leaves → white background, blue text.
✅ Works as expected.

Touch:
User taps button → browser fires :hover
→ blue background, white text.
User lifts finger → hover state stays.
→ background is still blue.
User taps elsewhere → hover clears.

❌ The button looks "stuck" in the hover state.
❌ User thinks the page is broken.
❌ Multiple buttons look "active" at once.

This is the sticky hover problem. The fix is
to apply :hover only where it makes sense.

✅ The fix — @media (hover: hover)

Only apply hover styles when hover is real.

scoped-hover.css
.button {
background: white;
color: #2965f1;
padding: 0.5rem 1rem;
border: 1px solid #2965f1;
}

/* Hover only on devices with a real pointer */
@media (hover: hover) and (pointer: fine) {
.button:hover {
background: #2965f1;
color: white;
}
}

/* Active state works everywhere */
.button:active {
transform: scale(0.98);
}
Desktop:
✅ Hover → blue background.
✅ Click → brief scale-down (active).
✅ Mouse leaves → white again.

Touch:
✅ No hover state (media query blocks it).
✅ Tap → brief scale-down (active).
✅ After tap → white again, no stickiness.
✅ No confusion, no "stuck" state.

Tablet with stylus:
✅ Stylus hover works (pointer: fine matches).
✅ Finger tap works (active state).

hybrid devices (touchscreen laptops):
✅ Hover works with trackpad/mouse.
✅ Active works with touch.
✅ Each input gets the right feedback.

This is the correct pattern for 2026.

✅ The full pattern — hover + active + focus

Every state, every device.

all-states.css
.button {
background: white;
color: #2965f1;
padding: 0.5rem 1rem;
border: 1px solid #2965f1;
border-radius: 4px;
cursor: pointer;
transition: transform 0.1s ease;
}

/* 1. Hover — desktop only */
@media (hover: hover) and (pointer: fine) {
.button:hover {
background: #2965f1;
color: white;
}
}

/* 2. Active — all devices (touch and mouse) */
.button:active {
transform: scale(0.98);
}

/* 3. Focus-visible — keyboard only */
.button:focus-visible {
outline: 2px solid #2965f1;
outline-offset: 2px;
}
:hover (desktop only) → mouse enters the element.
:active (all devices) → press the element.
:focus-visible (keyboard) → Tab to the element.

Three states, three inputs:
Desktop mouse → hover, then active, then none.
Touch → active only.
Keyboard → focus-visible only.

✅ Every input gets feedback.
✅ No sticky hover.
✅ No missing keyboard focus.
✅ Matches the native UI of each device.

⚠️ :focus-visible is separate from :focus —
it only fires on keyboard focus, not on
mouse click. This is what makes it the right
tool for focus outlines (see focus-visible.html).

Don't use @media (hover: none) to remove hover styles. It works for mobile but breaks hybrid devices (touchscreen laptops, tablets with keyboards) where the user might have a real pointer. The pattern @media (hover: hover) and (pointer: fine) is stricter and more correct: it matches only devices that have both a hover-capable pointer AND a precise pointer. On a touchscreen laptop, @media (hover: hover) still matches when the mouse is connected — which is what you want. @media (hover: none) matches when the primary input is touch, but the user might switch to mouse — the styles are wrong. Always use the positive check, not the negative one.

⚠️ Common mistakes

The hover-on-touch mistakes that cost real time.

1. Using :hover without a media query

Sticky hover on touch. Fix: wrap in @media (hover: hover) and (pointer: fine).

2. Using @media (hover: none) to remove

Breaks hybrid devices. Fix: @media (hover: hover) to apply.

3. JavaScript UA sniffing

"Is this an iPhone?" — unreliable. Fix: CSS media queries for capability.

4. No :active feedback

Touch users get no click feedback. Fix: add :active styles.

5. :hover on non-interactive elements

Divs with hover that do nothing. Fix: only interactive elements get hover.

6. Hover-only reveal

Content shown only on hover is inaccessible on touch. Fix: tap-toggle or always visible.

7. Forgetting pointer: fine

hover: hover alone matches stylus + touch combos. Fix: add pointer: fine.

8. No :focus-visible

Keyboard users can't see focus. Fix: add :focus-visible outline.

❓ Frequently asked

Why doesn't :hover work on touch screens?

Because touch screens have no cursor. :hover fires when the mouse pointer enters an element — on touch, there's no pointer, so there's no hover. Some mobile browsers try to simulate hover by treating a tap as a hover, which produces the 'sticky hover' problem: the hover state fires on tap and stays until the next tap elsewhere. The correct answer: use the media query @media (hover: hover) to apply hover styles only on devices that actually support hover.

What is the @media (hover: hover) media query?

It matches devices that have hover capability — desktop, laptop, stylus. It does not match touch-only devices. Use it to wrap your :hover rules: @media (hover: hover) { .button:hover { ... } }. On phones, the hover styles simply don't apply, and touch users don't get the sticky-hover artifact. Combine with pointer: fine for devices with a precise pointer.

What is the sticky hover problem?

On touch devices, some browsers simulate :hover by treating the first tap as hover and the second tap as click. The result: the user taps an element, and the hover styles stay applied until they tap somewhere else. The element looks 'stuck' in its hover state. Fix: wrap :hover rules in @media (hover: hover) so touch devices never apply them. Or use :active for touch feedback.

Should I use :active instead of :hover for touch?

For touch feedback, yes. :active applies while an element is being pressed — it fires on touchstart and touchend. On touch, :active gives immediate feedback that the user is interacting. It's not a replacement for :hover on desktop, but it's the right state for touch. Use both: @media (hover: hover) { .button:hover { ... } } for desktop hover, and .button:active { ... } for press feedback on all devices.

How do I detect touch devices?

Don't use JavaScript user-agent sniffing — it's unreliable. Use CSS media queries: @media (hover: none) matches devices without hover, @media (pointer: coarse) matches touch, @media (pointer: fine) matches mouse and stylus. The most reliable pattern: @media (hover: hover) and (pointer: fine) for desktop-only styles.

Should I just remove :hover entirely for touch?

No — :hover is still useful on touch-capable devices with a mouse or stylus, and on desktop. The right approach: wrap :hover rules in @media (hover: hover) so they apply only where hover is a real interaction. Touch users get :active and tap feedback. Desktop users get hover. Both get the same tap/click feedback.

🔗 Related

← All CSS problems
Problem solved · CSS