On this page
Show supplementary context for an interactive element on hover or focus.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
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 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

- Trigger
- Tooltip
Basic usage
Wrap any focusable element with TooltipTrigger and pass the tooltip content as the tooltip prop.
Edit live code
<TooltipTrigger tooltip="Submits your meter reading"> <PrimaryCTAButton>Submit reading</PrimaryCTAButton> </TooltipTrigger>
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:
Edit live code
<TooltipTrigger tooltip="Submits your meter reading"> <PrimaryCTAButton>Submit reading</PrimaryCTAButton> </TooltipTrigger>
A child that is not natively focusable is given tabIndex=0, so it can still be reached by keyboard:
Edit live code
<TooltipTrigger tooltip="Estimated from your last three readings"> <span>1,204 kWh</span> </TooltipTrigger>
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.
Edit live code
<P> This is a paragraph <DefinitionTooltip tooltip="This is the tooltip content">with a tooltip</DefinitionTooltip></P>
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.
Edit live code
<P>Standing charge <IconTooltip tooltip="A fixed daily amount, charged whether or not you use any energy" /></P>
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.
Edit live code
<Cluster spaceBetween={4}> <ClusterItem> <TooltipTrigger tooltip="Above the trigger" placement="top"> <SecondaryCTAButton>Top</SecondaryCTAButton> </TooltipTrigger> </ClusterItem> <ClusterItem> <TooltipTrigger tooltip="Below the trigger (default)" placement="bottom"> <SecondaryCTAButton>Bottom</SecondaryCTAButton> </TooltipTrigger> </ClusterItem> <ClusterItem> <TooltipTrigger tooltip="Left of the trigger" placement="left"> <SecondaryCTAButton>Left</SecondaryCTAButton> </TooltipTrigger> </ClusterItem> <ClusterItem> <TooltipTrigger tooltip="Right of the trigger" placement="right"> <SecondaryCTAButton>Right</SecondaryCTAButton> </TooltipTrigger> </ClusterItem> </Cluster>
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.
Edit live code
<Card data-nebula-tooltip-boundary> <Cluster spaceBetween={2} justify="flex-end"> <ClusterItem> <TooltipTrigger tooltip="This tooltip shifts to stay inside the card rather than the window"> <SecondaryCTAButton>Near the edge</SecondaryCTAButton> </TooltipTrigger> </ClusterItem> </Cluster> </Card>
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:
Edit live code
<TooltipTrigger tooltip="Sends the reading to your supplier" relationship="description"> <PrimaryCTAButton>Submit reading</PrimaryCTAButton> </TooltipTrigger>
The trigger has no visible label, so the tooltip becomes its accessible name:
Edit live code
<TooltipTrigger tooltip="Gas supply" relationship="label"> <Icon name="gas" size="2em" /> </TooltipTrigger>
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.
Edit live code
<Cluster spaceBetween={4}> <ClusterItem> <TooltipTrigger tooltip="Appears after 100ms"> <SecondaryCTAButton>Default delay</SecondaryCTAButton> </TooltipTrigger> </ClusterItem> <ClusterItem> <TooltipTrigger tooltip="Appears after 1500ms" delay> <SecondaryCTAButton>Long delay</SecondaryCTAButton> </TooltipTrigger> </ClusterItem> </Cluster>
usage
- Tooltip must not contain information that is required to complete a taskTooltips 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.Copy rule ID for tooltip.no-critical-information
- Tooltip must not be the only place where information is documentedTooltips are not always reachable. Information that only exists in a tooltip is inaccessible to touch users and users who do not explore every element.Copy rule ID for tooltip.not-only-source
- DefinitionTooltip should be used for inline term definitions, not as a general-purpose tooltip triggerThe 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.Copy rule ID for tooltip.definition-tooltip-for-terms
- IconTooltip should be used only to provide additional context for a labelled element, not as the sole source of important informationIconTooltip 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.Copy rule ID for tooltip.icon-tooltip-info-only
- Page or feature using tooltips should not require tooltips on many elements to be usableWhen 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.Copy rule ID for tooltip.excessive-use-signals-design-problem
content
- Tooltip should contain no more than two short sentencesTooltips 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.Copy rule ID for tooltip.content-concise
- Tooltip should not be used to explain an ambiguous trigger label instead of improving the label itselfA 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.Copy rule ID for tooltip.no-label-improvement-substitute
interaction
- TooltipTrigger will add tabIndex=0 to the child element when the child has no tabIndex set, so the trigger is keyboard-reachable by defaultCopy rule ID for tooltip.will-tabindex
- 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 usedCopy rule ID for tooltip.will-boundary-reposition
- 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 1500msCopy rule ID for tooltip.will-show-on-hover-and-focus
- TooltipTrigger child must be a focusable elementTooltips 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.Copy rule ID for tooltip.trigger-focusable
- TooltipTrigger relationship prop must be set to "label" when the trigger has no other visible text labelWith 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.Copy rule ID for tooltip.relationship-label-for-icon-only
- TooltipTrigger relationship prop should remain as "description" (the default) when the trigger has a visible text labelUsing 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.Copy rule ID for tooltip.relationship-description-for-labelled
- TooltipTrigger child should have a minimum touch target size of 24x24 CSS pixelsTooltips 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.Copy rule ID for tooltip.minimum-touch-target
composition
- TooltipTrigger children must be a single React element, not a string, fragment, or arrayTooltipTrigger 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.Copy rule ID for tooltip.trigger-single-element
- Tooltip must not contain interactive elements such as links, buttons, or form fieldsTooltips do not receive keyboard focus and cannot be navigated into. Interactive content inside a tooltip is unreachable by keyboard and screen reader users.Copy rule ID for tooltip.no-interactive-content
Tooltip
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
isOpen | boolean | — | No | — |
state | TooltipTriggerState | — | No | Open state from `useTooltipTriggerState`, which `TooltipTrigger` supplies automatically. |
placement | bottom top left right | 'bottom' | No | Side of the trigger the tooltip appears on. |
children | ReactNode | — | Yes | Content of the tooltip. |
DefinitionTooltip
Also accepts all props from ButtonHTMLAttributes, HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
tooltip | ReactNode | — | Yes | Content shown in the tooltip. |
placement | bottom top left right | 'bottom' | No | Side of the trigger the tooltip appears on. |
isOpen | boolean | — | No | Controls whether the tooltip is open, overriding hover and focus behaviour. |
delay | boolean | — | No | Waits 1.5 seconds before showing the tooltip on hover instead of 0.1 seconds. |
relationship | label description | 'description' | No | How the tooltip is announced: `description` adds it to the trigger's accessible description, while `label` makes it the trigger's accessible name. |
children | ReactNode | — | Yes | — |
IconTooltip
Also accepts all props from ButtonHTMLAttributes, HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
tooltip | ReactNode | — | Yes | Content shown in the tooltip. |
placement | bottom top left right | 'bottom' | No | Side of the trigger the tooltip appears on. |
isOpen | boolean | — | No | Controls whether the tooltip is open, overriding hover and focus behaviour. |
delay | boolean | — | No | Waits 1.5 seconds before showing the tooltip on hover instead of 0.1 seconds. |
relationship | label description | 'description' | No | 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.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
children | ReactElement | — | Yes | Single focusable element that triggers the tooltip. It is given a `tabIndex` of `0` unless it sets its own. |
tooltip | ReactNode | — | Yes | Content shown in the tooltip. |
placement | bottom top left right | 'bottom' | No | Side of the trigger the tooltip appears on. |
delay | boolean | — | No | Waits 1.5 seconds before showing the tooltip on hover instead of 0.1 seconds. |
relationship | label description | 'description' | No | How the tooltip is announced: `description` adds it to the trigger's accessible description, while `label` makes it the trigger's accessible name. |
isOpen | boolean | — | No | Controls whether the tooltip is open, overriding hover and focus behaviour. |
Styles
text-trigger
Defined in Tooltip/DefinitionTooltip.tsx
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
Defined in Tooltip/IconTooltip.tsx
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
Defined in Tooltip/Tooltip.tsx
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]};
}
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
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)
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
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
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 these tests and statuses mean
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.
The tests
- 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.
The statuses
- 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.