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.
How tooltips work
Section titled “How tooltips work”- 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-positionplaces the bubble:top,top-left,top-right,bottom,bottom-left,bottom-right,leftorright. 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
:hoverand:focus-visibleand 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 |
Basic tooltip
Section titled “Basic tooltip”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>Positions
Section titled “Positions”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>Customise with variables
Section titled “Customise with variables”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>.danger-zone { --nu-tooltip--Background: #{token(danger)}; --nu-tooltip--Color: #{token(text-on-danger)};}--nu-tooltip--Background also colours the arrow, so both always match.
Variables
Section titled “Variables”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
Section titled “tooltip-variables”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.
Configuration
Section titled “Configuration”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.
Errors
Section titled “Errors”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.
Accessibility and RTL
Section titled “Accessibility and RTL”- 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-labelwhen the tooltip names an icon-only control, oraria-describedbypointing 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 plainspanordiv. - 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.
topandbottomare centred and look the same in both directions.top-left,top-right,bottom-left,bottom-right,leftandrightare physical: choose the position per direction if the bubble must stay on the inline-end side.
<div class="py-12 text-center"> <button type="button" aria-label="Settings" data-tooltip="Settings" data-tooltip-position="top">⚙</button></div><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>