Vanilla UI Kit Tooltip v1.0.0 · toast →

Tooltips & popovers, one file.

Zero-dependency tooltips that follow your page's theme on their own. Hover, click, and tab through everything below — every card is live.

Placements

Four sides with auto-flip when there's no room, plus an arrow that tracks the trigger.

<button data-vtt="I'm on top"
        data-vtt-placement="top">

Triggers

Hover (also shows on keyboard focus), click, or focus-only — try tabbing through.

new Tooltip('#save', {
  trigger: 'click'   // hover | click | focus | manual
})

Interactive popover

The panel stays open while your pointer or focus is inside it, so it can hold links and buttons.

new Tooltip('#del', {
  trigger: 'click', interactive: true,
  content: function (el) { return panelEl }
})

HTML content

Content is text by default; html: true is an explicit opt-in for trusted markup.

new Tooltip('#hint', {
  html: true,
  content: 'Press <b>⌘S</b> to save'
})

Manual + API

trigger: 'manual' binds nothing — you drive it with show(), hide(), toggle().

var tip = new Tooltip('#el', {
  trigger: 'manual', content: '…' })
tip.toggle()

Headless & family theming

Tooltip.defaults.styles = false injects no CSS — the behavior and ARIA stay, the look is yours (see Tooltip.css for a starting point). With the VC core loaded, one call themes every family component.

<script src="https://cdn.jsdelivr.net/gh/vanilla-ui-kit/components/tooltip/tooltip.js"></script>
VC.config({ accent: '#b45309' })

API reference

Constructor & usage

new Tooltip(target, options?)   // target: element or CSS selector

One class, four trigger modes:

Add interactive: true for a panel that stays open while the pointer or focus is inside it and may hold focusable content. Also works with CommonJS/AMD and is SSR-safe (no-op without a DOM); Tooltip.create(target, options) is a constructor alias.

Options

Second constructor argument; all keys of Tooltip.defaults. Change a default for every tooltip with, e.g., Tooltip.defaults.placement = 'bottom'.

NameTypeDefaultDescription
contentstring | fn | Element''String, fn(triggerEl) (re-resolved on every show), or a DOM element.
placementstring'top''top' | 'bottom' | 'left' | 'right'; flips to the opposite side when there is no room, clamps to the viewport.
triggerstring'hover''hover' | 'click' | 'focus' | 'manual'. Hover also shows on keyboard focus; 'manual' binds nothing.
delaynumber | objectnullNumber (both ways) or { show, hide } in ms. Hover defaults to { show: 80, hide: 120 }; other triggers to 0. Focus always shows immediately.
offsetnumber8Gap in px between the trigger and the panel.
arrowbooleantrueRender the pointer arrow (it tracks the trigger even when the panel is clamped).
interactivebooleanfalsePopover mode: the panel stays open while the pointer or focus is inside it, and may contain focusable content.
htmlbooleanfalseContent is text by default (rendered with textContent); opt in for trusted markup.
themestring'auto''auto' | 'light' | 'dark' (see Theming).
stylesbooleantruefalse = headless, no CSS ever injected (see Theming).
labelsobject{ popover: 'Popover' }Accessible names; popover labels click-trigger panels.
onShowfunctionnullfn(tooltip) — see Events & callbacks.
onHidefunctionnullfn(tooltip) — see Events & callbacks.

Methods

All methods return the instance (chainable).

MethodReturnsDescription
show()thisShow the panel (re-resolves function content, repositions, joins an open <dialog>’s top layer if the trigger lives in one).
hide()thisHide the panel.
toggle()thisShow if hidden, hide if visible.
update(content?)thisReplace the content; re-renders and repositions if currently visible. Call with no argument to re-render existing (function) content.
destroy()thisHide, remove the panel and all listeners, restore trigger ARIA attributes, and unregister the instance.

Statics & helpers

MemberReturnsDescription
Tooltip.create(target, options?)TooltipConstructor alias — same as new Tooltip(…).
Tooltip.get(target)Tooltip | nullThe instance bound to an element (or selector), or null.
Tooltip.autoInit(root?)Tooltip[]Activate every [data-vtt] under root (default: document) that has no instance yet. Runs automatically on load; call it for content added later.
Tooltip.defaultsobjectGlobal defaults — change any once, e.g. Tooltip.defaults.theme = 'dark'.
Tooltip.saltstring | falseCSS isolation namespace (default 'vc1'). Set your own token before the first tooltip, or false to disable.
Tooltip.cssstringThe full stylesheet, always rendered with the current salt — a starting point for headless styling.
Tooltip.versionstringLibrary version.

