# Tooltip Show supplementary context for an interactive element on hover or focus. Section: components Docs: https://nebula.kaluza.com/components/tooltip --- ## When to use - To provide supplementary, non-critical context for an interactive element, for example explaining what a button or field does when the label alone is ambiguous - To define a technical term or piece of industry jargon inline. Use `DefinitionTooltip` for this case. - To label an icon-only control where a visible text label is not possible. Use `IconTooltip` for this case. ## When not to use - **For critical information** - tooltips are not reliably accessible on touch devices and may be missed by keyboard users who do not explore every element. If users need the information to complete a task, show it in the interface. - **To compensate for a poor label** - if the trigger label needs a tooltip to be understood, improve the label instead. - **For interactive content** - tooltips cannot contain links, buttons, or other interactive elements. They do not receive focus and are not navigable. - **For long or complex content** - a tooltip should be short enough to read at a glance. For longer explanations, use a [Disclosure](/components/disclosure) instead. - **As the only place information is documented** - tooltips are not always reachable. Any information in a tooltip should also be available through another route. - **For form field context** - if you need to explain what to enter in a form field, use the hint text prop on the field component instead. Hint text is always visible and does not require discovery. ## Anatomy 1. **Trigger** 2. **Tooltip** ## Basic usage Wrap any focusable element with `TooltipTrigger` and pass the tooltip content as the `tooltip` prop. ```tsx Submit reading ``` ## Variants and features ### TooltipTrigger `TooltipTrigger` wraps any single focusable element and manages tooltip state. The child element is cloned with the necessary ARIA and event handler props. Any element that is not natively focusable (such as a div or span) will be given `tabIndex=0` automatically. A natively focusable child is used as-is: ```tsx Submit reading ``` A child that is not natively focusable is given `tabIndex=0`, so it can still be reached by keyboard: ```tsx 1,204 kWh ``` ### DefinitionTooltip Use `DefinitionTooltip` when you need to define a term or phrase inline within body text. The trigger renders as the surrounding text with a dotted underline in `colors.info`, signalling that additional context is available. ```tsx

This is a paragraph with a tooltip

``` ### IconTooltip Use `IconTooltip` to add a supplementary information tooltip next to a label or heading. It renders a hardcoded info icon button. The icon and button cannot be customised. If you need a different trigger, use `TooltipTrigger` directly. ```tsx

Standing charge

