Skip to content

Tooltips

Tooltips show a short text label next to an element when it is hovered or focused with the keyboard. They are pure CSS: the text comes from a data-tooltip attribute and is drawn by the element’s ::after pseudo-element, with a small arrow drawn by ::before. No JavaScript is needed.

  • One attribute. Any element with data-tooltip="..." is a trigger. role="tooltip" is not needed on the trigger, and should not be added: it would replace the trigger’s real role (button, link) for assistive technology.
  • Position attribute. data-tooltip-position places the bubble: top, top-left, top-right, bottom, bottom-left, bottom-right, left or right. Without it the bubble shows on top, with its arrow and offset.
  • Optional width. data-tooltip-size (small, medium, large) gives the bubble a fixed width and lets the text wrap. Without it, the text stays on one line.
  • Shown on hover and keyboard focus. The bubble fades in on :hover and :focus-visible and slides into place in both cases: 5px for the top positions, 10px for the others.
  • Component variables. Colours, size, timing and type are --nu-tooltip--* custom properties, so a scope or a single trigger can restyle them without new CSS rules.
Attribute Values Default
data-tooltip The tooltip text Required
data-tooltip-position top, top-left, top-right, bottom, bottom-left, bottom-right, left, right top
data-tooltip-size small, medium, large None: one line, as wide as the text

Hover the button, or move focus to it with the Tab key.

<div class="py-12 text-center">
<button type="button" data-tooltip="Saves a draft" data-tooltip-position="top">Save</button>
</div>

top and bottom centre the bubble on the trigger. The -left and -right variants keep the arrow centred and extend the bubble to one side, which keeps it on screen for a trigger near the edge of the page. left and right place the bubble beside the trigger, centred vertically.

<div class="flex wrap gap-6 py-12 px-16">
<button type="button" data-tooltip="Top" data-tooltip-position="top">top</button>
<button type="button" data-tooltip="Extends to the left" data-tooltip-position="top-left">top-left</button>
<button type="button" data-tooltip="Extends to the right" data-tooltip-position="top-right">top-right</button>
</div>
<div class="flex wrap gap-6 py-12 px-16">
<button type="button" data-tooltip="Bottom" data-tooltip-position="bottom">bottom</button>
<button type="button" data-tooltip="Extends to the left" data-tooltip-position="bottom-left">bottom-left</button>
<button type="button" data-tooltip="Extends to the right" data-tooltip-position="bottom-right">bottom-right</button>
</div>
<div class="flex wrap gap-6 py-6 px-16">
<button type="button" data-tooltip="Left" data-tooltip-position="left">left</button>
<button type="button" data-tooltip="Right" data-tooltip-position="right">right</button>
</div>

The position names are physical sides: left is always the left side of the screen, also on a right-to-left page.

By default the text does not wrap (white-space: nowrap), so a long label makes a wide bubble. A size gives the bubble a fixed width from a variable and lets the text wrap.

Value Width variable Default
small --nu-tooltip--WidthSmall 80px
medium --nu-tooltip--WidthMedium 150px
large --nu-tooltip--WidthLarge 260px
<div class="flex wrap gap-6 py-16 px-8">
<button type="button" data-tooltip="Copied to the clipboard" data-tooltip-position="top" data-tooltip-size="small">small</button>
<button type="button" data-tooltip="Copies the share link to the clipboard" data-tooltip-position="top" data-tooltip-size="medium">medium</button>
<button type="button" data-tooltip="Copies a share link that works for anyone with access to this project" data-tooltip-position="top" data-tooltip-size="large">large</button>
</div>

The variables are emitted on :root, or on $parent-selector when one is set. The colour variables are also re-declared in every theme block, so a tooltip inside a data-theme section takes that section’s colours. Override them on any ancestor to restyle a group of tooltips, or on the trigger itself.

<div class="py-12 text-center">
<button type="button" data-tooltip="Deletes the file" data-tooltip-position="top" style="--nu-tooltip--Background: var(--nu-danger); --nu-tooltip--Color: var(--nu-text-on-danger);">Delete</button>
</div>

--nu-tooltip--Background also colours the arrow, so both always match.

The names follow the component grammar, --nu-tooltip--{Name}, not the single-hyphen token grammar: they are settings of one component, not design tokens, and token() does not accept them.

