Tooltip
A tooltip displays a description of an element on hover or focus.
install | yarn add react-aria-components |
---|---|
version | 3.17.0 |
usage | import {Tooltip} from 'react-aria-components' |
Example#
import {Button, OverlayArrow, Tooltip, TooltipTrigger} from 'react-aria-components';
<TooltipTrigger>
<Button>✏️</Button>
<Tooltip>
<OverlayArrow>
<svg width={8} height={8}>
<path d="M0 0,L4 4,L8 0" />
</svg>
</OverlayArrow>
Edit
</Tooltip>
</TooltipTrigger>
import {
Button,
OverlayArrow,
Tooltip,
TooltipTrigger
} from 'react-aria-components';
<TooltipTrigger>
<Button>✏️</Button>
<Tooltip>
<OverlayArrow>
<svg width={8} height={8}>
<path d="M0 0,L4 4,L8 0" />
</svg>
</OverlayArrow>
Edit
</Tooltip>
</TooltipTrigger>
import {
Button,
OverlayArrow,
Tooltip,
TooltipTrigger
} from 'react-aria-components';
<TooltipTrigger>
<Button>✏️</Button>
<Tooltip>
<OverlayArrow>
<svg
width={8}
height={8}
>
<path d="M0 0,L4 4,L8 0" />
</svg>
</OverlayArrow>
Edit
</Tooltip>
</TooltipTrigger>
Show CSS
.react-aria-Tooltip {
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
border-radius: 4px;
background: slateblue;
color: white;
outline: none;
padding: 2px 8px;
max-width: 150px;
&[data-placement=top] {
margin-bottom: 8px;
--origin: translateY(4px);
}
&[data-placement=bottom] {
margin-top: 8px;
--origin: translateY(-4px);
& .react-aria-OverlayArrow svg {
transform: rotate(180deg);
}
}
&[data-placement=right] {
margin-left: 8px;
--origin: translateX(-4px);
& .react-aria-OverlayArrow svg {
transform: rotate(90deg);
}
}
&[data-placement=left] {
margin-right: 8px;
--origin: translateX(4px);
& .react-aria-OverlayArrow svg {
transform: rotate(-90deg);
}
}
& .react-aria-OverlayArrow svg {
display: block;
fill: slateblue;
}
&[data-entering] {
animation: slide 200ms;
}
&[data-exiting] {
animation: slide 200ms reverse ease-in;
}
}
@keyframes slide {
from {
transform: var(--origin);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.react-aria-Button {
background: var(--spectrum-global-color-gray-50);
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px;
appearance: none;
vertical-align: middle;
font-size: 1.2rem;
text-align: center;
margin: 0;
outline: none;
padding: 6px;
transition: border-color 200ms;
&[data-hovered] {
border-color: var(--spectrum-global-color-gray-500);
}
&[data-pressed] {
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
background: var(--spectrum-global-color-gray-100);
border-color: var(--spectrum-global-color-gray-600);
}
&[data-focus-visible] {
border-color: slateblue;
box-shadow: 0 0 0 1px slateblue;
}
}
.react-aria-Tooltip {
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
border-radius: 4px;
background: slateblue;
color: white;
outline: none;
padding: 2px 8px;
max-width: 150px;
&[data-placement=top] {
margin-bottom: 8px;
--origin: translateY(4px);
}
&[data-placement=bottom] {
margin-top: 8px;
--origin: translateY(-4px);
& .react-aria-OverlayArrow svg {
transform: rotate(180deg);
}
}
&[data-placement=right] {
margin-left: 8px;
--origin: translateX(-4px);
& .react-aria-OverlayArrow svg {
transform: rotate(90deg);
}
}
&[data-placement=left] {
margin-right: 8px;
--origin: translateX(4px);
& .react-aria-OverlayArrow svg {
transform: rotate(-90deg);
}
}
& .react-aria-OverlayArrow svg {
display: block;
fill: slateblue;
}
&[data-entering] {
animation: slide 200ms;
}
&[data-exiting] {
animation: slide 200ms reverse ease-in;
}
}
@keyframes slide {
from {
transform: var(--origin);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.react-aria-Button {
background: var(--spectrum-global-color-gray-50);
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px;
appearance: none;
vertical-align: middle;
font-size: 1.2rem;
text-align: center;
margin: 0;
outline: none;
padding: 6px;
transition: border-color 200ms;
&[data-hovered] {
border-color: var(--spectrum-global-color-gray-500);
}
&[data-pressed] {
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
background: var(--spectrum-global-color-gray-100);
border-color: var(--spectrum-global-color-gray-600);
}
&[data-focus-visible] {
border-color: slateblue;
box-shadow: 0 0 0 1px slateblue;
}
}
.react-aria-Tooltip {
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
border-radius: 4px;
background: slateblue;
color: white;
outline: none;
padding: 2px 8px;
max-width: 150px;
&[data-placement=top] {
margin-bottom: 8px;
--origin: translateY(4px);
}
&[data-placement=bottom] {
margin-top: 8px;
--origin: translateY(-4px);
& .react-aria-OverlayArrow svg {
transform: rotate(180deg);
}
}
&[data-placement=right] {
margin-left: 8px;
--origin: translateX(-4px);
& .react-aria-OverlayArrow svg {
transform: rotate(90deg);
}
}
&[data-placement=left] {
margin-right: 8px;
--origin: translateX(4px);
& .react-aria-OverlayArrow svg {
transform: rotate(-90deg);
}
}
& .react-aria-OverlayArrow svg {
display: block;
fill: slateblue;
}
&[data-entering] {
animation: slide 200ms;
}
&[data-exiting] {
animation: slide 200ms reverse ease-in;
}
}
@keyframes slide {
from {
transform: var(--origin);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.react-aria-Button {
background: var(--spectrum-global-color-gray-50);
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px;
appearance: none;
vertical-align: middle;
font-size: 1.2rem;
text-align: center;
margin: 0;
outline: none;
padding: 6px;
transition: border-color 200ms;
&[data-hovered] {
border-color: var(--spectrum-global-color-gray-500);
}
&[data-pressed] {
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
background: var(--spectrum-global-color-gray-100);
border-color: var(--spectrum-global-color-gray-600);
}
&[data-focus-visible] {
border-color: slateblue;
box-shadow: 0 0 0 1px slateblue;
}
}
Features#
The HTML title
attribute can be used to create a tooltip, but it cannot be styled. Custom styled
tooltips can be hard to build in an accessible way. TooltipTrigger
and Tooltip
help build
fully accessible tooltips that can be styled as needed.
- Styleable – States for entry and exit animations are included for easy styling, and an optional arrow element can be rendered.
- Accessible – The trigger element is automatically associated with the tooltip via
aria-describedby
. Tooltips are displayed when an element receives focus. - Hover behavior – Tooltips display after a global delay on hover of the first tooltip, with no delay on subsequent tooltips to avoid unintended flickering. Emulated hover events on touch devices are ignored.
- Positioning – The tooltip is positioned relative to the trigger element, and automatically flips and adjusts to avoid overlapping with the edge of the browser window.
Anatomy#
A tooltip consists of two parts: the trigger element and the tooltip itself. Users may reveal the tooltip by hovering or focusing the trigger.
Accessibility#
Tooltip triggers must be focusable and hoverable in order to ensure that all users can activate them. When displayed, TooltipTrigger automatically associates the tooltip with the trigger element so that it is described by the tooltip content.
Note: tooltips are not shown on touch screen interactions. Ensure that your UI is usable without tooltips, or use an alternative component such as a Popover to show information in an adjacent element.
Props#
TooltipTrigger#
Name | Type | Default | Description |
children | ReactNode | — | |
isDisabled | boolean | — | Whether the tooltip should be disabled, independent from the trigger. |
delay | number | 1500 | The delay time for the tooltip to show up. See guidelines. |
trigger | 'focus' | — | By default, opens for both focus and hover. Can be made to open only for focus. |
isOpen | boolean | — | Whether the overlay is open by default (controlled). |
defaultOpen | boolean | — | Whether the overlay is open by default (uncontrolled). |
Events
Name | Type | Default | Description |
onOpenChange | (
(isOpen: boolean
)) => void | — | Handler that is called when the overlay's open state changes. |
Tooltip#
Name | Type | Default | Description |
placement | Placement | 'bottom' | The placement of the element with respect to its anchor element. |
containerPadding | number | 12 | The placement padding that should be applied between the element and its surrounding container. |
offset | number | 0 | The additional offset applied along the main axis between the element and its anchor element. |
crossOffset | number | 0 | The additional offset applied along the cross axis between the element and its anchor element. |
shouldFlip | boolean | true | Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely. |
isOpen | boolean | — | Whether the element is rendered. |
children | ReactNode | (
(values: TooltipRenderProps
)) => ReactNode | — | |
className | string | (
(values: TooltipRenderProps
)) => string | — | |
style | CSSProperties | (
(values: TooltipRenderProps
)) => CSSProperties | — |
Accessibility
Name | Type | Default | Description |
aria-label | string | — | Defines a string value that labels the current element. |
aria-labelledby | string | — | Identifies the element (or elements) that labels the current element. |
aria-describedby | string | — | Identifies the element (or elements) that describes the object. |
aria-details | string | — | Identifies the element (or elements) that provide a detailed, extended description for the object. |
OverlayArrow#
OverlayArrow
accepts all HTML attributes.
Styling#
React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className
attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName
naming convention.
.react-aria-Tooltip {
/* ... */
}
.react-aria-Tooltip {
/* ... */
}
.react-aria-Tooltip {
/* ... */
}
A custom className
can also be specified on any component. This overrides the default className
provided by React Aria with your own.
<Tooltip className="my-tooltip">
{/* ... */}
</Tooltip>
<Tooltip className="my-tooltip">
{/* ... */}
</Tooltip>
<Tooltip className="my-tooltip">
{/* ... */}
</Tooltip>;
In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using DOM attributes, which you can target in CSS selectors. These are ARIA attributes wherever possible, or data attributes when a relevant ARIA attribute does not exist. For example:
.react-aria-Tooltip[data-placement=left] {
/* ... */
}
.react-aria-Tooltip[data-placement=left] {
/* ... */
}
.react-aria-Tooltip[data-placement=left] {
/* ... */
}
The className
and style
props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.
<OverlayArrow
className={({ placement }) =>
placement === 'left' || placement === 'right' ? 'rotate-90' : 'rotate-0'}
>
{/* ... */}
</OverlayArrow>;
<OverlayArrow
className={({ placement }) =>
placement === 'left' || placement === 'right'
? 'rotate-90'
: 'rotate-0'}
>
{/* ... */}
</OverlayArrow>;
<OverlayArrow
className={(
{ placement }
) =>
placement ===
'left' ||
placement ===
'right'
? 'rotate-90'
: 'rotate-0'}
>
{/* ... */}
</OverlayArrow>;
Tooltips also support entry and exit animations via states exposed as data attributes and render props. Tooltip
will automatically wait for any exit animations to complete before it is removed from the DOM.
.react-aria-Tooltip[data-entering] {
animation: slide 300ms;
}
.react-aria-Tooltip[data-exiting] {
animation: slide 300ms reverse;
}
@keyframes slide {
/* ... */
}
.react-aria-Tooltip[data-entering] {
animation: slide 300ms;
}
.react-aria-Tooltip[data-exiting] {
animation: slide 300ms reverse;
}
@keyframes slide {
/* ... */
}
.react-aria-Tooltip[data-entering] {
animation: slide 300ms;
}
.react-aria-Tooltip[data-exiting] {
animation: slide 300ms reverse;
}
@keyframes slide {
/* ... */
}
The states, selectors, and render props for each component used in a TooltipTrigger
are documented below.
TooltipTrigger#
The TooltipTrigger
component does not render any DOM elements (it only passes through its children) so it does not support styling. If you need a wrapper element, add one yourself inside the <TooltipTrigger>
.
<TooltipTrigger>
<div className="my-tooltip-trigger">
{/* ... */}
</div>
</TooltipTrigger>
<TooltipTrigger>
<div className="my-tooltip-trigger">
{/* ... */}
</div>
</TooltipTrigger>
<TooltipTrigger>
<div className="my-tooltip-trigger">
{/* ... */}
</div>
</TooltipTrigger>;
Tooltip#
A Tooltip
can be targeted with the .react-aria-Tooltip
CSS selector, or by overriding with a custom className
. It supports the following states and render props:
Name | CSS Selector | Description |
placement | [data-placement="left | right | top | bottom"] | The placement of the tooltip relative to the trigger. |
isEntering | [data-entering] | Whether the tooltip is currently entering. Use this to apply animations. |
isExiting | [data-exiting] | Whether the tooltip is currently exiting. Use this to apply animations. |
OverlayArrow#
A OverlayArrow
can be targeted with the .react-aria-OverlayArrow
CSS selector, or by overriding with a custom className
. It supports the following states and render props:
Name | CSS Selector | Description |
placement | [data-placement="left | right | top | bottom"] | The placement of the overlay relative to the trigger. |
Reusable wrappers#
If you will use a Tooltip in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency.
This example wraps Tooltip
and all of its children together into a single component.
function MyTooltip({children, ...props}) {
return (
<Tooltip {...props}>
<OverlayArrow>
<svg width={8} height={8}><path d="M0 0,L4 4,L8 0" /></svg>
</OverlayArrow>
{children}
</Tooltip>
);
}
<TooltipTrigger>
<Button>💾</Button>
<MyTooltip>Save</MyTooltip>
</TooltipTrigger>
function MyTooltip({ children, ...props }) {
return (
<Tooltip {...props}>
<OverlayArrow>
<svg width={8} height={8}>
<path d="M0 0,L4 4,L8 0" />
</svg>
</OverlayArrow>
{children}
</Tooltip>
);
}
<TooltipTrigger>
<Button>💾</Button>
<MyTooltip>Save</MyTooltip>
</TooltipTrigger>
function MyTooltip(
{ children, ...props }
) {
return (
<Tooltip {...props}>
<OverlayArrow>
<svg
width={8}
height={8}
>
<path d="M0 0,L4 4,L8 0" />
</svg>
</OverlayArrow>
{children}
</Tooltip>
);
}
<TooltipTrigger>
<Button>💾</Button>
<MyTooltip>
Save
</MyTooltip>
</TooltipTrigger>
Usage#
The following examples show how to use the MyTooltip
component created in the above example.
Delay#
Tooltips appear after a short delay when hovering the trigger, or instantly when using keyboard focus. This delay is called the "warmup period", and it is a global timer, shared by all tooltips. Once a tooltip is displayed, other tooltips display immediately. If the user waits for the "cooldown period" before hovering another element, the warmup timer restarts.
<div style={{display: 'flex', gap: 8}}>
<TooltipTrigger>
<Button>Hover me</Button>
<MyTooltip>I come up after a delay.</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>Then hover me</Button>
<MyTooltip>If you did it quickly, I appear immediately.</MyTooltip>
</TooltipTrigger>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<TooltipTrigger>
<Button>Hover me</Button>
<MyTooltip>I come up after a delay.</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>Then hover me</Button>
<MyTooltip>
If you did it quickly, I appear immediately.
</MyTooltip>
</TooltipTrigger>
</div>
<div
style={{
display: 'flex',
gap: 8
}}
>
<TooltipTrigger>
<Button>
Hover me
</Button>
<MyTooltip>
I come up after a
delay.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>
Then hover me
</Button>
<MyTooltip>
If you did it
quickly, I appear
immediately.
</MyTooltip>
</TooltipTrigger>
</div>
The delay can be adjusted for hover using the delay
prop.
<TooltipTrigger delay={0}>
<Button>💾</Button>
<MyTooltip>Save</MyTooltip>
</TooltipTrigger>
<TooltipTrigger delay={0}>
<Button>💾</Button>
<MyTooltip>Save</MyTooltip>
</TooltipTrigger>
<TooltipTrigger
delay={0}
>
<Button>💾</Button>
<MyTooltip>
Save
</MyTooltip>
</TooltipTrigger>
Trigger#
By default, tooltips appear both on hover and on focus. The trigger
prop can be set to "focus"
to display the tooltip only on focus, and not on hover.
<TooltipTrigger trigger="focus">
<Button>💿</Button>
<MyTooltip>Burn CD</MyTooltip>
</TooltipTrigger>
<TooltipTrigger trigger="focus">
<Button>💿</Button>
<MyTooltip>Burn CD</MyTooltip>
</TooltipTrigger>
<TooltipTrigger trigger="focus">
<Button>💿</Button>
<MyTooltip>
Burn CD
</MyTooltip>
</TooltipTrigger>
Controlled open state#
The open state of the tooltip can be controlled via the defaultOpen
and isOpen
props.
function Example() {
let [isOpen, setOpen] = React.useState(false);
return (
<>
<TooltipTrigger isOpen={isOpen} onOpenChange={setOpen}>
<Button>📣</Button>
<MyTooltip>Notifications</MyTooltip>
</TooltipTrigger>
<p>Tooltip is {isOpen ? 'showing' : 'not showing'}</p>
</>
);
}
function Example() {
let [isOpen, setOpen] = React.useState(false);
return (
<>
<TooltipTrigger
isOpen={isOpen}
onOpenChange={setOpen}
>
<Button>📣</Button>
<MyTooltip>Notifications</MyTooltip>
</TooltipTrigger>
<p>Tooltip is {isOpen ? 'showing' : 'not showing'}</p>
</>
);
}
function Example() {
let [isOpen, setOpen] =
React.useState(
false
);
return (
<>
<TooltipTrigger
isOpen={isOpen}
onOpenChange={setOpen}
>
<Button>
📣
</Button>
<MyTooltip>
Notifications
</MyTooltip>
</TooltipTrigger>
<p>
Tooltip is{' '}
{isOpen
? 'showing'
: 'not showing'}
</p>
</>
);
}
Placement#
The Tooltip's placement with respect to its trigger element can be adjusted using the placement
prop. See the props table for a full list of available placement combinations.
<div style={{ display: 'flex', gap: 8 }}>
<TooltipTrigger>
<Button>⬅️</Button>
<MyTooltip placement="start">
In left-to-right, this is on the left. In right-to-left, this is on the
right.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>⬆️</Button>
<MyTooltip placement="top">This tooltip is above the button.</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>⬇️</Button>
<MyTooltip placement="bottom">
This tooltip is below the button.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>➡️</Button>
<MyTooltip placement="end">
In left-to-right, this is on the right. In right-to-left, this is on the
left.
</MyTooltip>
</TooltipTrigger>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<TooltipTrigger>
<Button>⬅️</Button>
<MyTooltip placement="start">
In left-to-right, this is on the left. In
right-to-left, this is on the right.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>⬆️</Button>
<MyTooltip placement="top">
This tooltip is above the button.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>⬇️</Button>
<MyTooltip placement="bottom">
This tooltip is below the button.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>➡️</Button>
<MyTooltip placement="end">
In left-to-right, this is on the right. In
right-to-left, this is on the left.
</MyTooltip>
</TooltipTrigger>
</div>
<div
style={{
display: 'flex',
gap: 8
}}
>
<TooltipTrigger>
<Button>⬅️</Button>
<MyTooltip placement="start">
In left-to-right,
this is on the
left. In
right-to-left,
this is on the
right.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>⬆️</Button>
<MyTooltip placement="top">
This tooltip is
above the button.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>⬇️</Button>
<MyTooltip placement="bottom">
This tooltip is
below the button.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>➡️</Button>
<MyTooltip placement="end">
In left-to-right,
this is on the
right. In
right-to-left,
this is on the
left.
</MyTooltip>
</TooltipTrigger>
</div>
Offset and cross offset#
The Tooltip's offset with respect to its trigger can be adjusted using the offset
and
crossOffset
props. The offset
prop controls the spacing applied along the main axis between the element and its
trigger whereas the crossOffset
prop handles the spacing applied along the cross axis.
Below is a tooltip offset by an additional 50px above the trigger.
<TooltipTrigger>
<Button>☝️</Button>
<MyTooltip offset={50}>This will shift up.</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>☝️</Button>
<MyTooltip offset={50}>This will shift up.</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>☝️</Button>
<MyTooltip
offset={50}
>
This will shift up.
</MyTooltip>
</TooltipTrigger>
Below is a tooltip cross offset by an additional 100px to the right of the trigger.
<TooltipTrigger>
<Button>👉</Button>
<MyTooltip crossOffset={60} placement="bottom">
This will shift over to the right.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>👉</Button>
<MyTooltip crossOffset={60} placement="bottom">
This will shift over to the right.
</MyTooltip>
</TooltipTrigger>
<TooltipTrigger>
<Button>👉</Button>
<MyTooltip
crossOffset={60}
placement="bottom"
>
This will shift
over to the right.
</MyTooltip>
</TooltipTrigger>
Disabled#
The isDisabled
prop can be provided to a TooltipTrigger to disable the tooltip, without disabling the trigger it displays on.
<TooltipTrigger isDisabled>
<Button>🖨</Button>
<MyTooltip>Print</MyTooltip>
</TooltipTrigger>
<TooltipTrigger isDisabled>
<Button>🖨</Button>
<MyTooltip>Print</MyTooltip>
</TooltipTrigger>
<TooltipTrigger
isDisabled
>
<Button>🖨</Button>
<MyTooltip>
Print
</MyTooltip>
</TooltipTrigger>
Advanced customization#
Hooks#
If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useTooltipTrigger for more details.