Skip to content
Tooltip

A tooltip is a floating element that displays information related to an anchor element when it receives keyboard focus or the mouse hovers over it.

Essentials

An accessible tooltip component has the following:

  • Dynamic anchor positioning: The tooltip will remain attached to its reference element and remain in view for the user regardless of where it is positioned on the screen.
  • Events: When the mouse hovers over or focuses the reference element, the tooltip opens. When it leaves/blurs, it closes.
  • Dismissal: When the user presses the esc key while the tooltip is open, it closes.
  • Role: The elements are given relevant role and ARIA attributes to be accessible to screen readers.

Examples

Both of these examples have sections explaining them in-depth below.

Basic tooltip

CodeSandbox demo

This example demonstrates how to create a tooltip for use in a single instance to familiarize yourself with the fundamentals.

Let’s walk through the example:

Open state

import {useState} from 'react';
 
function Tooltip() {
  const [isOpen, setIsOpen] = useState(false);
}
import {useState} from 'react';
 
function Tooltip() {
  const [isOpen, setIsOpen] = useState(false);
}

isOpenisOpen determines whether or not the tooltip is currently open on the screen. It is used for conditional rendering.

useFloating hook

The useFloating()useFloating() hook provides positioning and context for our tooltip. We need to pass it some information:

  • openopen: The open state from our useState()useState() hook above.
  • onOpenChangeonOpenChange: A callback function that will be called when the tooltip is opened or closed. We’ll use this to update our isOpenisOpen state.
  • middlewaremiddleware: Import and pass middleware to the array that ensure the tooltip remains on the screen, no matter where it ends up being positioned.
  • whileElementsMountedwhileElementsMounted: Ensure the tooltip remains anchored to the reference element by updating the position when necessary, only while both the reference and floating elements are mounted for performance.
import {
  useFloating,
  autoUpdate,
  offset,
  flip,
  shift,
} from '@floating-ui/react';
 
function Tooltip() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {x, y, strategy, refs, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    middleware: [offset(10), flip(), shift()],
    whileElementsMounted: autoUpdate,
  });
}
import {
  useFloating,
  autoUpdate,
  offset,
  flip,
  shift,
} from '@floating-ui/react';
 
function Tooltip() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {x, y, strategy, refs, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    middleware: [offset(10), flip(), shift()],
    whileElementsMounted: autoUpdate,
  });
}

Interaction hooks

Interaction hooks return objects containing keys of props that enable the tooltip to be opened, closed, or accessible to screen readers.

Using the contextcontext that was returned from the hook, call the interaction hooks:

import {
  // ...
  useHover,
  useFocus,
  useDismiss,
  useRole,
  useInteractions,
} from '@floating-ui/react';
 
function Tooltip() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {x, y, strategy, refs, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    middleware: [offset(10), flip(), shift()],
    whileElementsMounted: autoUpdate,
  });
 
  const hover = useHover(context, {move: false});
  const focus = useFocus(context);
  const dismiss = useDismiss(context);
  const role = useRole(context, {role: 'tooltip'});
 
  // Merge all the interactions into prop getters
  const {getReferenceProps, getFloatingProps} = useInteractions([
    hover,
    focus,
    dismiss,
    role,
  ]);
}
import {
  // ...
  useHover,
  useFocus,
  useDismiss,
  useRole,
  useInteractions,
} from '@floating-ui/react';
 
function Tooltip() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {x, y, strategy, refs, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    middleware: [offset(10), flip(), shift()],
    whileElementsMounted: autoUpdate,
  });
 
  const hover = useHover(context, {move: false});
  const focus = useFocus(context);
  const dismiss = useDismiss(context);
  const role = useRole(context, {role: 'tooltip'});
 
  // Merge all the interactions into prop getters
  const {getReferenceProps, getFloatingProps} = useInteractions([
    hover,
    focus,
    dismiss,
    role,
  ]);
}
  • useHover()useHover() adds the ability to toggle the tooltip open or closed when the reference element is hovered over. The movemove option is set to false so that mousemovemousemove events are ignored.
  • useFocus()useFocus() adds the ability to toggle the tooltip open or closed when the reference element is focused.
  • useDismiss()useDismiss() adds the ability to dismiss the tooltip when the user presses the esc key.
  • useRole()useRole() adds the correct ARIA attributes for a tooltiptooltip to the tooltip and reference elements.

Finally, useInteractions()useInteractions() merges all of their props into prop getters which can be used for rendering.

Rendering

Now we have all the variables and hooks set up, we can render out our elements.