Variable Default Description
--nu-tooltip--Background var(--nu-text-regular) Bubble and arrow colour. Opaque, and flips with the theme.
--nu-tooltip--Color var(--nu-text-inverted) Text colour.
--nu-tooltip--ShadowColor rgb(0 0 0 / 20%) Colour of the bubble shadow (0 3px 7px).
--nu-tooltip--ZIndex var(--nu-z-tooltip) Stacking level of the bubble and arrow. See Z-index.
--nu-tooltip--TransitionDuration var(--nu-duration-200) The default transition duration token.
--nu-tooltip--TransitionTimingFunction var(--nu-ease-in-out) The default transition easing token.
--nu-tooltip--TransitionDelay 0s Delay before the fade starts.
--nu-tooltip--FontSize var(--nu-font-size-sm) Text size.
--nu-tooltip--FontWeight normal Text weight.
--nu-tooltip--TextTransform none Text transform, for example uppercase.
--nu-tooltip--WidthSmall 80px Width for data-tooltip-size="small".
--nu-tooltip--WidthMedium 150px Width for data-tooltip-size="medium".
--nu-tooltip--WidthLarge 260px Width for data-tooltip-size="large".
--nu-tooltip--BorderRadius var(--nu-radius-xs) Bubble corner radius.

Because the background is the regular text colour and the text is the inverted text colour, the bubble contrasts with the page in both light and dark themes. The duration and easing follow the $transition-default-duration and $transition-default-ease settings.

tooltip-variables emits the full set of variables with their defaults. Core already includes it on :root (or $parent-selector); include it again only to reset a scope to the defaults, then override what you need.

.help-panel {
@include tooltip-variables;
--nu-tooltip--WidthLarge: 320px;
}

There is no mixin that builds a tooltip on your own selector: the tooltip rules are written for the data-tooltip attribute.

Tooltips are a component and are controlled by the tooltips feature flag alone. The variables and rules are emitted whenever the flag is on, whether or not $generate-utility-classes is true; set "tooltips": false to leave them out.

@use "@nuvoui/core" with (
$feature-flags: ("tooltips": false)
);

With $parent-selector set, the variables are emitted on that selector and the rules match [data-tooltip] inside it only.

The tooltip rules sit in the components cascade layer, so your own unlayered CSS overrides them without higher specificity.

When the theme feature flag is off, no colour tokens exist and the variables use fixed values: rgba(17, 17, 17, 0.9) for the background and #fff for the text.

The defaults read keys from several scale maps. A custom map without one of these keys raises an error instead of emitting a tooltip without that value.

Key Map
tooltip $z-index-levels
sm $font-sizes
xs $border-radii
The default duration and easing $durations, $timing-functions
// error
@use "@nuvoui/core" with (
$z-index-levels: ("modal": 900)
);

The compiler reports: Tooltips need the key tooltip in $z-index-levels. Configured keys: modal. Add tooltip back to $z-index-levels.

  • Give the trigger an accessible name or description. Text drawn with content: attr(data-tooltip) is not reliably announced: some browser and screen reader combinations read pseudo-element content, others do not. Put the information in the markup as well: aria-label when the tooltip names an icon-only control, or aria-describedby pointing at visually hidden text when it adds a description.
  • Use focusable triggers. The bubble appears on :focus-visible, so keyboard users see it only on elements that can receive focus, such as buttons and links. Do not put a tooltip on a plain span or div.
  • Touch screens have no hover. On a phone or tablet the bubble may not appear, or may stay visible after a tap. Never put information that a user needs only in a tooltip.
  • Short, non-interactive text. The bubble is plain text. Links or buttons cannot go inside it.
  • Dismissal. A CSS-only tooltip cannot be closed with the Escape key while the pointer or focus stays on the trigger. WCAG 1.4.13 (Content on Hover or Focus) asks for dismissible content when it can cover other content; keep tooltips small and place them where they do not hide anything important.
  • Reduced motion. The NuvoUI reset shortens all transitions to almost zero under prefers-reduced-motion: reduce, so the fade and slide are skipped for users who ask for less motion.
  • Right to left. top and bottom are centred and look the same in both directions. top-left, top-right, bottom-left, bottom-right, left and right are physical: choose the position per direction if the bubble must stay on the inline-end side.
An icon-only button: the accessible name comes from aria-label, the tooltip shows it visually
<div class="py-12 text-center">
<button type="button" aria-label="Settings" data-tooltip="Settings" data-tooltip-position="top">&#9881;</button>
</div>
A description: aria-describedby points at visually hidden text with the same content
Visible to everyone with the link
<div class="py-12 text-center">
<button type="button" aria-describedby="publish-help" data-tooltip="Visible to everyone with the link" data-tooltip-position="top">Publish</button>
<span id="publish-help" class="sr-only">Visible to everyone with the link</span>
</div>