oriUI

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.

ClassTypeDescription
ori-tooltipBlockWrapper: 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__triggerPartInline-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__bubblePartThe 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 baseShared 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_*ColorApplied 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.

Above the triggerBelow the triggerLeft of triggerRight of trigger
html
<!-- 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 -->
vue
<OriTooltip content="Above the trigger" placement="top">
    <button>top</button>
</OriTooltip>
<OriTooltip content="Below the trigger" placement="bottom">
    <button>bottom</button>
</OriTooltip>
<OriTooltip content="Left of trigger" placement="left">
    <button>left</button>
</OriTooltip>
<OriTooltip content="Right of trigger" placement="right">
    <button>right</button>
</OriTooltip>

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 neutralPrimaryDangerSuccessInfoWarn
html
<!-- 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>
vue
<!-- default (neutral inverse chip) -->
<OriTooltip content="Default neutral" placement="bottom">
    <button>default</button>
</OriTooltip>

<!-- semantic colors -->
<OriTooltip content="Primary" color="primary" placement="bottom">
    <button>primary</button>
</OriTooltip>
<OriTooltip content="Danger" color="danger" placement="bottom">
    <button>danger</button>
</OriTooltip>

Rich content

When plain text is not enough, use the #content slot instead of the content prop. Slot content takes precedence over the prop.

html
<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>
vue
<OriTooltip placement="bottom">
    <button>hover me</button>
    <template #content>
        Keyboard shortcut: <kbd>Ctrl</kbd> + <kbd>S</kbd>
    </template>
</OriTooltip>

With icon-only button

A common pattern: supplement an icon-only button with a tooltip so the action is discoverable via keyboard focus.

Add item
html
<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>
vue
<OriTooltip content="Add item" placement="right">
    <OriButton icon="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" aria-label="Add item" variant="soft" />
</OriTooltip>

Common patterns

Three icon buttons in a toolbar, each with a labeled tooltip — a typical editor or action-bar pattern.

Bold (Ctrl+B)Italic (Ctrl+I)Underline (Ctrl+U)
html
<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>
vue
<div style="display: flex; gap: 0.25rem" role="toolbar" aria-label="Text formatting">
    <OriTooltip content="Bold (Ctrl+B)" placement="bottom">
        <OriButton icon="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z" aria-label="Bold" variant="quiet" />
    </OriTooltip>
    <OriTooltip content="Italic (Ctrl+I)" placement="bottom">
        <OriButton icon="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4h-8z" aria-label="Italic" variant="quiet" />
    </OriTooltip>
    <OriTooltip content="Underline (Ctrl+U)" placement="bottom">
        <OriButton icon="M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z" aria-label="Underline" variant="quiet" />
    </OriTooltip>
</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 requested placement class (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 a useId()-generated id, exposed on the default slot scope as bubbleId. The bubble stays in the DOM permanently (not v-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-describedby is announced when the element bearing it is focused, and the .ori-tooltip__trigger wrapper 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 own aria-label.
  • The trigger's native :focus-visible ring is not overridden — the real control inside keeps its own focus indicator.
  • The decorative arrow is a ::after pseudo-element — invisible to assistive technology, no aria-hidden needed.
  • 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-describedby onto arbitrary slot content without JS. The Vue component therefore renders it on the .ori-tooltip__trigger wrapper and exposes bubbleId on the slot scope; that wrapper copy is advisory only — it announces nothing, because aria-describedby surfaces 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-visible governs the bubble's CSS visibility, not the ARIA announcement; the two are independent.

KeyAction
TabFocuses the trigger control; shows the tooltip.
Shift+TabBlurs the trigger control; hides the tooltip.
Enter / SpaceActivates the trigger and hides the tooltip, as a click does.
EscapeHides 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

PropTypeDefaultDescription
colorThemeColor—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).
contentstring—Tooltip text. For rich content use the #content slot instead — it takes precedence over this prop.
placementAnchoredPlacement'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

SlotScopeDescription
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>).