Events & callbacks

CallbackSignatureFires
onShowfn(tooltip)After the panel becomes visible.
onHidefn(tooltip)After the panel is hidden.

Tooltip dispatches no CustomEvents — the two callbacks above are the whole event surface.

Declarative init

Every [data-vtt] element is activated on load (and via Tooltip.autoInit(root) for content added later):

AttributeOptionNotes
data-vttcontentThe tooltip text; also marks the element for auto-init.
data-vtt-placementplacementtop / bottom / left / right.
data-vtt-triggertriggerhover / click / focus / manual.
data-vtt-interactiveinteractiveBoolean flag; bare attribute = true, "false"/"0" = false.
data-vtt-themethemeauto / light / dark.
data-vtt-delaydelayNumber in ms (applies to both show and hide).
<button data-vtt="Tooltip text"
        data-vtt-placement="right"
        data-vtt-trigger="click"
        data-vtt-interactive>…</button>

Theming

Auto light/dark with the family’s resolution order: <html data-theme> / data-bs-theme / .dark class → prefers-color-scheme, re-resolved live. Pin one instance with { theme: 'dark' } or all of them with Tooltip.defaults.theme = 'dark'. All colors are CSS custom properties on .vtt (dark values on .vtt[data-theme=dark]):

PropertyRole
--vtt-accentLinks and focus outlines inside the panel.
--vtt-bgPanel background.
--vtt-textText color.
--vtt-mutedSecondary text.
--vtt-faintBorder.
--vtt-shadowPanel shadow.
--vtt-radiusCorner radius.
--vtt-max-widthMaximum panel width.
--vtt-fontFont stack.

CSS isolation — panels render as class="vtt vc1" and structural rules ship salted, so host-page design systems can’t override them; the --vtt-* variable overrides above keep working (var definitions are deliberately unsalted). Custom token: Tooltip.salt = 'acme' before the first tooltip; disable with Tooltip.salt = false.

Headless — Tooltip.defaults.styles = false (or per instance styles: false) injects no CSS, ever. You keep the full behavior (triggers, delays, flipping, ARIA) and the markup contract: .vtt.vtt-open[data-placement="top"][data-theme="dark"] containing .vtt-arrow (only when arrow; position set inline) and .vtt-content; .vtt-interactive is added in popover mode (it re-enables pointer events). Start from Tooltip.css (string) or dist/tooltip.css. With the VC core loaded, VC.config({ accent: … }) themes tooltips and every other family component, and popups position through the shared VC.position engine.

Accessibility

KeyAction
Tab (focus the trigger)Shows hover/focus tooltips immediately (no delay).
Enter / Space (click trigger)Toggles a click popover (native button activation).
Tab (into an interactive panel)Keeps the panel open while focus is inside it.
EscapeDismisses any visible tooltip; click popovers return focus to their trigger.

Hover/focus tooltips follow the WAI-ARIA tooltip pattern: the panel gets role="tooltip" and the trigger gets aria-describedby while it is visible (an existing aria-describedby is preserved and restored). Click popovers set aria-expanded + aria-controls on the trigger; the panel is a labelled role="group" (name via labels.popover). Focusable content inside interactive panels gets :focus-visible outlines; prefers-reduced-motion disables all animation.

Headless mode

Set Tooltip.defaults.styles = false and no CSS is ever injected — you keep the full behavior (triggers, delays, positioning, flipping, ARIA) and the stable .vtt-* class hooks to style however you like. Both demos below run in an <iframe>: the styled examples above have already injected the stylesheet into this page, so a headless tooltip out here would still match those rules.

Unstyled (styles: false)

The raw browser rendering — the panel is a plain positioned <div> (positioning is behavior, so it still lands next to the trigger). It is opened on load; click the trigger to toggle it.

Tooltip.defaults.styles = false          // headless: zero CSS injected
new Tooltip('#tip', { content: '…', trigger: 'click', placement: 'bottom' })

Bring your own design (Tailwind)

The same headless init with the .vtt-* hooks mapped to Tailwind utilities via @apply — a light panel with a matching arrow instead of the kit's dark one, and the .vtt-open state class drives the transition. The button fetches cdn.tailwindcss.com, so nothing loads until you ask.