Tooltip

A short description of one child, shown on hover, keyboard focus and a long touch - a role="tooltip" in a Portal, placed beside its child and flipped for want of room.

Demos

For usage examples, visit the component demo page: Tooltip

Import

import { Tooltip } from '@soroush.tech/design-system/Tooltip'

Props

PropTypeDefaultDescription
childrenReactElement?-The one element it is about. It holds a ref and takes the handlers. None with anchorPosition.
anchorPosition{ top, left }-A point in the window (client coordinates, as Popover's) to place it at, not beside the child - with no child, isOpen opens it.
titleReactNode-What it says. Nothing says no tooltip.
placement'top' | 'bottom' | 'left' | 'right' | 'auto', each with -start or -end'top'The side of the child, and the end it lines up with; auto takes the side with the most room. Flips for want of room.
slots{ root?, arrow? }-The element a part is drawn as - the surface or the arrow. It keeps its styles.
slotProps{ root?, arrow? }-Props for a part, put first: the tooltip's own ref, id, role, pointer handlers and the arrow's position win.
hasArrowbooleanfalseAn arrow towards the middle of the child.
enterDelay / leaveDelaynumber100 / 0In milliseconds: before it opens, and after the pointer or focus has left.
enterTouchDelay / leaveTouchDelaynumber700 / 1500How long a touch is held to open it, and how long after it ends it closes.
shouldDescribeChildbooleanfalseDescribes the child (aria-describedby, while open) instead of naming it (aria-label).
isDescriptionPersistentbooleanfalseWith shouldDescribeChild: describes the child all the time, by a hidden copy of the title, as a native title does.
isOpen / defaultOpenbooleanfalseControlled with onOpen and onClose, or at first.
onOpen / onClose(event) => void-Fired when it asks to open or close - once each, however often it is asked.
isInteractivebooleantrueWhether the pointer can move onto the tooltip, which then stays open. With false it takes no pointer events - a click goes to what is under it.
shouldWrapDisabledChildbooleantrueWhether a disabled child (disabled) is wrapped for the pointer handlers and the placement. Leave it out for a child only marked disabled that still takes the pointer - a menu row.
shouldOpenOnHover / shouldOpenOnFocus / shouldOpenOnTouchbooleantrueWhat opens it. Focus is keyboard focus: a press with the pointer is not.
layerkeyof Theme['zOrder']'tooltip'Stacking layer.
borderRadiuskeyof Theme['radii']theme.defaults.surfaceRadius, else squareCorner radius.
bgkeyof Theme['background']'modal'Background.
textColorkeyof Theme['text']theme.defaults.textColor, else 'initial'Text color.
className, data-testidOn the tooltip.

Naming and describing

By default a text title names its child (aria-label) - for an icon with no text of its own - unless the child already has a name. With shouldDescribeChild it describes it instead: the child gets aria-describedby for as long as the tooltip is open, added to a description it already has.

With isDescriptionPersistent as well, the description holds while the tooltip is closed, as that of a native title does: a visually hidden copy of the title is rendered beside the child, outside the Portal, and the child points at it the whole time (the open tooltip is then not read a second time). It is in the server-rendered HTML. It is one more element beside the child, which a parent that counts its children will see.

What a parent hands it

A parent that clones its child - a Menu with a Tooltip for its trigger - hands the clone to the Tooltip. It goes on to the child: the ref, the id, aria-*, handlers and anything else the tooltip does not take itself. The child's own handler and the one it was handed both run, the child's first, and a description it was handed is added to its own and the tooltip's; anything else replaces what the child had, as a clone would. So <Menu trigger={<Tooltip title="More"><IconButton /></Tooltip>}> opens, names and places the menu as a bare button would. A child hidden from assistive technology (aria-hidden) is not named by the title.

At a point

With anchorPosition the tooltip is placed at a point in the window, not beside a child - for a hint that depends on where the pointer is, such as a canvas. It needs no child, and is open when isOpen says so; with a title that changes as the point does, it follows both. It keeps its gap from the point, and flips to the other side for want of room, as it does around a child. With a child and a point, the point wins, and neither the child's hover, focus and touch nor the pointer on the tooltip open or close it (an open asked for just before is dropped): isOpen does. Escape still asks it to close, through onClose. Nothing describes or names a child it does not have.

A tooltip that is not interactive

By default the pointer can move onto the tooltip and it stays open, so its text can be read and selected (WCAG 1.4.13), and a click on it is a click on the tooltip. For a text hint that holds nothing to use, set isInteractive={false}: the surface takes no pointer events, so a click goes to what is under it, and the tooltip closes as the pointer leaves the child.

A press of the child closes the tooltip as a native title goes away: a mouse or pen press, not a touch, which is left to its long press. A press anywhere else cannot let a click through - the click goes to what was under the pointer when it went down, which is the tooltip - so only isInteractive={false} does.

A group of tooltips

Around a toolbar or a rail, a tooltip that waits its enterDelay for every icon is slow. Wrap them in TooltipGroup and the next one opens at once while another is open, or within skipDelayDuration (milliseconds, 300 by default) of the last one closing; after that the tooltip's own enterDelay applies again. A tooltip that opens closes the one still showing, so two are never up together.

<TooltipGroup skipDelayDuration={300}>
  <Tooltip title="Layers">
    <IconButton aria-label="Layers" />
  </Tooltip>
  <Tooltip title="Assets">
    <IconButton aria-label="Assets" />
  </Tooltip>
</TooltipGroup>

It renders nothing of its own and takes hover and keyboard focus; a touch keeps enterTouchDelay. The nearest group wins. Tooltips outside a group are unchanged, and a tooltip whose isOpen the app controls is left alone: the group neither opens nor closes it, and it does not start the window.

To group every tooltip of an app at once, with no wrapper, set skipDelayDuration in theme.components.Tooltip.defaultProps (milliseconds; unset or 0 is off). Every tooltip under that theme then shares one window by the same rule, and a TooltipGroup around a tooltip wins over it.

A disabled child

A disabled control sends no pointer events of its own (React holds them back), so a Tooltip around one would never open. When the child has disabled, the tooltip wraps it in an inline-grid span and listens, and is placed, on that. The control keeps its own ref; nothing else changes, and a child that is not disabled is not wrapped. A disabled control cannot take the focus, so it opens on hover and touch.

Customization

theme.components.Tooltip styles the root (the surface) and the arrow. Its defaultProps set placement, hasArrow, layer, borderRadius, bg, textColor, enterDelay, leaveDelay, enterTouchDelay, leaveTouchDelay, shouldDescribeChild, isDescriptionPersistent, shouldWrapDisabledChild, isInteractive, skipDelayDuration, shouldOpenOnHover, shouldOpenOnFocus and shouldOpenOnTouch - not children, title, anchorPosition, isOpen or the handlers. The arrow takes the surface's background. The tooltip key of theme.zOrder is its layer.

Server rendering

The tooltip is drawn in a Portal, which renders nothing on the server: it is absent from server-rendered and prerendered HTML, and mounts on the client when it opens (on a click, hover, focus or key, or when the app sets it open). One that starts open, by its open prop, is still closed in the static HTML and opens on hydration. Its trigger renders on the server as usual.

Source code

If this page does not answer your question, have a look at the implementation of the component for more detail.