Plate № 10 · UX primitives · accessibility
A pattern from the gf.cx specimen book
Jargon tooltip · data-tip
"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.
| Layer | What it does |
|---|---|
assets.gf.cx/tooltip/tooltip.css | Pure-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.py | Regex 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.
Time to first byte (TTFB) measures network + server latency. Largest contentful paint (LCP) measures perceived loading speed.
The trap — title= 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('"', """)
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.
| Attribute | Role |
|---|---|
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
- Above only by default. Near-top-of-viewport elements need
data-tip-pos="below". Near-right-edge elements needdata-tip-align="right". The CSS has both variants. - SVG elements can't host pseudo-elements. Overlay an invisible HTML hit-target on the SVG datum instead — see
assets.gf.cx/tooltip/README.md.
Where it applies
- Report tables with metric acronyms (FCP, LCP, CLS, GSC, CrUX) — the annotator auto-wraps every match on render
- Dashboard kicker labels, status-bar legends, sortable column headers where space is too tight for prose
- Any surface where the primary audience is the operator, not a general public — the tooltip buys comprehension without cluttering the layout
- Skip for terms that appear in headings or repeated more than ~3× on one page — define them once in prose instead
Reusable elements
The named, copyable pieces — lift any one without the others:
tooltip.css— the pure-CSS[data-tip]primitive: dark card (#1a1816), caret arrow, viewport-clamped width, no JS. One<link>import.tooltip-interactive.js— optional JS layer fordata-tip-hrefterms that need a real "Learn more ↗" link. Suppresses the CSS pseudo-elements for those elements only._annotate_jargon(html)— the server-side term annotator inseo_render_html.py: GLOSSARY dict + regex pass, skips<code>/<pre>/<a>/<abbr>/<svg>blocks, emitsaria-label(nottitle) to prevent the double-tooltip trap.aria-labelovertitle— the accessibility contract: screen readers readaria-label; the browser does not spawn a native gray box from it. Drop-in replacement fortitle="".
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.pyauto-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.cxmakes this zero-friction for writers once wired into the render pipeline.