Tooltip

Free

Hover/click/focus-triggered tooltip with 4 positions, 6 theme variants and a configurable show delay.

</>sv-tooltip tooltipoverlayhint
import { SvTooltipComponent } from '@styloviz/overlays';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Click trigger, custom width

example.htmlAngular
<sv-tooltip text="Copied to clipboard" trigger="click" position="right" [maxWidth]="220">
  <button>Copy</button>
</sv-tooltip>

Track visibility

example.htmlAngular
<sv-tooltip text="Save changes" (visibleChange)="onTooltipToggle($event)">
  <button>Save</button>
</sv-tooltip>

Inside a table cell or card — portal it

example.htmlAngular
<!-- The bubble is an absolute child of the trigger wrapper by default, so an
     ancestor with overflow: hidden clips it. [portal]="true" moves it to a
     body portal and positions it by measurement: it escapes the clip, flips
     when the preferred side does not fit, and is clamped to the viewport. -->
<td class="overflow-hidden">
  <sv-tooltip text="Unpaid since 12 May" position="top" [portal]="true">
    <button>Overdue</button>
  </sv-tooltip>
</td>

Explaining a disabled control

example.htmlAngular
<!-- A natively disabled button fires no pointer events and takes no focus,
     so a plain tooltip on it never appears. The preset takes the dead
     control out of hit-testing and gives the wrapper the tab stop.
     Prefer aria-disabled="true" when the control is yours - it needs none of
     this. Never use the preset around an ENABLED control. -->
<sv-tooltip text="Only admins can delete projects" [forDisabledControl]="true">
  <button disabled>Delete</button>
</sv-tooltip>

When to use

  • Revealing a short, supplementary hint on hover/focus of a control.
  • Explaining icon-only buttons or truncated labels.
  • For rich, interactive content use a Popover; for persistent info, inline text.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Inputs

NameTypeDefaultDescription
textstring''Tooltip text content.
positionTooltipPosition'top'Position relative to the trigger.
variantTooltipVariant'dark'Color variant.
sizeTooltipSize'md'Size — controls font and padding.
triggerTooltipTrigger'hover'What triggers the tooltip.
maxWidthnumber240Max width of the tooltip bubble (px).
delaynumber0Delay before showing (ms).
disabledbooleanfalseWhether the tooltip is disabled.
customClassstring''Extra CSS classes merged onto the tooltip bubble.
portalbooleanfalseRender the bubble in a `<body>` portal, positioned by measurement. THE DEFAULT IS AN `absolute` BUBBLE INSIDE THE TRIGGER WRAPPER, AND THAT IS WHY IT CLIPS. an absolutely positioned child is clipped by any ancestor with `overflow: hidden` — which is every table cell, card and scroll area in a dashboard — and the bubble is cut in half or disappears entirely. It is also placed by CSS classes alone, so it has no idea where the viewport edge is: a tooltip on the last column of a table renders off-screen. OPT-IN, DELIBERATELY. The in-place path is byte-for-byte what it was, because this is a published package and consumers style the bubble through its position in the DOM. Turning this on moves the bubble out of the wrapper and positions it with `svPositionAnchored` — the same arithmetic Popover, Popout and Dropdown Menu use — so it escapes the clip, flips when it does not fit, and is clamped to the viewport. Reach for it whenever the trigger is inside a table cell, a card, or anything that scrolls.
flipbooleantrueFlip to the opposite side when the preferred one does not fit. Only meaningful with `[portal]="true"` — the in-place path has no measurement to decide with.
offsetnumber8Gap between trigger and bubble, in px, when portalled. Matches the `mb-2` / `mt-2` the in-place path uses, so turning `portal` on does not move the bubble.
fullWidthbooleanfalseStretch the trigger wrapper to the width of its container. The wrapper is `inline-flex` by default, which is right for a word, an icon or a button — it hugs whatever it wraps. It is wrong the moment the trigger is a card in a grid or flex row: the wrapper becomes the track item and shrinks to its content, so the card no longer fills its cell. Set this to lay the wrapper out as a full-width block instead.
forDisabledControlbooleanfalseMake the tooltip work over a control the browser has `disabled`. A DISABLED CONTROL FIRES NO POINTER EVENTS AND TAKES NO FOCUS, so the tooltip explaining WHY it is disabled is the one tooltip in the kit that could never appear — and it is also the one users most need. The browser does not dispatch mouse events whose target is a disabled form control, so nothing reaches this wrapper either; and the control is out of the tab order, so the keyboard path is gone as well. WHAT THIS DOES IS TWO LINES AND BOTH ARE NECESSARY: the projected content gets `pointer-events: none`, which takes the disabled control out of hit-testing so hover and tap land on the WRAPPER; and the wrapper takes `tabindex="0"`, so the explanation is reachable by keyboard at all. `aria-describedby` already lives on the wrapper, so a screen-reader user who lands there hears the reason. PREFER `aria-disabled="true"` OVER `disabled` AND YOU NEED NONE OF THIS. A control marked `aria-disabled` stays focusable and keeps firing pointer events — it is announced as unavailable, your handler ignores the click, and every tooltip path works untouched. This preset is for the control you cannot change: a third-party component, or markup that is not yours. ⚠️ Do NOT set it around an ENABLED control. `pointer-events: none` applies to whatever is projected, so the control's own clicks stop working. Dev mode warns when the projected content does not look disabled.

