Tooltip
A CSS-driven tooltip that shows additional context when a control is focused or hovered. The bubble
is always in the DOM (so the aria-describedby reference is never dangling) and shown/hidden in CSS —
no state machine, no positioning engine. Show is :focus-visible (keyboard focus) and :hover wrapped
in @media (hover: hover) (pointer, without sticking open on touch); a click or a tap that focuses the
trigger does not show it. A press of the trigger hides it, the pointer can move onto the bubble, and
Escape hides it without moving focus or the pointer (WCAG 1.4.13).
The examples are organized by layer: the class reference is the standalone
@oriui/css layer, and the Framework API is the @oriui/vue component. Every
example is live — flip its code between HTML (the standalone classes, also your htmx / Astro / Svelte /
plain-HTML usage), Vue; HTML is the default.
Classes
A tooltip is a wrapper block plus a bubble element that composes the shared .ori-anchored placement
primitive — the same primitive documented on the Popover page. There
are no variant or size utilities; the bubble adapts to its content.
| Class | Type | Description |
|---|---|---|
ori-tooltip | Block | Wrapper: inline-flex. Sets --ori-anchor (one shared default anchor-name) and scopes it with anchor-scope, so each bubble pairs with its own trigger without per-instance wiring; plus local tokens (--ori-tooltip-bg, --ori-tooltip-color, --ori-tooltip-gap, --ori-tooltip-arrow). Carries ori-color_* on the wrapper only when the color prop is set. |
ori-tooltip__trigger | Part | Inline-flex wrapper around the trigger slot; carries the anchor-name that pairs it with the bubble. The Vue component also renders aria-describedby here, where it is advisory only (a non-focusable span announces nothing) — in your own markup put that attribute on the focusable control inside. |
ori-tooltip__bubble | Part | The tooltip surface, role=tooltip. Always in the DOM but visibility:hidden + opacity:0 + pointer-events:none until shown. Composed with .ori-anchored for placement. Background var(--ori-tooltip-bg), shadow var(--ori-shadow-md), radius var(--ori-tooltip-radius). Arrow via ::after, shown only where anchor positioning is supported. |
ori-anchored / ori-anchored_* | Placement base | Shared floating-panel placement primitive: position:fixed + position-anchor + collision-aware flip via position-try-fallbacks. See the Popover class reference for the full 12-value modifier list. |
ori-color_* | Color | Applied on the wrapper when color is set; repoints --ori-color / --ori-color-on for the bubble fill + contrast text. Omit for the default neutral inverse chip (dark in light mode). |
À la carte: the classes above ship in @oriui/css/components/tooltip.css. The shared .ori-anchored
placement primitive is inlined here, so anchored.css needs no separate import. Import a foundation
(@oriui/css/base.css or @oriui/css/tokens.css) first — the token utilities (ori-color_*,
ori-size-radius_*, …) live there, not in the component file. The full bundle @oriui/css is the default and
already carries both; see à-la-carte imports.
Placements
Twelve placements are available via the shared .ori-anchored primitive — a bare side (top /
bottom / left / right) centers on the cross axis, -start / -end align to the trigger's start
/ end edge (logical, RTL-aware; see the Popover class reference for the
full list) — though a tooltip typically only needs the four centered sides shown below. top is the
default. The bubble is viewport-anchored (position: fixed) and collision-aware: position-try-fallbacks
flips it to the opposite side when the viewport would clip it. An engine without CSS Anchor
Positioning support lands the bubble at its fixed origin next to the trigger — unplaced, but still
readable.
<!-- top (default) -->
<span class="ori-tooltip">
<!-- aria-describedby goes on the FOCUSABLE control, not on the wrapper: a description is
announced when the element bearing it is focused, and this <span> never is. -->
<span class="ori-tooltip__trigger">
<button aria-describedby="tip-1">top</button>
</span>
<span id="tip-1" class="ori-tooltip__bubble ori-anchored ori-anchored_top" role="tooltip"> Above the trigger </span>
</span>
<!-- swap the modifier: ori-anchored_top → _bottom / _left / _right -->
Colors
Omit color for the default neutral inverse chip (dark in light mode). Pass any semantic color to
tint the bubble with the matching role palette.
<!-- default — no color class -->
<span class="ori-tooltip">
<span class="ori-tooltip__trigger"><button aria-describedby="tip-2">default</button></span>
<span id="tip-2" class="ori-tooltip__bubble ori-anchored ori-anchored_bottom" role="tooltip">Default neutral</span>
</span>
<!-- colored — add ori-color_<role> on the wrapper -->
<span class="ori-tooltip ori-color_danger">
<span class="ori-tooltip__trigger"><button aria-describedby="tip-3">danger</button></span>
<span id="tip-3" class="ori-tooltip__bubble ori-anchored ori-anchored_bottom" role="tooltip">Danger</span>
</span>
Rich content
When plain text is not enough, use the #content slot instead of the content prop. Slot content
takes precedence over the prop.
<span class="ori-tooltip">
<span class="ori-tooltip__trigger">
<button aria-describedby="tip-4">hover me</button>
</span>
<span id="tip-4" class="ori-tooltip__bubble ori-anchored ori-anchored_bottom" role="tooltip">
Keyboard shortcut: <kbd>Ctrl</kbd> + <kbd>S</kbd>
</span>
</span>
With icon-only button
A common pattern: supplement an icon-only button with a tooltip so the action is discoverable via keyboard focus.
<span class="ori-tooltip">
<span class="ori-tooltip__trigger">
<button
class="ori-button ori-button_icon ori-variant_soft ori-color_primary"
aria-label="Add item"
aria-describedby="tip-5"
>
<i class="ori-icon" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" /></svg>
</i>
</button>
</span>
<span id="tip-5" class="ori-tooltip__bubble ori-anchored ori-anchored_right" role="tooltip"> Add item </span>
</span>
Common patterns
Three icon buttons in a toolbar, each with a labeled tooltip — a typical editor or action-bar pattern.
<div style="display: flex; gap: 0.25rem" role="toolbar" aria-label="Text formatting">
<span class="ori-tooltip">
<span class="ori-tooltip__trigger">
<button
class="ori-button ori-button_icon ori-variant_quiet …"
aria-label="Bold"
aria-describedby="tip-bold"
>
<!-- bold icon -->
</button>
</span>
<span id="tip-bold" class="ori-tooltip__bubble ori-anchored ori-anchored_bottom" role="tooltip">
Bold (Ctrl+B)
</span>
</span>
<!-- repeat for Italic, Underline -->
</div>
Limitations
Show and hide are CSS-only, so these are out of scope:
- The arrow does not track a collision flip — the bubble itself repositions via
position-try-fallbacks, but the decorative arrow is keyed to the requestedplacementclass (a pure-CSS pseudo-element cannot observe which fallback fired), so it keeps pointing at the requested side — correct in the common, uncollided case. - Show/hide delay — no hover-intent debounce; the bubble appears immediately on hover/focus.
Accessibility
The accessibility contract holds across every layer, with one deliberate divergence noted below: in
hand-written markup you put aria-describedby on the focusable control yourself, which the Vue
component cannot do for arbitrary slot content.
- The bubble has
role="tooltip"and auseId()-generated id, exposed on the default slot scope asbubbleId. The bubble stays in the DOM permanently (notv-if) so the reference is never dangling — a pointed-at id that does not exist is ignored by assistive technology. - To announce the tooltip to screen readers, put
aria-describedby="<bubbleId>"on your own focusable control — the<button>or<a>inside the trigger, as every HTML example above does.aria-describedbyis announced when the element bearing it is focused, and the.ori-tooltip__triggerwrapper is a non-focusable<span>, so the copy the Vue component renders on that wrapper announces nothing on its own. The component can't augment arbitrary slot content without JS, so in Vue this wiring is the consumer's to add (see the trade-off below). Icon-only controls should still carry their ownaria-label. - The trigger's native
:focus-visiblering is not overridden — the real control inside keeps its own focus indicator. - The decorative arrow is a
::afterpseudo-element — invisible to assistive technology, noaria-hiddenneeded. - Color is applied only on the wrapper, not on the bubble directly, so the
ori-color_*utility is never shadowed.
Design / a11y trade-off: a pure-CSS tooltip can't inject
aria-describedbyonto arbitrary slot content without JS. The Vue component therefore renders it on the.ori-tooltip__triggerwrapper and exposesbubbleIdon the slot scope; that wrapper copy is advisory only — it announces nothing, becausearia-describedbysurfaces on focus of the element bearing it and the wrapper isn't focusable. For the description to actually be announced, bind:aria-describedby="bubbleId"on your own focusable control. Hand-written markup has no such limit: put the attribute straight on the<button>.:focus-visiblegoverns the bubble's CSS visibility, not the ARIA announcement; the two are independent.
| Key | Action |
|---|---|
Tab | Focuses the trigger control; shows the tooltip. |
Shift+Tab | Blurs the trigger control; hides the tooltip. |
Enter / Space | Activates the trigger and hides the tooltip, as a click does. |
Escape | Hides the tooltip while focus or the pointer stays; leaving re-arms it. |
The Vue component follows the model of Radix, React Aria and Zag: the tooltip shows on hover and on
keyboard focus, never on focus a click or a tap put there, and it hides when the trigger is pressed —
whatever the press opens, the hint would cover it. An Escape that hides no tooltip, or that comes while a
popover or modal dialog sits above it, passes through to whatever else listens for it.
In the CSS layer, two attributes on .ori-tooltip are yours to wire. data-ori-dismissed hides the bubble:
set it on Escape and on a click of the trigger, and remove it once the pointer and the focus have left.
data-ori-pointer-focus turns off the focus rule only: browsers start matching :focus-visible on a
clicked control at the next key press, so set it on focusin when the focus inside does not match
:focus-visible, and remove it on focusout. Without the second one, a key pressed after a click shows
the bubble.
Framework API
The props, events, slots, and polymorphism of the Vue component. The standalone CSS layer has no component API — its surface is the classes above.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
color | ThemeColor | — | Bubble fill color. Adds ori-color_<color> on the wrapper, repointing --ori-color / --ori-color-on. Omit for the default neutral inverse chip (dark in light mode). |
content | string | — | Tooltip text. For rich content use the #content slot instead — it takes precedence over this prop. |
placement | AnchoredPlacement | 'top' | Placement relative to the trigger — 'top' | 'bottom' | 'left' | 'right', each also -start / -end (12 values total). Drives the ori-anchored_<placement> modifier on the bubble; collision-aware via position-try-fallbacks. |
Events & attributes
OriTooltip declares no custom emits. It does not set inheritAttrs: false, so attributes placed
directly on <OriTooltip> fall through to the root <span class="ori-tooltip"> wrapper.
Slots
| Slot | Scope | Description |
|---|---|---|
default | — | The trigger. Rendered inside .ori-tooltip__trigger. Should contain a real focusable control (button or link) so keyboard focus reveals the tooltip via :focus-visible — bind the slot-scope bubbleId to its aria-describedby to get the announcement. |
content | — | Rich tooltip content. Overrides the content prop when provided. Use for markup beyond plain text (e.g. <kbd>). |