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

ClassTypeDescription
ori-tooltipBlockWrapper: 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__triggerPartInline-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__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).

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">
    <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 -->
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" 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>
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" 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>
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" 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>
vue
<OriTooltip content="Add item" placement="right">
    <OriButton icon="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" aria-label="Add item" variant="tonal" />
</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" 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>
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="plain" />
    </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="plain" />
    </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="plain" />
    </OriTooltip>
</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 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 — the standalone classes and the Vue component render the same attributes.

  • 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. aria-describedby is announced when the element bearing it is focused, and the .ori-tooltip__trigger wrapper is a non-focusable <span> — so the wrapper's own aria-describedby does 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 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 (a NOTES.md gotcha confirmed for OriProgress and OriTooltip alike).

Design / a11y trade-off: a pure-CSS tooltip can't inject aria-describedby onto arbitrary slot content without JS, so the component puts it on the .ori-tooltip__trigger wrapper (which only guarantees the bubble id resolves) and exposes bubbleId on the slot scope. For the description to actually be announced, the consumer binds :aria-describedby="bubbleId" on their own focusable control — aria-describedby announces on focus of the element bearing it, and the wrapper isn't focusable. :focus-within 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.
EscapeNot 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

PropTypeDefaultDescription
colorThemeColorBubble 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).
contentstringTooltip 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
defaultThe 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.
contentRich tooltip content. Overrides the content prop when provided. Use for markup beyond plain text (e.g. <kbd>).