Plate № 10 · UX primitives · accessibility

A pattern from the gf.cx specimen book

Jargon tooltip · data-tip

Source · Dan, 2026-05-31

"Nit-picking — can you add the tooltip pattern from assets.gf.cx to the metrics? FCP are human-readable. Audit/apply across all reports — anytime we use jargon-heavy, put its explainable tooltip in."

The problem

Technical reports and dashboards accumulate acronyms — FCP, LCP, CLS, INP, TTFB, GSC, CrUX. Readers who know them are fine; readers who don't can't act on the data. Adding a glossary page adds friction. Inline prose definitions bloat the table. The browser's native title="" tooltip solves it but looks awful: grey system font, 500ms hover delay, no viewport-clamping, no theme cohesion.

The shape — two pieces

A CSS primitive from assets.gf.cx and a server-side term-annotator work together: the CSS handles rendering, the annotator handles coverage.

LayerWhat it does
assets.gf.cx/tooltip/tooltip.cssPure-CSS [data-tip] selector — dark card, caret arrow, viewport-clamped max-width: min(320px, calc(100vw - 32px)). No JS.
_annotate_jargon(html) in seo_render_html.pyRegex pass over rendered HTML — wraps known GLOSSARY terms in <abbr> with data-tip. Skips <code>, <pre>, <a>, <abbr>, <svg> blocks to avoid rewriting paths or SVG text nodes.

The element, in-page

The live copy below renders inline — the tooltip CSS is already loaded by this page, so the LCP and TTFB tooltips above are already live. Hover the dotted terms in the demo to confirm the styled card — no JS, no native gray box.

The traptitle= fires twice

The instinct is to set both title="…" (accessibility) and data-tip="…" (styled tooltip). Don't. The browser fires its own native gray box from title= ~500ms after hover — you get two overlapping tooltips simultaneously.

Fix: use aria-label="…" instead of title="…". Screen readers read it; the browser does not show a native tooltip.

<!-- ❌ Double tooltip — browser shows gray box + styled card -->
<abbr title="First Contentful Paint" data-tip="First Contentful Paint — …">FCP</abbr>

<!-- ✅ Single styled tooltip — aria-label for screen readers only -->
<abbr aria-label="First Contentful Paint — …" data-tip="First Contentful Paint — …">FCP</abbr>

Import

<link rel="stylesheet" href="https://assets.gf.cx/tooltip/tooltip.css">

Server-side annotation

The annotator in seo_render_html.py does a regex pass after the HTML is rendered — every known GLOSSARY term gets wrapped automatically. Writers never need to hand-code <abbr> tags.

GLOSSARY = {
    "FCP": "First Contentful Paint — time until the first text or image is painted on screen (good ≤ 1,800 ms).",
    "LCP": "Largest Contentful Paint — time until the largest image or text block is visible (good ≤ 2,500 ms). A Core Web Vital.",
    # ... more terms
}

def _annotate_jargon(html: str) -> str:
    # stash <code>, <pre>, <a>, <abbr>, <svg> blocks first
    for term, expansion in GLOSSARY.items():
        safe = expansion.replace('"', "&quot;")
        html = re.sub(
            r"\b(" + re.escape(term) + r")\b",
            rf'<abbr aria-label="{safe}" data-tip="{safe}">\1</abbr>',
            html
        )
    # restore stashed blocks
    return html

Interactive variant — data-tip-href

When a tooltip needs a clickable "Learn more ↗" link, the CSS-only card can't deliver it (content: attr() is text-only, pointer-events: none). The interactive variant replaces the CSS card with a real DOM element for those terms.

AttributeRole
data-tip="…"Tooltip body text (same as base pattern)
data-tip-href="https://…"Triggers JS card instead of CSS card; "Learn more ↗" links here
aria-label="…"Screen-reader text (same value as data-tip)

CSS suppresses the ::after/::before pseudo-elements for these elements; the JS card takes over with pointer-events: auto. A 120ms hide delay lets the cursor travel from the trigger across the gap into the card without it closing.

<!-- CSS-only (no link) -->
<abbr aria-label="First Contentful Paint — …" data-tip="First Contentful Paint — …">FCP</abbr>

<!-- Interactive (with Learn more link) -->
<abbr aria-label="First Contentful Paint — …"
      data-tip="First Contentful Paint — …"
      data-tip-href="https://web.dev/articles/fcp">FCP</abbr>
<!-- Import both files; JS self-injects its card CSS -->
<link rel="stylesheet" href="https://assets.gf.cx/tooltip/tooltip.css">
<script src="https://assets.gf.cx/tooltip/tooltip-interactive.js" defer></script>

Limitations

Where it applies

Reusable elements

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

Reference

Source
Dan, 2026-05-31 · a nit-pick request to make Core Web Vitals acronyms human-readable in all dare.co.uk devreports.
In use / example
devreports.dare.co.uk — every report rendered by seo_render_html.py auto-annotates FCP, LCP, CLS, INP, TTFB, GSC, CrUX via _annotate_jargon(). Hover any acronym in a metric table to see the styled card live.
Reusable elements
tooltip.css (CSS-only base) · tooltip-interactive.js (JS variant for linked cards) · _annotate_jargon(html) server-side annotator (listed above).
Origin
Accessibility nit-pick, 2026-05-31 — the insight that reports read by non-experts should define their own jargon inline, and that the tooltip primitive from assets.gf.cx makes this zero-friction for writers once wired into the render pipeline.