Tooltip
FreeHover/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 demoMore examples
Click trigger, custom width
example.htmlAngular
3 lines
<sv-tooltip text="Copied to clipboard" trigger="click" position="right" [maxWidth]="220"> <button>Copy</button> </sv-tooltip>
Track visibility
example.htmlAngular
3 lines
<sv-tooltip text="Save changes" (visibleChange)="onTooltipToggle($event)"> <button>Save</button> </sv-tooltip>
Inside a table cell or card — portal it
example.htmlAngular
9 lines
<!-- 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
8 lines
<!-- 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
| Name | Type | Default | Description |
|---|---|---|---|
| text | string | '' | Tooltip text content. |
| position | TooltipPosition | 'top' | Position relative to the trigger. |
| variant | TooltipVariant | 'dark' | Color variant. |
| size | TooltipSize | 'md' | Size — controls font and padding. |
| trigger | TooltipTrigger | 'hover' | What triggers the tooltip. |
| maxWidth | number | 240 | Max width of the tooltip bubble (px). |
| delay | number | 0 | Delay before showing (ms). |
| disabled | boolean | false | Whether the tooltip is disabled. |
| customClass | string | '' | Extra CSS classes merged onto the tooltip bubble. |
| portal | boolean | false | Render 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. |
| flip | boolean | true | Flip 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. |
| offset | number | 8 | Gap 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. |
| fullWidth | boolean | false | Stretch 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. |
| forDisabledControl | boolean | false | Make 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
| Event | Payload type | Description |
|---|---|---|
| visibleChange | boolean | Emitted 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.