{"id":10678,"date":"2026-08-13T16:31:22","date_gmt":"2026-08-13T21:31:22","guid":{"rendered":"https:\/\/master.dev\/blog\/?p=10678"},"modified":"2026-08-13T16:31:23","modified_gmt":"2026-08-13T21:31:23","slug":"delayed-then-instant-tooltips-with-html-css-alone","status":"publish","type":"post","link":"https:\/\/master.dev\/blog\/delayed-then-instant-tooltips-with-html-css-alone\/","title":{"rendered":"Delayed-Then-Instant Tooltips with HTML &amp; CSS Alone"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Hey! This post is a response to <a href=\"https:\/\/master.dev\/blog\/tooltips-need-a-delay-and-then-they-need-to-skip-it\/\">Abhishek Jakhar&#8217;s post about delayed tooltips<\/a>, or, perhaps better put, a second part showing an alternative approach to the same problem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I really like Abhishek&#8217;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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s just good UX thinking, and I like to see it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But also, the solutions are quite deep into JavaScript land. The core of the solution is a <code>setTimeout<\/code>\/<code>clearTimeout<\/code> which sets a &#8220;warmed&#8221; state that determines the tooltip&#8217;s open timing. It&#8217;s all done in React, so the React components need to share and pass around state. There is <code>useRef<\/code>, <code>useState<\/code>, <code>useEffect<\/code>, <code>useMemo<\/code>, and even <code>createContext<\/code>\/<code>useContext<\/code> all being used to do this.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I would push back against that if that was the technology being used <em>just<\/em> for this, but that&#8217;s almost surely not the case. Abhishek said it&#8217;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&#8217;s implementing a solution within the context of what is already going on, which is understandable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What I&#8217;d like to say is:<\/strong> you don&#8217;t need all that just for the UX we&#8217;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. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A Master developer has many tools \ud83d\ude09.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A Semantic HTML Foundation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s a decent tooltip setup:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span>\n  <span class=\"hljs-attr\">interestfor<\/span>=<span class=\"hljs-string\">\"tip-1\"<\/span>\n  <span class=\"hljs-attr\">aria-labelledby<\/span>=<span class=\"hljs-string\">\"tip-1\"<\/span>\n&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">svg<\/span> <span class=\"hljs-attr\">...<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">svg<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"tip-1\"<\/span> <span class=\"hljs-attr\">popover<\/span>=<span class=\"hljs-string\">\"hint\"<\/span>&gt;<\/span>\n  I'm the tooltip.\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Believe it or not, this HTML alone produces a working tooltip. Hover over the button; you&#8217;ll see the HTML tooltip. Hover off, and it&#8217;s gone. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s the magic of popovers, and the additional magic of <code>popover=\"hint\"<\/code> and <code>interestfor<\/code> which deals with &#8220;interest&#8221;, meaning hovers essentially (or other fancy stuff like you&#8217;re clearly <em>gazing<\/em> at a bit of UI in the metaverse or some crap, which is why web standards is cool: they think of stuff like that). <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Anchoring the Tooltip (and Dealing with Edge Detection)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Another lovely little thing is that when a popover is opened, the &#8220;default&#8221; anchor is the element that opened it. So we can just use anchor positioning and assume the anchor is the button.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So with just this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-attr\">&#91;popover]<\/span> {\n  <span class=\"hljs-attribute\">position<\/span>: fixed;\n  <span class=\"hljs-attribute\">background<\/span>: red;\n  <span class=\"hljs-attribute\">color<\/span>: white;\n  <span class=\"hljs-attribute\">position-area<\/span>: block-start;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">We&#8217;ll get a popover that opens above the button on hover:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019ffc55-c279-71d3-95e4-9ca3b673f1f9\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019ffc55-c279-71d3-95e4-9ca3b673f1f9?height=450&amp;theme-id=1&amp;slug-hash=019ffc55-c279-71d3-95e4-9ca3b673f1f9&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019ffc55-c279-71d3-95e4-9ca3b673f1f9\" title=\"CodePen Embed 019ffc55-c279-71d3-95e4-9ca3b673f1f9\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Plus, some &#8220;try&#8221; logic that handles edge detection too! <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">position-try<\/span>:\n    <span class=\"hljs-selector-tag\">flip-block<\/span>,\n    <span class=\"hljs-selector-tag\">flip-inline<\/span>,\n    <span class=\"hljs-selector-tag\">flip-block<\/span> <span class=\"hljs-selector-tag\">flip-inline<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s a big advantage to CSS\/popover style tooltips. <\/p>\n\n\n\n<p class=\"learn-more wp-block-paragraph\">Fair warning that there is no Safari <a href=\"https:\/\/caniuse.com\/wf-popover-hint\">support for <code>popover=\"hint\"<\/code><\/a> yet, but we&#8217;ll get fallbacks all sorted out by the end.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Controlling Timing in CSS<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The magic just keeps rolling here, I&#8217;m telling you. We can <em>control how long the interest-powered hover delays<\/em> with CSS as well:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">button<\/span> {\n  <span class=\"hljs-attribute\">interest-delay-start<\/span>: <span class=\"hljs-number\">200ms<\/span>;\n  <span class=\"hljs-attribute\">interest-delay-end<\/span>: <span class=\"hljs-number\">150ms<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.area<\/span><span class=\"hljs-selector-pseudo\">:has(<\/span><span class=\"hljs-selector-pseudo\">:popover-open)<\/span> <span class=\"hljs-selector-tag\">button<\/span> {\n  <span class=\"hljs-attribute\">interest-delay-start<\/span>: <span class=\"hljs-number\">0s<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s really the heart of the whole thing Abhishek was going for! In a few lines!<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019ffc5e-e476-70e3-b310-3f44fc8b8c3f\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019ffc5e-e476-70e3-b310-3f44fc8b8c3f?height=450&amp;theme-id=1&amp;slug-hash=019ffc5e-e476-70e3-b310-3f44fc8b8c3f&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019ffc5e-e476-70e3-b310-3f44fc8b8c3f\" title=\"CodePen Embed 019ffc5e-e476-70e3-b310-3f44fc8b8c3f\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Fallbacks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We don&#8217;t have full browser support here, which is another point in favor of Abhishek&#8217;s all-JavaScript method, which shouldn&#8217;t have any trouble anywhere. But we can test for support in CSS and design our own fallbacks, which <em>actually can replicate all this behavior anyway<\/em> (just with more code).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The trick to the timing is to have a <code>--warm<\/code> variable that is essentially either on or off. The popovers naturally <code>transition<\/code> their display value, so if we just add a delay, we got what we need.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">@supports not (interest-delay-start: <span class=\"hljs-number\">0<\/span>s) {\n  .area {\n    --warm: <span class=\"hljs-number\">0<\/span>;\n    transition: --warm <span class=\"hljs-number\">0<\/span>s <span class=\"hljs-number\">700<\/span>ms;\n    &amp;:hover {\n      --warm: <span class=\"hljs-number\">1<\/span>;\n      transition: --warm <span class=\"hljs-number\">0<\/span>s <span class=\"hljs-number\">250<\/span>ms;\n    }\n  }\n\n  &#91;popover] {\n    transition-delay: calc((<span class=\"hljs-number\">1<\/span> - <span class=\"hljs-keyword\">var<\/span>(--warm)) * <span class=\"hljs-number\">200<\/span>ms);\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s a version of that (sans anchor stuff, just to focus on this one thing):<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019ffcf9-ca7d-7d54-a60b-9d9bdb89d9d4\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019ffcf9-ca7d-7d54-a60b-9d9bdb89d9d4?height=450&amp;theme-id=1&amp;slug-hash=019ffcf9-ca7d-7d54-a60b-9d9bdb89d9d4&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019ffcf9-ca7d-7d54-a60b-9d9bdb89d9d4\" title=\"CodePen Embed 019ffcf9-ca7d-7d54-a60b-9d9bdb89d9d4\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Demo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Wrap that all up together with some anchor positioning fallbacks and such and we have a nice working demo.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_019ff341-f6c6-791f-8edd-e6043b80a6cf\" src=\"\/\/codepen.io\/editor\/anon\/embed\/019ff341-f6c6-791f-8edd-e6043b80a6cf?height=750&amp;theme-id=1&amp;slug-hash=019ff341-f6c6-791f-8edd-e6043b80a6cf&amp;default-tab=css,result\" height=\"750\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed 019ff341-f6c6-791f-8edd-e6043b80a6cf\" title=\"CodePen Embed 019ff341-f6c6-791f-8edd-e6043b80a6cf\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n","protected":false},"excerpt":{"rendered":"<p>We don&#8217;t NEED JavaScript to adjust tooltip timing displays. Super modern CSS and HTML has this covered simply, and we can do fully successful fallbacks.<\/p>\n","protected":false},"author":1,"featured_media":10684,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[7,31,527,120,417],"class_list":["post-10678","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-css","tag-html","tag-interest-invokers","tag-popover","tag-tooltips"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/08\/hovers.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10678","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/comments?post=10678"}],"version-history":[{"count":6,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10678\/revisions"}],"predecessor-version":[{"id":10685,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10678\/revisions\/10685"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media\/10684"}],"wp:attachment":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media?parent=10678"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/categories?post=10678"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/tags?post=10678"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}