Tooltip
Hover annotation
Intent
Tooltip is a small floating annotation that appears when the
user hovers or focuses an element -- a label for an icon-only
button, an explanation of a truncated value, a hint for a
keyboard shortcut. It supplements the trigger; the trigger
must still convey its primary purpose (an icon button needs
its own aria-label; a tooltip alone isn't an
accessible name).
brio's tooltip is pure CSS -- no custom element, no JavaScript.
Show / hide is handled by :hover and
:has(:focus-visible) on the wrapping element; the
tooltip itself is a real DOM element with
role="tooltip" and an id so the
trigger can wire aria-describedby for assistive
tech.
Reach for popover when the floating content is interactive (a form, a menu, anything the user can click into). Tooltips don't take focus, can't be interactive, and dismiss on mouseleave -- they're for read-only supplementary text.
Basic usage
Wrap the trigger and tooltip in a
<span class="tooltip-wrap">. The tooltip
element gets class="tooltip", an id,
and role="tooltip". The trigger gets
aria-describedby pointing at the tooltip's id.
<docs-resize>
<div class="cluster" style="padding-block: var(--space-xl);">
<span class="tooltip-wrap">
<button class="button is-outlined" type="button" aria-describedby="tip-save">Save</button>
<span class="tooltip" id="tip-save" role="tooltip">Saves the current document</span>
</span>
<span class="tooltip-wrap">
<button class="button is-outlined" type="button" aria-describedby="tip-export">Export</button>
<span class="tooltip" id="tip-export" role="tooltip">Download a copy in your selected format</span>
</span>
<span class="tooltip-wrap">
<button class="button is-outlined" type="button" aria-describedby="tip-publish">Publish</button>
<span class="tooltip" id="tip-publish" role="tooltip">Make this visible to all subscribers</span>
</span>
</div>
</docs-resize>Variants
Position
Position the tooltip on any of the four sides of the trigger via a class on the tooltip element. Default is top -- no class needed.
<docs-resize>
<div class="cluster" style="padding: var(--space-2xl) var(--space-xl);">
<span class="tooltip-wrap">
<button class="button is-outlined" type="button" aria-describedby="tip-pos-top">Top</button>
<span class="tooltip" id="tip-pos-top" role="tooltip">Tooltip above the trigger (default)</span>
</span>
<span class="tooltip-wrap">
<button class="button is-outlined" type="button" aria-describedby="tip-pos-bottom">Bottom</button>
<span class="tooltip is-bottom" id="tip-pos-bottom" role="tooltip">Tooltip below the trigger</span>
</span>
<span class="tooltip-wrap">
<button class="button is-outlined" type="button" aria-describedby="tip-pos-start">Start</button>
<span class="tooltip is-start" id="tip-pos-start" role="tooltip">Tooltip to the inline-start side</span>
</span>
<span class="tooltip-wrap">
<button class="button is-outlined" type="button" aria-describedby="tip-pos-end">End</button>
<span class="tooltip is-end" id="tip-pos-end" role="tooltip">Tooltip to the inline-end side</span>
</span>
</div>
</docs-resize>Triggers other than buttons
Tooltips work on any trigger -- a button is just the most
common one. The tooltip wrap and tooltip element don't care
what's inside; the show / hide CSS keys off
:hover and :has(:focus-visible),
both of which apply regardless of the trigger element type.
The accessibility wiring matters more than the element. Two requirements every trigger needs:
-
aria-describedby="tip-id"on the trigger, pointing at the tooltip's id. This is what makes the tooltip available to screen readers. -
Keyboard reachability. Buttons and
links are naturally focusable; spans, images, SVGs,
and other non-interactive elements are not -- give
them
tabindex="0"so users can Tab to them and trigger the tooltip via keyboard.
Link trigger
Links are naturally focusable, so no
tabindex needed. Use a tooltip to clarify
where a link goes when the link text is terse ("Docs",
"API", "Status").
<docs-resize>
<div class="cluster" style="padding-block: var(--space-xl);">
<span class="tooltip-wrap">
<a href="#" aria-describedby="tip-link-docs">Docs</a>
<span class="tooltip" id="tip-link-docs" role="tooltip">Component reference and patterns</span>
</span>
<span class="tooltip-wrap">
<a href="#" aria-describedby="tip-link-status">Status</a>
<span class="tooltip" id="tip-link-status" role="tooltip">Live system status and incident history</span>
</span>
</div>
</docs-resize>Icon trigger (non-button)
A standalone informational icon -- a small "info" or
"help" SVG. Wrap in a focusable span with
tabindex="0" and role="img" +
aria-label for the icon's identity. The
tooltip provides the supplementary detail via
aria-describedby.
<style>
.demo-info-icon {
display: inline-flex;
vertical-align: middle;
color: var(--color-text-muted);
}
</style>
<docs-resize>
<div class="cluster" style="padding-block: var(--space-xl);">
<label>
Username
<span class="tooltip-wrap">
<span tabindex="0" role="img" aria-label="Help" aria-describedby="tip-icon-rules" class="demo-info-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 16v-4"></path>
<path d="M12 8h.01"></path>
</svg>
</span>
<span class="tooltip" id="tip-icon-rules" role="tooltip">Letters, numbers, and underscores. 3-20 characters.</span>
</span>
</label>
</div>
</docs-resize>Status indicator
A colored dot or pill carrying state. The tooltip names the state in words for users who can't distinguish color or who need the literal value.
<style>
.demo-status-dot {
display: inline-block;
inline-size: 0.625rem;
block-size: 0.625rem;
border-radius: 50%;
vertical-align: middle;
}
.demo-status-dot.is-success { background-color: var(--color-success); }
.demo-status-dot.is-warning { background-color: var(--color-warning); }
.demo-status-dot.is-danger { background-color: var(--color-danger); }
</style>
<docs-resize>
<div class="cluster" style="padding-block: var(--space-xl);">
<span class="tooltip-wrap">
<span tabindex="0" role="img" aria-label="Online" aria-describedby="tip-status-online" class="demo-status-dot is-success"></span>
<span class="tooltip" id="tip-status-online" role="tooltip">All systems operational</span>
</span>
<span class="tooltip-wrap">
<span tabindex="0" role="img" aria-label="Degraded" aria-describedby="tip-status-degraded" class="demo-status-dot is-warning"></span>
<span class="tooltip" id="tip-status-degraded" role="tooltip">Partial outage in eu-west-1</span>
</span>
<span class="tooltip-wrap">
<span tabindex="0" role="img" aria-label="Offline" aria-describedby="tip-status-offline" class="demo-status-dot is-danger"></span>
<span class="tooltip" id="tip-status-offline" role="tooltip">Service unavailable</span>
</span>
</div>
</docs-resize>Truncated text
Long text truncated with text-overflow: ellipsis;
the tooltip reveals the full string. Wrap the truncated
text in a focusable span so keyboard users can reach it
too.
<style>
.demo-truncated-row {
padding-block: var(--space-xl);
max-inline-size: 14rem;
}
.demo-truncated-row .tooltip-wrap {
display: block;
}
.demo-truncated {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
<docs-resize>
<div class="demo-truncated-row">
<span class="tooltip-wrap">
<span tabindex="0" aria-describedby="tip-truncated" class="demo-truncated">
A very long file path that doesn't fit in the available container width
</span>
<span class="tooltip" id="tip-truncated" role="tooltip">A very long file path that doesn't fit in the available container width</span>
</span>
</div>
</docs-resize>State and interaction
- Mouse hover: tooltip appears after
--tooltip-show-delay(300ms by default). Hides instantly when the cursor leaves the wrap. The delay debounces brief incidental hovers (cursor passing over while moving toward something else) so tooltips don't flicker. - Keyboard focus: tooltip appears when the trigger receives focus-visible (Tab navigation), with the same show delay. Hides when focus leaves.
- Touch: the platform's tap-and-hold gesture or focus event handles touch invocation. brio's tooltip doesn't fight the platform here.
- No interactivity inside the tooltip.
Tooltips have
pointer-events: none-- the cursor passes through them. By design: tooltips shouldn't contain links or buttons. If your annotation needs interactive content, use popover.
Reduced motion
Under @media (prefers-reduced-motion: reduce),
the fade and slide animation is dropped (transition duration
becomes 0). The show delay is preserved -- it's not motion,
it's a debounce that filters out incidental hovers, and users
who want reduced motion still benefit from that filter.
Multi-line tooltips
Tooltip text wraps at --tooltip-max-inline-size
(default 16rem). Override the token for wider or narrower
tooltips per instance. Use multi-line tooltips sparingly --
if the annotation needs more than two short lines, consider
a popover or inline help text instead.
<docs-resize>
<div class="cluster" style="padding-block: var(--space-xl);">
<span class="tooltip-wrap">
<button class="button is-outlined" type="button" aria-describedby="tip-long">What is this?</button>
<span class="tooltip" id="tip-long" role="tooltip">A longer explanation that wraps onto multiple lines because the content exceeds the default max-width.</span>
</span>
</div>
</docs-resize>Class reference
.tooltip-wrap-- positioning context wrapping the trigger and tooltip pair. Required.display: inline-block;position: relative..tooltip-- the tooltip element. Carriesrole="tooltip"and anid; the trigger references the id viaaria-describedby..is-bottom,.is-start,.is-end-- position variants on the tooltip. Default (no class) is top.
Customization
Tooltip exposes scoped tokens for theming
(--tooltip-bg, --tooltip-color),
sizing (--tooltip-padding-x,
--tooltip-padding-y,
--tooltip-radius,
--tooltip-font-size,
--tooltip-max-inline-size), positioning
(--tooltip-offset,
--tooltip-arrow-size), and timing
(--tooltip-show-delay,
--tooltip-fade-duration).
Default theming is high-contrast: --color-text
background with --color-text-inverse text. The
convention -- a dark tooltip on light surfaces, light tooltip
on dark surfaces -- adapts to dark mode automatically because
those tokens flip together.
Full token list and defaults:
src/css/tooltip.css.
Accessibility
-
aria-describedby is the load-bearing wire.
The trigger's
aria-describedbymust point at the tooltip'sid. Without it, screen readers announce only the trigger's own accessible name, not the tooltip text. Brio's CSS makes the tooltip visible to sighted users;aria-describedbymakes it available to assistive tech. -
The trigger needs its own accessible name.
A tooltip is supplementary -- it can't replace the
trigger's name. An icon-only button must have an
aria-labeldescribing its action; the tooltip then adds context. Hovering an unnamed icon button announces "button" + the tooltip text, which is confusing. -
No interactive content. Tooltips
dismiss when the user moves their pointer or focus
away, so any link or button inside is unreachable. Plus
pointer-events: nonemeans the cursor passes through. If you need interactive floating content, that's a popover, not a tooltip. - Don't put critical information in tooltips. Tooltips are dismissible by accident, hidden on touch devices unless the user knows to tap-and-hold, and not always discoverable. Anything the user must read goes in the page itself (or a popover for "click to learn more"). Tooltips are for hints, not facts.
-
Clipping in overflow:hidden ancestors.
Brio's tooltip stays in normal DOM flow (not the top
layer). If the tooltip's content exceeds an
overflow:hidden parent, it clips. Workarounds: give the
wrap (or its closest clipping ancestor)
overflow: visible, or restructure the markup so the tooltip lifts to a non-clipping context.
CSS Reference: Tooltip Wrap
Inline-block positioning context for trigger and tooltip pair
Source: src/css/tooltip.css
Classes
| Class | Description |
|---|---|
.tooltip-wrap | component root |
CSS Reference: Tooltip
Supplementary annotation that appears on hover or keyboard focus
Source: src/css/tooltip.css
Tokens
| Token | Default | Description |
|---|---|---|
--tooltip-bg | var(--color-text) | Background color for the tooltip surface |
--tooltip-color | var(--color-text-inverse) | Text color inside the tooltip |
--tooltip-padding-x | var(--ui-pad-x-tight) | Horizontal padding inside the tooltip |
--tooltip-padding-y | var(--ui-pad-y-compact) | Vertical padding inside the tooltip |
--tooltip-radius | var(--radius-sm) | Corner radius for the tooltip |
--tooltip-font-size | var(--ui-text-xs) | Font size for tooltip text |
--tooltip-max-inline-size | 16rem | Maximum width before text wraps |
--tooltip-offset | 0.375rem | Gap between the tooltip and its trigger |
--tooltip-arrow-size | 5px | Size of the directional arrow |
--tooltip-show-delay | 300ms | Delay before the tooltip appears on hover or focus |
--tooltip-fade-duration | var(--duration-fast) | Duration of the fade and slide animation |
Classes
| Class | Description |
|---|---|
.tooltip | component root |
.is-bottom | Positions the tooltip below its trigger |
.is-start | Positions the tooltip to the inline-start side of its trigger |
.is-end | Positions the tooltip to the inline-end side of its trigger |