Plate № 11 · UX primitives · interactivity

A pattern from the gf.cx specimen book

A tooltip you can reach into

Source · Dan, 2026-06-01

"Lets A/B test — for selector-area tooltips that have an embedded link that lights up red. This involved smart demarcation of the zone, to allow the user to 'click it' without it disappearing."

The problem

The CSS-only data-tip tooltip is text-only. content: attr(data-tip) renders as a plain string — no HTML, no links, no interactive elements inside the card. The pseudo-element also carries pointer-events: none, so even if you could inject a link, the cursor would fall straight through it. For glossary terms that have canonical reference pages (FCP → web.dev/articles/fcp), a "Learn more ↗" link inside the tooltip is genuinely useful — but CSS can't deliver it.

PieceWhat it does
data-tip-href="https://…"Signals that this element needs the JS card — the CSS card is suppressed for these elements via an explicit override rule in tooltip.css
tooltip-interactive.jsSelf-contained IIFE. Queries all [data-tip][data-tip-href] elements, injects a single shared DOM card (.tip-interactive) into document.body, positions it above each trigger on mouseenter
120ms hide delayThe guard that makes the pattern work. On mouseleave a timer schedules hide rather than hiding immediately — the cursor can travel from the trigger across the gap into the card. mouseenter on the card cancels the timer
Red "Learn more ↗" link.tip-interactive__linkcolor: #c8364c, bold, opens in new tab with rel="noopener"

The critical trap is the pixel gap between the trigger element and the card (the caret arrow space). If you hide the card on mouseleave of the trigger immediately, the cursor momentarily has no target and the card vanishes before the user reaches the link. The 120ms delay is the fix: small enough to feel instant, large enough for a deliberate move from trigger to card.

The element, in-page

Hover the dotted term below. When the card appears, slide your cursor across the gap to click the red "Learn more" link — the 120ms delay keeps the card open while you travel. The demo renders inline (not in an iframe) so pattern-preview's inline mode can share the tooltip-interactive.js already loaded on this page.

The CSS suppression rule (already baked into tooltip.css v0.2.0) prevents the plain CSS card from doubling up with the JS card:

abbr[data-tip][data-tip-href]:hover::after,
abbr[data-tip][data-tip-href]:hover::before { display: none !important; }

Where it applies

Update · gate the upgrade on significance

2026-08-20 — a second live adoption, on the status.gf.cx/r2/ bucket pages, stretched the contract in two ways worth folding back in.

Reads the room — adaptive placement

Source · Dan, 2026-08-13

"I like how our pattern can adapt to the space, above or below based on its position."

The card defaults to sitting above its trigger — out of the way of the text being read. But "above" is only free real estate when there's room above. A term in the page's first heading, or in the lede, has almost nothing between it and the top of the viewport: place a 90-pixel-tall card above a trigger 30 pixels down the page and its top edge lands at a negative coordinate — the card renders off-screen. Placement has to be decided per trigger, at hover time, from the actual geometry.

The whole decision is three lines. ch is the card height, r the trigger's rectangle; the ternary picks the vertical origin and the class toggle tells CSS which way the caret points:

var roomAbove = (r.top - ch - 10) >= 8;
card.classList.toggle('tip-below', !roomAbove);
var top = (roomAbove ? r.top - ch - 10 : r.bottom + 10) + window.scrollY;

The caret is pure CSS — the flip is a class, never inline style. By default the caret hangs off the card's bottom edge pointing down at the trigger; .tip-below moves it to the top edge pointing up, so a flipped card still visibly ties to the word it annotates:

.tip-interactive::after      { top: 100%; border-top-color: var(--tip-bg); }
.tip-interactive.tip-below::after { top: auto; bottom: 100%;
    border-top-color: transparent; border-bottom-color: var(--tip-bg); }

The same idea already governs the horizontal axis: the card is clamped 16px inside the viewport edges, and --tip-caret-x keeps the caret pointing at the trigger's centre after the body is nudged inward. Vertical flip and horizontal clamp are the two halves of one principle — measure, then place. See it live on any dash.gf.cx report, where a glossary term in the opening line drops its card below, caret up.

Reusable elements

The named, copyable pieces — lift any one without the others:

Reference

Source
Dan, 2026-06-01 · interactive tooltip developed during A/B testing on selector-area glossary terms in devreports.
In use / example
dash.gf.cx/reports/ — live, with [data-tip][data-tip-href] on glossary terms (FCP, LCP, CLS, etc.); hover a dotted term for the red "Learn more ↗" link, and note a term in the first heading flips its card below. Also status.gf.cx/r2/ (2026-08-20) — significance-gated storage sparklines. Also home.gf.cx/vehicles/ford-f250 — the cost-when-new figure's card.
Reusable elements
tooltip.css · tooltip-interactive.js · data-tip/data-tip-href attribute contract · 120ms hide-delay guard (listed above).
Origin
A/B test on selector-area tooltip interactivity, 2026-06-01 — the question was whether a link inside a tooltip is reachable; the 120ms delay is the answer. The adaptive above/below flip (pos()) was extracted into tooltip-interactive.js at v1.2.0 (2026-08-13) and folded into this pattern 2026-08-21 (was the separate adaptive-tooltip-placement pattern).