function Tooltip() {
  // ...
  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Reference element
      </button>
      {isOpen && (
        <div
          ref={refs.setFloating}
          style={{
            position: strategy,
            top: y ?? 0,
            left: x ?? 0,
            width: 'max-content',
          }}
          {...getFloatingProps()}
        >
          Tooltip element
        </div>
      )}
    </>
  );
}
function Tooltip() {
  // ...
  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Reference element
      </button>
      {isOpen && (
        <div
          ref={refs.setFloating}
          style={{
            position: strategy,
            top: y ?? 0,
            left: x ?? 0,
            width: 'max-content',
          }}
          {...getFloatingProps()}
        >
          Tooltip element
        </div>
      )}
    </>
  );
}
  • {...getReferenceProps()}{...getReferenceProps()} / {...getFloatingProps()}{...getFloatingProps()} spreads the props from the interaction hooks onto the relevant elements. They contain props like onMouseEnteronMouseEnter, aria-describedbyaria-describedby, etc.

Reusable tooltip component

CodeSandbox demo

It is better to create a reusable component API that can be used in a variety of different scenarios more easily. We can place all of our hooks into a single custom hook for better reusability, which is then used by a controller component which encapsulates the state.

The reusable component can:

  • Be uncontrolled or controlled
  • Accept any element as the <TooltipTrigger /><TooltipTrigger />
  • Read the open state to change styles
function App() {
  return (
    <Tooltip>
      <TooltipTrigger>My trigger</TooltipTrigger>
      <TooltipContent>My tooltip</TooltipContent>
    </Tooltip>
  );
}
function App() {
  return (
    <Tooltip>
      <TooltipTrigger>My trigger</TooltipTrigger>
      <TooltipContent>My tooltip</TooltipContent>
    </Tooltip>
  );
}

Controller component

  • <Tooltip /><Tooltip />

This is the controller component that manages the tooltip’s state and provides the API to the rest of the components.

Render components

These components read the context provided by the root Tooltip component and render the appropriate elements.

The components must be wrapped in forwardRef()forwardRef() to allow refs, and should merge the refs to ensure all refs are preserved and forwarded to the element. Props are also merged to prevent overwriting.

  • <TooltipTrigger /><TooltipTrigger /> is the trigger button the tooltip is attached to. This accepts an asChildasChild prop if you want to attach it to a custom element. It also has a data-statedata-state attached to style based on the open/closed state.
  • <TooltipContent /><TooltipContent /> is the tooltip element, which can contain any children (React nodes).

Delay groups

One of the most useful UX improvements for tooltips is making nearby tooltips share a delay.

<FloatingDelayGroup delay={200}>
  <Tooltip>
    <TooltipTrigger>Ref 1</TooltipTrigger>
    <TooltipContent className="Tooltip">Label 1</TooltipContent>
  </Tooltip>
  <Tooltip>
    <TooltipTrigger>Ref 2</TooltipTrigger>
    <TooltipContent className="Tooltip">Label 2</TooltipContent>
  </Tooltip>
  <Tooltip>
    <TooltipTrigger>Ref 3</TooltipTrigger>
    <TooltipContent className="Tooltip">Label 3</TooltipContent>
  </Tooltip>
</FloatingDelayGroup>
<FloatingDelayGroup delay={200}>
  <Tooltip>
    <TooltipTrigger>Ref 1</TooltipTrigger>
    <TooltipContent className="Tooltip">Label 1</TooltipContent>
  </Tooltip>
  <Tooltip>
    <TooltipTrigger>Ref 2</TooltipTrigger>
    <TooltipContent className="Tooltip">Label 2</TooltipContent>
  </Tooltip>
  <Tooltip>
    <TooltipTrigger>Ref 3</TooltipTrigger>
    <TooltipContent className="Tooltip">Label 3</TooltipContent>
  </Tooltip>
</FloatingDelayGroup>

Disabled buttons

Sometimes you want to disable a button, but still show the tooltip while it’s disabled.

Disabling a button with a tooltip prevents it from being accessible, but can be worked around using a different prop. This supplants the disableddisabled prop to allow events to fire, including keyboard access.

const Button = React.forwardRef(function Button(
  {visuallyDisabled, disabled, ...props},
  ref
) {
  return (
    <button
      {...props}
      ref={ref}
      disabled={visuallyDisabled ? undefined : disabled}
      aria-disabled={visuallyDisabled ? 'true' : undefined}
      // You'll want to do this for all relevant event handlers.
      onClick={visuallyDisabled ? undefined : props.onClick}
    />
  );
});
const Button = React.forwardRef(function Button(
  {visuallyDisabled, disabled, ...props},
  ref
) {
  return (
    <button
      {...props}
      ref={ref}
      disabled={visuallyDisabled ? undefined : disabled}
      aria-disabled={visuallyDisabled ? 'true' : undefined}
      // You'll want to do this for all relevant event handlers.
      onClick={visuallyDisabled ? undefined : props.onClick}
    />
  );
});