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.

Saves the current document Download a copy in your selected format Make this visible to all subscribers
<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.

Tooltip above the trigger (default) Tooltip below the trigger Tooltip to the inline-start side Tooltip to the inline-end side
<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.

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 Component reference and patterns Status Live system status and incident history
<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.

All systems operational Partial outage in eu-west-1 Service unavailable
<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.

A very long file path that doesn't fit in the available container width A very long file path that doesn't fit in the available container width
<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&#39;t fit in the available container width
            </span>
            <span class="tooltip" id="tip-truncated" role="tooltip">A very long file path that doesn&#39;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.

A longer explanation that wraps onto multiple lines because the content exceeds the default max-width.
<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. Carries role="tooltip" and an id; the trigger references the id via aria-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-describedby must point at the tooltip's id. 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-describedby makes 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-label describing 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: none means 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

ClassDescription
.tooltip-wrapcomponent root

CSS Reference: Tooltip

Supplementary annotation that appears on hover or keyboard focus

Source: src/css/tooltip.css

Tokens

TokenDefaultDescription
--tooltip-bgvar(--color-text)Background color for the tooltip surface
--tooltip-colorvar(--color-text-inverse)Text color inside the tooltip
--tooltip-padding-xvar(--ui-pad-x-tight)Horizontal padding inside the tooltip
--tooltip-padding-yvar(--ui-pad-y-compact)Vertical padding inside the tooltip
--tooltip-radiusvar(--radius-sm)Corner radius for the tooltip
--tooltip-font-sizevar(--ui-text-xs)Font size for tooltip text
--tooltip-max-inline-size16remMaximum width before text wraps
--tooltip-offset0.375remGap between the tooltip and its trigger
--tooltip-arrow-size5pxSize of the directional arrow
--tooltip-show-delay300msDelay before the tooltip appears on hover or focus
--tooltip-fade-durationvar(--duration-fast)Duration of the fade and slide animation

Classes

ClassDescription
.tooltipcomponent root
.is-bottomPositions the tooltip below its trigger
.is-startPositions the tooltip to the inline-start side of its trigger
.is-endPositions the tooltip to the inline-end side of its trigger