Outputs

EventPayload typeDescription
visibleChangebooleanEmitted whenever the tooltip's visibility changes (true = shown, false = hidden).

Customization & theming

  • Four placements and six themes.
  • Hover, click or focus triggers — and on a pointer that cannot hover, a `hover` tooltip becomes tap-to-toggle by itself, with the show delay skipped and a press anywhere else dismissing it. A `focus` or `click` trigger is left alone.
  • Configurable show/hide delay.
  • `customClass` on the bubble; `visibleChange` output.
  • `[portal]="true"` renders the bubble in a `<body>` portal positioned by measurement — the same arithmetic Popover, Popout and Dropdown Menu use. It escapes an `overflow: hidden` ancestor (every table cell, card and scroll area), flips with `[flip]`, is clamped to the viewport, keeps its arrow over the trigger when clamped, and re-anchors on scroll of any ancestor. Off by default so existing markup renders exactly as before.
  • A portalled tooltip paints in the ambient tooltip band, so it clears a dialog it appears inside — and it never becomes the surface Escape closes.
  • `[forDisabledControl]="true"` — the disabled-control preset: the projected control gets `pointer-events: none` so hover and tap land on the wrapper, and the wrapper becomes a tab stop described by the tooltip from the start (Enter/Space toggle a `click` tooltip there). Dev mode warns when the projected content is not actually disabled.

Best practices

  • Do: Keep tooltip text to a few words — it's a hint, not documentation.
  • Do: Ensure it appears on keyboard focus, not hover alone (it does).
  • Do: Set `[portal]="true"` whenever the trigger sits inside a table cell, a card, or anything that scrolls or hides its overflow — otherwise the bubble is clipped by that ancestor, and near a viewport edge it renders off-screen.
  • Don't: Don't hide essential information in a tooltip — many users never trigger it, and a touch user only finds it by tapping. If the text is needed to complete the task, put it in visible helper text, or in a click-opened Popover (`[interactive]="false"` for a read-only note).
  • Don't: Don't put interactive content in a tooltip — links, buttons, form fields. The component takes a plain `text` string and the bubble is `pointer-events-none`, so this is structurally prevented rather than merely discouraged: if you need something clickable in a floating panel, that is a Popover.

Accessibility

  • Shows on keyboard focus, not just hover, and emits `visibleChange` on every transition.
  • Associated via aria-describedby; dismissible with Esc.
  • ★ AND IT WORKS ON TOUCH, which a hover tooltip does not — worse, it half-works: mobile browsers synthesise `mouseenter` on tap, so the tap meant to activate the trigger raises the tooltip over the control just pressed, and with no `mouseleave` coming it stays there. Where hovering is unavailable the tooltip switches to tap-to-toggle, skips the delay (a tap is deliberate), and dismisses on a press elsewhere — there is no `mouseleave` on a finger and no Escape key on a phone. It follows a pointer that changes mid-session, because a convertible laptop folds.

Feedback on Tooltip

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.