Tooltip
A short label that appears on hover or focus.
import { Tooltip } from "@/ui";Tooltip wraps MUI's Tooltip in Petavue styling: a dark box with 12px white text that fades in over 180ms. Use it to name an icon-only control, to explain a control whose label cannot say everything, or to show text that has been cut short.
InfoTooltip, also exported from @/ui, is a different thing: a static label with an info icon that does not open or position itself. src/components/Tooltip.jsx is a third, hand-built tooltip that nothing imports. Use the default Tooltip export described here.
Usage
Wrap one element and give the text in title. Pass describeChild so the element keeps its own name and the tooltip is read as extra description.
<Tooltip title="Pull the latest numbers from HubSpot and Google Ads" describeChild>
<Button variant="secondaryGhost" label="Refresh data" icon={ArrowsClockwise} />
</Tooltip>The child must accept a ref and pass on the event props it receives. Button, native elements and anything built with forwardRef do. A plain function component that drops unknown props does not, and the tooltip will never open.
Icon-only buttons
Give the button an aria-label and pass describeChild. A child's own aria-label always takes priority over the tooltip, and with describeChild the tooltip text is never used as the name, so the tooltip can later say more than the label without renaming the button.
<Tooltip title="Delete page" describeChild>
<Button variant="secondaryGhost" icon={Trash} aria-label="Delete page" />
</Tooltip>When the tooltip is the name
Without describeChild, a text title is copied onto the child as its aria-label, unless the child passes an aria-label of its own. That is fine for an icon-only button with no label, where the tooltip text is the name.
<Tooltip title="Download as CSV">
<Button variant="secondaryGhost" icon={DownloadSimple} />
</Tooltip>Without describeChild | With describeChild | |
|---|---|---|
Text title | Set as the child's aria-label. An aria-label passed on the child wins. | The child keeps its name. While open, the tooltip is linked with aria-describedby. While closed, the text is also set as the child's title attribute. |
Element title | While open, the tooltip becomes the child's name through aria-labelledby. | While open, linked with aria-describedby. |
Any control with visible text needs describeChild. An aria-label overrides visible text, so without it "Refresh data" above would be announced as "Pull the latest numbers from HubSpot and Google Ads".
Placement
The default is bottom. Use top for most controls in the product so the tooltip does not cover what comes next. left and right suit narrow rails. Add arrow when the tooltip could be read as belonging to a neighbouring control. MUI flips the tooltip to the other side if there is no room.
<Tooltip title="Opens above" placement="top">…</Tooltip>
<Tooltip title="Opens below" placement="bottom">…</Tooltip>
<Tooltip title="Opens to the left" placement="left">…</Tooltip>
<Tooltip title="Opens to the right" placement="right">…</Tooltip>
<Tooltip title="With an arrow" placement="top" arrow>…</Tooltip>Text that is cut short
With displayTooltipOnOverflow, the tooltip only opens when its child is actually truncated. Hover both names: only the long one opens. The child must be the element that has the ellipsis, because that is the one measured. A ResizeObserver re-checks when the width changes.
<Tooltip title={campaign.name} displayTooltipOnOverflow placement="top">
<span className="block truncate max-w-[200px]">{campaign.name}</span>
</Tooltip>Rich content
title can be any element, such as a small legend. Keep it to a few lines. Raise or lower the width cap with maxWidth. If people need to click inside it, use a popover instead.
<Tooltip title={<SyncStatusLegend />} arrow placement="top" maxWidth="260px" describeChild>
<span tabIndex={0} aria-label="What the sync colours mean"><Info size={16} /></span>
</Tooltip>On a disabled control
A disabled button does not fire the events the tooltip listens for, and MUI logs an error if you wrap one directly. Wrap it in a focusable <span> and put the tooltip on that. This is the right place to say why the action is not available.
<Tooltip title="Connect a domain before you publish" placement="top">
<span className="inline-flex" tabIndex={0}>
<Button variant="primary" label="Publish page" disabled />
</span>
</Tooltip>Look
| Part | Style |
|---|---|
| Box | #424766 (--color-neutral-700) with a 1px #EEF0F7 (--color-neutral-100) border, 6px corners, 4px by 12px padding. |
| Text | 12px Poppins, white. |
| Width | Up to maxWidth, 642px by default. |
| Arrow | Same colour as the box. |
| Gap | 6px from the child, plus MUI's 4px margin. |
| Motion | Fades in and out over 180ms. |
The colours are hard-coded in Tooltip.jsx rather than read from the tokens. They match the tokens named above.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
title | ReactNode | Required for the tooltip to show. An empty value shows nothing. | |
children | one element | Must accept a ref and pass on event props. | |
placement | "top" | "bottom" | "left" | "right", plus -start and -end forms | "bottom" | |
arrow | boolean | false | |
describeChild | boolean | false | Keep the child's own name. See above. |
displayTooltipOnOverflow | boolean | false | Only open when the child is truncated. |
maxWidth | string | "642px" | |
tooltipActive | boolean | true | false renders the child with no tooltip at all. |
disableHover | boolean | false | Hover does nothing. Focus and touch still open it. |
open | boolean | Controls the open state from outside. | |
disablePortal | boolean | false | Render inside the parent instead of at the end of <body>. Rarely needed. |
styles | { tooltip?, arrow? } | {} | Style overrides for the box and the arrow. Avoid; the look should match everywhere. |
triggerResizeProps | array | [] | Accepted but not used for anything except the memo comparison. |
Any other prop (enterDelay, leaveDelay, onOpen, onClose, followCursor and so on) is passed to MUI's Tooltip.
The component is wrapped in memo with a custom comparison that only looks at children, title, placement, open, tooltipActive, displayTooltipOnOverflow, disableHover, disablePortal, maxWidth, styles and triggerResizeProps. Changing any other prop on its own, such as arrow or describeChild, may not re-render the tooltip.
Accessibility
- The tooltip opens on mouse hover after 100ms, on keyboard focus, and on a long press on touch screens. It closes when the pointer or focus leaves, and on Escape.
- The tooltip itself has
role="tooltip". - Do not put anything people need to read only in a tooltip. Touch users rarely see it. Put essential information on the page.
- Use
describeChildwhenever the child has visible text or a label of its own. See the table above. - Make the child focusable. A tooltip on a plain
<span>or icon needstabIndex={0}and anaria-label, or keyboard users never see it.
Do and don't
- Do keep tooltip text short: a name, or one sentence.
- Do use a tooltip to say why a control is disabled.
- Do use
displayTooltipOnOverflowfor names that may be cut short in tables and cards. - Don't put links, buttons or form fields inside a tooltip. It closes as the pointer moves towards them.
- Don't repeat the visible label in the tooltip. "Refresh data" does not need a tooltip saying "Refresh data".
- Don't restyle it with
styles. Every tooltip in the product should look the same.