``` ### Placement Tooltips default to appearing below the trigger. Use `placement` to change the direction. Collision detection runs automatically for `top` and `bottom` placements. The tooltip shifts horizontally if it would overflow the window or a boundary element. ```tsx Top Bottom Left Right ``` ### Tooltip boundary By default, collision detection uses the window width as the boundary. To constrain the tooltip within a specific container, for example a panel or modal, add the `data-nebula-tooltip-boundary` attribute to that ancestor element. ```tsx Near the edge ``` ### Accessible name vs description The `relationship` prop controls how the tooltip content is announced by screen readers. - **`description` (default):** the tooltip provides supplementary context. The trigger has its own visible label; the tooltip describes it. Screen readers announce both the label and the tooltip. - **`label`:** the tooltip is the accessible name of the trigger. Use this when the trigger has no other visible label, for example an icon-only button. The tooltip content replaces the trigger's accessible name. The trigger has its own visible label, so the tooltip describes it: ```tsx Submit reading ``` The trigger has no visible label, so the tooltip becomes its accessible name: ```tsx ``` ### Delay Tooltips appear after a 100ms delay by default. Set `delay={true}` to use a 1500ms delay, appropriate when tooltips might be triggered accidentally during normal page interaction. ```tsx Default delay Long delay ``` ## Rules and guidance ### Usage - **Tooltip** MUST NOT contain information that is required to complete a task. Rationale: Tooltips are not reliably accessible on touch devices and may not be discovered by all keyboard users. If information is required, show it in the interface, in hint text, a label, or a Disclosure. [id: tooltip.no-critical-information; testability: context] - **Tooltip** MUST NOT be the only place where information is documented. Rationale: Tooltips are not always reachable. Information that only exists in a tooltip is inaccessible to touch users and users who do not explore every element. [id: tooltip.not-only-source; testability: manual] - **DefinitionTooltip** SHOULD be used for inline term definitions, not as a general-purpose tooltip trigger. Rationale: The dotted underline styling is a recognised convention for "this word has a definition". Using DefinitionTooltip for non-definitional content breaks this convention and confuses users who expect a definition. [id: tooltip.definition-tooltip-for-terms; testability: manual] - **IconTooltip** SHOULD be used only to provide additional context for a labelled element, not as the sole source of important information. Rationale: IconTooltip renders an info icon with aria-label="Info". It is designed for optional supplementary context. Important information should be visible in the label or hint text. [id: tooltip.icon-tooltip-info-only; testability: manual] - **Page or feature using tooltips** SHOULD NOT require tooltips on many elements to be usable. Rationale: When most elements need a tooltip to be understood, the interface itself needs work, not more tooltips. Over-reliance on tooltips burdens users who have to hover or focus every element to understand what they are looking at. [id: tooltip.excessive-use-signals-design-problem; testability: context] ### Content - **Tooltip** SHOULD contain no more than two short sentences. Rationale: Tooltips are transient and cannot be scrolled. Long content is truncated or overflows its container. If more explanation is needed, use a Disclosure to reveal persistent in-context content. [id: tooltip.content-concise; testability: manual] - **Tooltip** SHOULD NOT be used to explain an ambiguous trigger label instead of improving the label itself. Rationale: A tooltip that is needed to understand a label indicates the label is insufficient. Improving the label reduces cognitive load for all users, including those who cannot access the tooltip. [id: tooltip.no-label-improvement-substitute; testability: manual] ### Interaction - **TooltipTrigger** WILL add tabIndex=0 to the child element when the child has no tabIndex set, so the trigger is keyboard-reachable by default. [id: tooltip.will-tabindex; testability: auto] - **Tooltip** WILL adjust its horizontal offset to stay within the nearest ancestor that has the data-nebula-tooltip-boundary attribute. When no boundary ancestor is present, the viewport edge is used. [id: tooltip.will-boundary-reposition; testability: auto] - **Tooltip** WILL open after a 100ms delay on hover or keyboard focus, and close when the trigger loses focus or the pointer leaves. When the delay prop is true, the hover delay is 1500ms. [id: tooltip.will-show-on-hover-and-focus; testability: auto] - **TooltipTrigger child** MUST be a focusable element. Rationale: Tooltips are revealed on hover and keyboard focus. A trigger that is not focusable is inaccessible to keyboard users. TooltipTrigger automatically adds tabIndex=0 if the child has no tabIndex, but non-interactive elements should have a semantic role too. [id: tooltip.trigger-focusable; testability: auto] - **TooltipTrigger relationship prop** MUST be set to "label" when the trigger has no other visible text label. Rationale: With relationship="description" (the default), screen readers announce the trigger's own label first, then the tooltip as supplementary context. If the trigger has no visible label, there is nothing for the screen reader to announce first. The tooltip must become the accessible name via aria-labelledby. [id: tooltip.relationship-label-for-icon-only; testability: manual] - **TooltipTrigger relationship prop** SHOULD remain as "description" (the default) when the trigger has a visible text label. Rationale: Using relationship="label" on an element that already has a visible label causes the tooltip to replace rather than supplement the accessible name. Screen readers may announce the tooltip content instead of the visible label, which is confusing. [id: tooltip.relationship-description-for-labelled; testability: manual] - **TooltipTrigger child** SHOULD have a minimum touch target size of 24x24 CSS pixels. Rationale: Tooltips revealed by hover are not accessible on touch-only devices. Where a tooltip trigger must function on touch, the touch target must be large enough to activate reliably. [id: tooltip.minimum-touch-target; testability: manual] ### Composition - **TooltipTrigger children** MUST be a single React element, not a string, fragment, or array. Rationale: TooltipTrigger uses cloneElement to add ARIA and event handler props to the child. This only works on a single React element. Passing multiple children or a string will throw a runtime error. [id: tooltip.trigger-single-element; testability: auto] - **Tooltip** MUST NOT contain interactive elements such as links, buttons, or form fields. Rationale: Tooltips do not receive keyboard focus and cannot be navigated into. Interactive content inside a tooltip is unreachable by keyboard and screen reader users. [id: tooltip.no-interactive-content; testability: manual] ## Accessibility ### In brief Tooltip shows a short message when the trigger is hovered or focused with the keyboard. A screen reader announces the trigger, then the message as its description. The tooltip itself never takes focus. ### What Nebula provides - The tooltip has `role="tooltip"`, and while it is open the trigger points to it with `aria-describedby`, or with `aria-labelledby` for the `label` relationship. - It opens on hover and on keyboard focus of the trigger, and a closed tooltip is hidden from screen readers. - `IconTooltip` and `DefinitionTooltip` triggers are native buttons with a visible focus ring. ### Keyboard behaviour The trigger is a single tab stop, reached with `Tab`. The tooltip is not a tab stop. #### Interactions - **Tab** - Moves focus to the trigger and shows the tooltip ### Accessible design #### Don't put interactive elements in a tooltip The tooltip never takes focus, so a link or button inside it cannot be reached with the keyboard. WCAG: [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) #### Give a custom trigger a target of at least 24 by 24 CSS pixels The tooltip is only as easy to open as its trigger is to hit, and a small custom trigger is hard to hover or tap for a user with limited dexterity. WCAG: [2.5.8 Target Size (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) ### Accessible development #### Wrap a control, not a bare `Icon` `TooltipTrigger` makes its child focusable, but `Icon` is hidden from screen readers, so the focus stop is one a screen reader skips, and the tooltip hangs off it. Wrap a button, a link or another real control. WCAG: [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) #### If the trigger has no name of its own, set `relationship="label"` The tooltip is announced as a description by default, after the trigger's own name. A trigger with no visible text needs the tooltip as its name. WCAG: [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) ### Accessibility testing status Reviewed on 9 October 2026 against v17.0.1. | Test | Status | Tested with | | --- | --- | --- | | Default state | Tested | axe-core via the Storybook test runner (CI) | | Advanced states | Partially tested | axe-core via the Storybook test runner (CI) | | Keyboard navigation | Partially tested | keyboard (reported by the owner) | | Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) | What each test covers: - Default state: The component as it first appears, before anything is clicked or changed. - Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering. - Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring. - Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state. What these statuses mean: - Tested: Every check in this row passed in an automated run that repeats on every pull request. - Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change. - Partially tested: Some checks in this row were run once and passed; the rest are still to do. - Issue found: A check in this row failed. The ticket is shown beside the status. - Not tested: No check in this row has been run yet. - Not applicable: This row does not apply to the component, for the reason given beside it. The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used. ## Properties ### Tooltip - `isOpen` (type `boolean`, optional). - `state` (type `TooltipTriggerState`, optional). Open state from `useTooltipTriggerState`, which `TooltipTrigger` supplies automatically. - `placement` (type `'bottom' | 'top' | 'left' | 'right'`, optional, default `'bottom'`). Side of the trigger the tooltip appears on. - `children` (type `ReactNode`, required). Content of the tooltip. ### DefinitionTooltip Also accepts all props from `ButtonHTMLAttributes`, `HTMLAttributes`. - `tooltip` (type `ReactNode`, required). Content shown in the tooltip. - `placement` (type `'bottom' | 'top' | 'left' | 'right'`, optional, default `'bottom'`). Side of the trigger the tooltip appears on. - `isOpen` (type `boolean`, optional). Controls whether the tooltip is open, overriding hover and focus behaviour. - `delay` (type `boolean`, optional). Waits 1.5 seconds before showing the tooltip on hover instead of 0.1 seconds. - `relationship` (type `'label' | 'description'`, optional, default `'description'`). How the tooltip is announced: `description` adds it to the trigger's accessible description, while `label` makes it the trigger's accessible name. - `children` (type `ReactNode`, required). ### IconTooltip Also accepts all props from `ButtonHTMLAttributes`, `HTMLAttributes`. - `tooltip` (type `ReactNode`, required). Content shown in the tooltip. - `placement` (type `'bottom' | 'top' | 'left' | 'right'`, optional, default `'bottom'`). Side of the trigger the tooltip appears on. - `isOpen` (type `boolean`, optional). Controls whether the tooltip is open, overriding hover and focus behaviour. - `delay` (type `boolean`, optional). Waits 1.5 seconds before showing the tooltip on hover instead of 0.1 seconds. - `relationship` (type `'label' | 'description'`, optional, default `'description'`). How the tooltip is announced: `description` adds it to the trigger's accessible description, while `label` makes it the trigger's accessible name. ### TooltipTrigger Also accepts all props from `ButtonHTMLAttributes`, `HTMLAttributes`. - `children` (type `ReactElement`, required). Single focusable element that triggers the tooltip. It is given a `tabIndex` of `0` unless it sets its own. - `tooltip` (type `ReactNode`, required). Content shown in the tooltip. - `placement` (type `'bottom' | 'top' | 'left' | 'right'`, optional, default `'bottom'`). Side of the trigger the tooltip appears on. - `delay` (type `boolean`, optional). Waits 1.5 seconds before showing the tooltip on hover instead of 0.1 seconds. - `relationship` (type `'label' | 'description'`, optional, default `'description'`). How the tooltip is announced: `description` adds it to the trigger's accessible description, while `label` makes it the trigger's accessible name. - `isOpen` (type `boolean`, optional). Controls whether the tooltip is open, overriding hover and focus behaviour. ## Styles Reference only; you do not need this to use the component. This is the CSS its styled elements use, with theme tokens shown by name. `${...}` is an interpolation (a theme token, a mixin or another value from the code). A `&[prop]` or `@when ` block holds CSS that applies only under that condition; these are notation, not valid CSS. ### text-trigger ```css background: none; color: inherit; border: none; padding: 0; font: inherit; cursor: pointer; outline: inherit; text-decoration: underline; text-decoration-style: dotted; text-decoration-color: ${colors.info}; text-underline-offset: 0.18em; border-radius: ${radii.standard}; &:hover { text-decoration-color: ${colors.infoAlt}; } &:focus-visible { ${focusRing} } ``` ### trigger ```css display: inline-block; background: none; color: ${colors.info}; border: none; font: inherit; cursor: pointer; outline: inherit; padding: ${space[1]}; margin: -${space[1]}; width: ${space[6]}; height: ${space[6]}; line-height: ${space[4]}; &:hover { color: ${colors.infoAlt}; } &:focus-visible svg { border-radius: ${radii.standard}; ${focusRing} } ``` ### tooltip ```css position: absolute; z-index: 50; ${usePlacementStyles(placement, isOpen)} margin-left: ${offset}px; max-width: 25rem; text-align: left; font-weight: ${fontWeights.normal}; line-height: ${lineHeights.small}; padding: ${space[2]}; border-radius: ${radii.standard}; background-color: ${colors.heading}; color: ${colors.primaryContrast}; vertical-align: middle; width: max-content; white-space: normal; font-size: ${responsiveFontSizes.small[0]}; ${mediaQueries.smallAndUp} { font-size: ${responsiveFontSizes.small[1]}; } ``` ### trigger-wrapper ```css position: relative; display: inline-flex; ```