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
entirely in CSS — no JS state machine, no positioning engine. Show is pure :focus-within (keyboard)
and :hover wrapped in @media (hover: hover) (pointer, without sticking open on touch).
The examples are organised 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 (a shared default anchor-name pairing the bubble with its own trigger, so no per-instance wiring is needed) 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 aria-describedby referencing the bubble id and the anchor-name that pairs it with the bubble. |
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). |
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">
<span class="ori-tooltip__trigger" aria-describedby="tip-1">
<button>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" aria-describedby="tip-2"><button>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" aria-describedby="tip-3"><button>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" aria-describedby="tip-4">
<button>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" aria-describedby="tip-5">
<button class="ori-button ori-button_icon ori-variant_tonal ori-color_primary" aria-label="Add item">
<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" aria-describedby="tip-bold">
<button class="ori-button ori-button_icon ori-variant_plain …" aria-label="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
OriTooltip is a CSS-only implementation. The following features would require JavaScript and are out of scope for this component:
- Esc-to-dismiss — the CSS model dismisses on blur / pointer-leave only.
- 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 — the standalone classes and the Vue component render the same attributes.
- 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.aria-describedbyis announced when the element bearing it is focused, and the.ori-tooltip__triggerwrapper is a non-focusable<span>— so the wrapper's ownaria-describedbydoes not produce the announcement on its own (it only guarantees the id resolves). The component can't augment arbitrary slot content without JS, so 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 (a NOTES.md gotcha confirmed for OriProgress and OriTooltip alike).
Design / a11y trade-off: a pure-CSS tooltip can't inject
aria-describedbyonto arbitrary slot content without JS, so the component puts it on the.ori-tooltip__triggerwrapper (which only guarantees the bubble id resolves) and exposesbubbleIdon the slot scope. For the description to actually be announced, the consumer binds:aria-describedby="bubbleId"on their own focusable control —aria-describedbyannounces on focus of the element bearing it, and the wrapper isn't focusable.:focus-withingoverns 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. |
Escape | Not supported (CSS-only model — blur the trigger to hide it). |
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. (Svelte bindings are planned.)
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, which carries aria-describedby. Should contain a real focusable control (button or link) so keyboard focus reveals the tooltip via :focus-within. |
content | — | Rich tooltip content. Overrides the content prop when provided. Use for markup beyond plain text (e.g. <kbd>). |