Note #03 ยท
Hover in HTML: CSS :hover and sticky states on phones
HTML leaves hover to CSS, and a tap on a phone sets it the way a mouse does, sometimes until the next tap.
There is no hover tag in HTML, and no hover attribute either. Hover is a state an element is in while a pointer rests on it, and HTML leaves states to CSS. The rule people are usually after is the :hover pseudo-class:
a:hover {
text-decoration-thickness: 2px;
}
It takes one colon. Written as ::hover, it is parsed as a pseudo-element, and no pseudo-element has that name, so the selector is invalid and the browser drops the whole rule without a message. If ::hover sits in a selector list next to valid selectors, they are dropped with it.
HTML does have a few things that act like hover. The title attribute shows a native tooltip once the pointer has rested on an element for a moment. Event handler attributes such as onmouseover and onmouseout run script when the pointer arrives and leaves. There is no onhover, because there is no hover event to handle. And Chrome 142 added interestfor to buttons and links, which opens a popover when someone holds the pointer over the element, uses a keyboard shortcut, or long-presses it on a touchscreen. Firefox and Safari have not shipped it.
What :hover matches
The HTML standard defines :hover as matching the element a user designates with a pointing device, and every ancestor of that element as well. Point at a link inside a card and the link, the card, the list around the card and body all match at once. That is what makes .card:hover .title work, and it is also why a hover rule on a big container fires far more often than its author pictured.
The standard adds one more case. When a label matches :hover, the control it labels matches too. Point at the words "Remember me" and the checkbox beside them takes its hover style. Most of the time that is what you want. Now and then it explains a field that lights up while the pointer is nowhere near it.
What a tap does to it
A finger cannot rest over anything without touching it, so it is natural to assume hover rules never fire on a phone. They do. So that pages written for the mouse keep working, a tap sends pointer events with a pointerType of "touch" and then compatibility mouse events, mouseover, mousedown and mouseup among them, before the click. The tapped element can take :hover along the way. MDN describes three outcomes depending on the browser: :hover never matches, matches for a moment, or keeps matching after the finger lifts until something else is touched. The last one is what people report as sticky hover. A button tapped on a phone keeps its hover color, and it looks stuck.
The Pointer Events spec makes the mismatch easy to see. On a device that cannot hover, the browser must fire pointerout and pointerleave right after pointerup. Pointer events report that the finger has gone while the CSS state can still say it is there.
Tap the two tiles on a phone, then point at them with a mouse. The left tile has a bare :hover rule, and the right one has the same rule inside @media (hover: hover). The row under them lists what this device reports to the hover and pointer media queries, and the log lists the events from the last touch, click or pass of the pointer, with moves left out.
hover: any-hover: pointer:
Two taps on an iPhone
Safari on iOS goes a step further. WebKit watches what the page does with the synthetic mouse events it sends for a tap. If they make clickable content appear in the viewport, for example an element going from display: none, visibility: hidden or opacity: 0 to visible, WebKit treats the tap as having opened a hover menu and does not send the click. The second tap is the click. Apple documented the behavior in its Safari guide for the first iPhones, and the code that applies it, ContentChangeObserver, is still in WebKit today.
That is where the "link needs two taps on iPhone" bug report comes from. A card that reveals a "Read more" link on :hover eats the first tap on every iPhone, while the same card works with a mouse. In a navigation menu the pause can be intended, since the first tap opens the menu and the second follows a link inside it.
Keeping hover off touchscreens
Media Queries Level 4 has a feature for this. (hover: hover) matches when the primary pointing device can hover easily, and (hover: none) matches when it cannot, or when there is no pointing device at all. Put the hover styles inside the query and phones never see them:
@media (hover: hover) {
.card:hover { translate: 0 -2px; }
}
.card:has(:focus-visible) {
translate: 0 -2px;
}
The second rule is for keyboards. Someone moving through the page with Tab never hovers anything, and :has(:focus-visible) gives them the same cue when the link inside the card takes keyboard focus, without lifting the card every time it is clicked.
The query reports on the primary pointer only, and it is up to the browser to decide which device that is. A laptop with a touchscreen usually matches (hover: hover), because the trackpad counts as primary, and a finger on that same screen still leaves hover behind. (any-hover: hover) matches when any connected device can hover, which is true of that laptop too, so it rarely makes a better test for styling. The spec also says that a touchscreen where a long press stands in for hover still reports none, and it warns authors not to assume :hover will never match on such a device.
What hover should never carry
Anything a person needs, such as a delete button or a submenu, has to be reachable without hover. Put it behind :hover and phone users get it through a sticky tap or not at all, and keyboard users do not get it. The title attribute has the same problem: MDN calls it highly problematic for people on touch-only devices, people using keyboards, and people using screen readers or magnifiers.
For a tooltip that works with every input, tie a popover to a button with popovertarget. It opens on a click, a tap, Enter or Space, and Escape closes it, with no script. When interestfor reaches the other engines, the same popover can open on hover and long press as well, and the button keeps working everywhere it has not.
Questions
Is there a hover tag in HTML?
No. HTML has no hover element or attribute. Hover is styled in CSS with the :hover pseudo-class, for example a:hover { color: red; }. The nearest things in HTML are the title tooltip, onmouseover handlers, and the interestfor attribute, which only Chromium browsers support so far.
What is the difference between :hover and ::hover?
:hover with one colon is the pseudo-class every browser supports. ::hover with two colons would be a pseudo-element, and none exists by that name, so the selector is invalid and the browser ignores the whole rule.
Why does hover stay on after I tap a button on my phone?
A tap sends compatibility mouse events, and the tapped element can take :hover with them. In some mobile browsers it keeps matching until you touch something else. Wrap hover styles in @media (hover: hover) so touch-only devices never apply them.
How do I disable hover effects on mobile?
Put the :hover rules inside @media (hover: hover). The query matches only when the primary pointer can hover, which leaves out touch-only phones and tablets. Give :focus-visible the same styles so keyboard users still see the cue.
Why do links need two taps on iPhone?
Safari on iOS skips the click when the mouse events from a tap make clickable content appear, for example a menu shown with :hover. It treats the first tap as hover and the second as the click. Keep links visible without hover, or put the hover rule inside @media (hover: hover).