I use a large cursor. It's an accessibility setting, and apparently also a crime against tooltip design. Half the tooltips I run into are partially covered by my own cursor the moment they appear. The tooltip shows up right where my pointer is, and my pointer covers part of what I was trying to read.
Nobody designed this. It just happened, because nobody thought about it.
That small annoyance is actually a good entry point into something bigger: tooltip placement is a real design decision, and most design systems assign it a default value and move on. I want to think through it more carefully here, using the evolution of toggletip implementations as a way to show how the platform is increasingly helping us get this right.
Tooltips vs Toggletips
Quick clarification before we go further. Heydon Pickering's Tooltips and Toggletips draws a distinction worth keeping in mind:
A tooltip appears on hover and focus. It supplements an element that already has a label or function. It shouldn't be the only source of a label.
A toggletip is triggered by an explicit user action, typically a dedicated button. Its whole job is to reveal supplementary information, and it sticks around until dismissed.
I'm focusing on toggletips here. They're more inclusive by nature: they work for mouse, keyboard, and touch users equally, and don't depend on hover, which touch devices don't have and which is unreliable for users with motor impairments.
Two Things I Try to Keep in Mind Before Writing Any Code
Make it as inclusive as possible. Every choice should serve the widest range of users: mouse, keyboard, touch, assistive technology, low vision, motor impairments, cognitive differences. Inclusion isn't something you layer on at the end. It's where you start.
Stay as close to the platform as possible. The web platform keeps getting better at handling things that used to need custom JavaScript. Native solutions are more robust, play better with assistive technology, age better, and are less to maintain. I lean on them hard. The one rule: never sacrifice inclusion to avoid writing a script.
These two things are also why I want to start with WCAG rather than with code.
The Baseline: WCAG 1.4.13
WCAG isn't a compliance checklist to consult at the end of a project. It's a pretty good specification of what inclusive behaviour actually looks like. For toggletips, the relevant one is Success Criterion 1.4.13 Content on Hover or Focus, which covers three requirements for content that appears on hover or focus.
Strictly speaking, the SC doesn't address content triggered by activation: a click, Space, or Enter. Toggletips fall into that gap. But the three requirements are exactly the right framework to apply anyway. If anything, a toggletip that fails one of them is a bigger problem than a hover tooltip that does, because the user made a deliberate choice to see that content. The spirit of 1.4.13 extends to activation even where the letter doesn't.
So, the three requirements:
Dismissible. The user can close the tooltip without moving focus or the pointer. In practice: Escape must work.
Hoverable. Once the tooltip is visible, the user can move the pointer over it without it closing. A tooltip that vanishes the moment the cursor drifts off the trigger fails this.
Persistent. The tooltip doesn't disappear on its own. It stays until the user explicitly closes it, moves focus away, or navigates elsewhere.
These are the bar any toggletip should clear. Let's see how three implementations hold up.
Three Implementations
1. JavaScript
A JavaScript toggletip can satisfy all three requirements. You wire up a click handler to toggle visibility, a keydown listener for Escape, and manage hover states across the trigger and the tooltip itself to cover the Hoverable requirement.
It works. But everything is on you: Escape handling, hover state tracking, focus management, ARIA attribute updates. None of it comes from the platform. Every piece is code you write and keep working.
JavaScript also gives you the runtime tools to make placement decisions:
getBoundingClientRect()exposes an element's computed size and position relative to the viewport, giving you the coordinates to figure out where the tooltip fits.scrollWidthandscrollHeightexpose content dimensions including what's hidden by overflow, useful for detecting triggers inside scroll containers.IntersectionObserverlets you watch how a target intersects the viewport or an ancestor root, so you can react when the trigger scrolls out of view.ResizeObserverlets you watch for element size changes without polling geometry manually, keeping placement accurate when layout shifts.
| Requirement | Satisfied |
|---|---|
| Dismissible | Yes, via keydown listener |
| Hoverable | Yes, via hover state management |
| Persistent | Yes, via controlled state |
2. No-Script (CSS and HTML)
A CSS toggletip using :focus-within, :hover, and a sibling or child element for the tooltip content can cover Hoverable and Persistent. The tooltip stays visible as long as the trigger or the tooltip itself is in the hover or focus path.
Dismissible is the one it can't do. Escape requires JavaScript. No way around that in a pure CSS implementation.
That's a real limitation, and not one to wave away. Dismissible matters. But two out of three with zero script and minimal code is genuinely good, and worth knowing. When the third requirement can be covered by layering in the platform-native solution coming up next, you get the best of both.
| Requirement | Satisfied |
|---|---|
| Dismissible | No |
| Hoverable | Yes |
| Persistent | Yes |
3. Popover hint + Invoker Commands + Anchor Positioning
This is where things get interesting. Three HTML and CSS features in combination now make it possible to build a toggletip that clears all three 1.4.13 requirements without a single line of author JavaScript.
The pieces:
popover="hint" designates the tooltip as a hint-type popover. The browser handles light dismissal including Escape natively, so Dismissible is satisfied without any script.
Invoker Commands (commandfor and command="toggle-popover" on the trigger button) wire the button to the popover declaratively in HTML. No click handlers.
CSS Anchor Positioning tethers the tooltip's position to the trigger in CSS. No getBoundingClientRect(), no layout calculations, no resize observers.
The full implementation:
<button
class="tooltip-trigger"
aria-describedby="tooltip-content"
commandfor="tooltip-content"
command="toggle-popover"
type="button">
<span class="sr-only">?, info tip</span>
<span aria-hidden="true">?</span>
</button>
<span popover="hint" role="tooltip" aria-live="assertive" id="tooltip-content">
Did you know you can reference hidden elements via aria-describedby and aria-labelledby?
</span>
.tooltip-trigger {
anchor-name: --tooltip-trigger;
}
[popover][role="tooltip"] {
position: absolute;
position-anchor: --tooltip-trigger;
inset: auto;
margin: 0;
position-area: top;
margin-block-end: 0.5rem;
max-inline-size: 24rem;
padding: 0.75rem 1rem;
border: 1px solid #ccc;
background: #fff;
color: #111;
box-shadow: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15);
}
A few things worth flagging:
aria-live="assertive" is technically redundant given role="tooltip" and the aria-describedby association, but it's there as a workaround for a known iOS VoiceOver bug where the tooltip content isn't reliably announced otherwise.
The ? button label is intentional. The visible character is ?, and so is the accessible name. Speech control users can say "click question mark" and have it work. That's the point.
On aria-describedby: Heydon argues it shouldn't be used on toggletip triggers, because screen reader users get the description before pressing the button, making the button seem to do nothing. Fair point in the general case. Here, the button name "?, info tip" gives enough signal that something will happen, so the pre-announced description is a minor cost rather than a real problem. The deeper principle still holds though: what makes interactive content accessible is the interplay between its name, semantics, state, and context. In production, the better experience is to dynamically set aria-describedby when the tooltip opens and remove it when it closes. That takes JavaScript, which is exactly the trade-off this no-script demo is making visible.
Live demo:
| Requirement | Satisfied |
|---|---|
| Dismissible | Yes, native Escape via popover |
| Hoverable | Yes, popover persists until dismissed |
| Persistent | Yes |
Browser support: popover="hint" sits at around 72% global coverage as of April 2026. Chrome 133+, Edge 133+, Firefox 149+ support it. Safari doesn't yet, including iOS Safari (caniuse). Invoker Commands reached Baseline in late 2025 across Chrome, Edge, Firefox, and Safari (caniuse). So this combination works well for progressive enhancement or internal tools, but isn't quite there yet for production use where full cross-browser parity matters.
So: Where Should the Tooltip Actually Go?
Anchor Positioning solves the mechanism of placement. It doesn't tell you what placement to choose. That's still a design call, and it deserves the same thought as any other.
Here's what I think about.
Available space
The obvious starting point: is there room? Anchor Positioning supports position-try-fallbacks, which lets you define a priority order of directions and lets the browser pick the first one that fits.
[popover][role="tooltip"] {
position-area: top;
position-try-fallbacks: bottom, left, right;
}
Don't assume the trigger is always somewhere safe. Triggers near viewport edges, inside scroll containers, or inside sticky headers need explicit fallback logic.
Reading direction and what placement implies
In left-to-right interfaces, placement to the right of a trigger reads as continuation. To the left reads as context or qualification. Vertical is more neutral.
Above tends to feel like a label. Below tends to feel like a consequence or description. Not a hard rule, but worth asking: what is this tooltip actually doing, and does the placement reinforce that or fight it?
What surrounds the trigger
A tooltip on a form input shouldn't compete with the field label above or the error message below. A tooltip in a dense toolbar should open in the direction with the most clearance, preferably at the top.
For toggletip buttons specifically (which unlike form inputs have no label above or error message below), above is the natural default. It keeps the tooltip clear of surrounding content and, as I'll get to in a moment, away from the cursor.
Think about the tooltip's relationship to the content around it, not just the trigger itself.
Gesture path
Where did the pointer come from to reach the trigger? A toolbar button at the top of the screen was probably approached from below. Placing the tooltip below forces the user to travel back through it. Prefer the direction away from the likely approach vector.
Coarse pointers and touch
Touch users have no hover. @media (pointer: coarse) lets you adapt placement or presentation for touch contexts. A tooltip placed above a trigger can get obscured by the user's hand on a touch device. Below is often safer for touch-primary interactions.
Large cursors and cursor obstruction
And here's where we started.
Users with low vision, motor impairments, or just a personal preference may use enlarged OS cursors, sometimes quite a bit larger than the default. A cursor set to maximum size in Windows or macOS can be 64 pixels or more. When a tooltip appears right next to its trigger, the cursor that got you there can cover part of what you're trying to read.
The placement that avoids this most reliably: above the trigger, centered horizontally on the trigger rather than anchored to the cursor position. That puts the tooltip above where the cursor is pointing, outside its footprint.
When above isn't available and the tooltip has to appear to the side or below, position-try-fallbacks should prefer horizontal placement over directly below, where cursor obstruction is worst.
This isn't an edge case. Large cursors are a common accessibility setting. It's worth one line of CSS.
Two Things to Take Away
The progression from JavaScript to no-script CSS to platform-native Popover and Anchor Positioning isn't just a story about less code. It's about where the responsibility for getting things right lives.
Users first. A no-script approach that fails Dismissible is less inclusive than a JavaScript approach that passes all three. Lean on the platform, but never at the cost of inclusion.
Stay close to the platform. Native Escape handling from popover, declarative layout from Anchor Positioning, semantic wiring from Invoker Commands: all of it is more robust, more future-proof, and less to maintain than the equivalent JavaScript. The platform caught up. Use it.
Tooltip placement is a small decision. But small decisions, made without thinking about the full range of people who'll encounter them, add up to interfaces that exclude people. It's worth thinking about where it goes.