Delayed-Then-Instant Tooltips with HTML & CSS Alone
Delayed-Then-Instant Tooltips with HTML & CSS Alone êŽë š
Hey! This post is a response to Abhishek Jakharâs post about delayed tooltips, or, perhaps better put, a second part showing an alternative approach to the same problem.
I really like Abhishekâs focus on the UX of the tooltips in his app. Both are true: a tooltip that comes up too fast is annoying for just-passing-by mouse cursors, and if you add a delay, they come up too slowly for someone intentionally mousing between them.
Thatâs just good UX thinking, and I like to see it.
But also, the solutions are quite deep into JavaScript land. The core of the solution is a setTimeout/clearTimeout which sets a âwarmedâ state that determines the tooltipâs open timing. Itâs all done in React, so the React components need to share and pass around state. There is useRef, useState, useEffect, useMemo, and even createContext/useContext all being used to do this.
I would push back against that if that was the technology being used just for this, but thatâs almost surely not the case. Abhishek said itâs a part of his larger app, which surely has much more going on, and just so happens to already be in React with Radix and Motion. So heâs implementing a solution within the context of what is already going on, which is understandable.
What Iâd like to say is: you donât need all that just for the UX weâre shooting for here. We can do all this in HTML and CSS with no JavaScript at all. And that solution would work in React-and-friends too, so ultimately might be a better solution overall.
A Master developer has many tools đ.
A Semantic HTML Foundation
Hereâs a decent tooltip setup:
<button
interestfor="tip-1"
aria-labelledby="tip-1"
>
<svg ...></svg>
</button>
<span id="tip-1" popover="hint">
I'm the tooltip.
</span>
Believe it or not, this HTML alone produces a working tooltip. Hover over the button; youâll see the HTML tooltip. Hover off, and itâs gone.
Thatâs the magic of popovers, and the additional magic of popover="hint" and interestfor which deals with âinterestâ, meaning hovers essentially (or other fancy stuff like youâre clearly gazing at a bit of UI in the metaverse or some crap, which is why web standards is cool: they think of stuff like that).
Anchoring the Tooltip (and Dealing with Edge Detection)
Another lovely little thing is that when a popover is opened, the âdefaultâ anchor is the element that opened it. So we can just use anchor positioning and assume the anchor is the button.
So with just this:
[popover] {
position: fixed;
background: red;
color: white;
position-area: block-start;
}
Weâll get a popover that opens above the button on hover:
Plus, some âtryâ logic that handles edge detection too!
position-try:
flip-block,
flip-inline,
flip-block flip-inline;
Thatâs a big advantage to CSS/popover style tooltips.
Note
Fair warning that there is no Safari support for popover="hint" yet, but weâll get fallbacks all sorted out by the end.
Controlling Timing in CSS
The magic just keeps rolling here, Iâm telling you. We can control how long the interest-powered hover delays with CSS as well:
button {
interest-delay-start: 200ms;
interest-delay-end: 150ms;
}
.area:has(:popover-open) button {
interest-delay-start: 0s;
}
Thatâs really the heart of the whole thing Abhishek was going for! In a few lines!
Fallbacks
We donât have full browser support here, which is another point in favor of Abhishekâs all-JavaScript method, which shouldnât have any trouble anywhere. But we can test for support in CSS and design our own fallbacks, which actually can replicate all this behavior anyway (just with more code).
The trick to the timing is to have a --warm variable that is essentially either on or off. The popovers naturally transition their display value, so if we just add a delay, we got what we need.
@supports not (interest-delay-start: 0s) {
.area {
--warm: 0;
transition: --warm 0s 700ms;
&:hover {
--warm: 1;
transition: --warm 0s 250ms;
}
}
[popover] {
transition-delay: calc((1 - var(--warm)) * 200ms);
}
}
Hereâs a version of that (sans anchor stuff, just to focus on this one thing):
Demo
Wrap that all up together with some anchor positioning fallbacks and such and we have a nice working demo.