Tooltip

Tooltips are visual-only floating elements that display information about a trigger element when a user hovers or focuses it.

Give FeedbackWAI-ARIABundle Size
'use client';
import * as React from 'react';
import { Tooltip } from '@base_ui/react/Tooltip';
import { styled } from '@mui/system';

export default function UnstyledTooltipIntroduction() {
  return (
    <div style={{ display: 'flex', gap: 10 }}>
      <Tooltip.Provider closeDelay={100}>
        <Tooltip.Root>
          <AnchorButton aria-label="bold">B</AnchorButton>
          <Tooltip.Positioner sideOffset={7}>
            <TooltipPopup>
              Bold
              <TooltipArrow />
            </TooltipPopup>
          </Tooltip.Positioner>
        </Tooltip.Root>
        <Tooltip.Root>
          <AnchorButton aria-label="italic">I</AnchorButton>
          <Tooltip.Positioner sideOffset={7}>
            <TooltipPopup>
              Italic
              <TooltipArrow />
            </TooltipPopup>
          </Tooltip.Positioner>
        </Tooltip.Root>
        <Tooltip.Root>
          <AnchorButton aria-label="underline">U</AnchorButton>
          <Tooltip.Positioner sideOffset={7}>
            <TooltipPopup>
              Underline
              <TooltipArrow />
            </TooltipPopup>
          </Tooltip.Positioner>
        </Tooltip.Root>
      </Tooltip.Provider>
    </div>
  );
}

const blue = {
  400: '#3399FF',
  600: '#0072E6',
  800: '#004C99',
};

export const TooltipPopup = styled(Tooltip.Popup)`
  ${({ theme }) => `
    background: ${theme.palette.mode === 'dark' ? 'white' : 'black'};
    color: ${theme.palette.mode === 'dark' ? 'black' : 'white'};
    padding: 4px 6px;
    border-radius: 4px;
    font-size: 95%;
    cursor: default;
  `}
`;

export const AnchorButton = styled(Tooltip.Trigger)`
  border: none;
  background: ${blue[600]};
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 16px;

  &:focus-visible {
    outline: 2px solid ${blue[400]};
    outline-offset: 2px;
  }

  &:hover,
  &[data-popup-open] {
    background: ${blue[800]};
  }

  &[aria-label='bold'] {
    font-weight: bold;
  }

  &[aria-label='italic'] {
    font-style: italic;
  }

  &[aria-label='underline'] {
    text-decoration: underline;
  }
`;

export const TooltipArrow = styled(Tooltip.Arrow)`
  ${({ theme }) => `
    width: 10px;
    height: 10px;
    transform: rotate(45deg);
    background: ${theme.palette.mode === 'dark' ? 'white' : 'black'};
    z-index: -1;

    &[data-side='top'] {
      bottom: -5px;
    }
    &[data-side='right'] {
      left: -5px;
    }
    &[data-side='bottom'] {
      top: -5px;
    }
    &[data-side='left'] {
      right: -5px;
    }
  `}
`;

Introduction

Base UI components are all available as a single package.

npm install @base_ui/react

Once you have the package installed, import the component.

import { Tooltip } from '@base_ui/react/Tooltip';

Anatomy

Tooltip is implemented using a collection of related components:

<Tooltip.Provider>
  <Tooltip.Root>
    <Tooltip.Trigger />
    <Tooltip.Positioner>
      <Tooltip.Popup>
        <Tooltip.Arrow />
      </Tooltip.Popup>
    </Tooltip.Positioner>
  </Tooltip.Root>
</Tooltip.Provider>

Provider

Tooltip.Provider provides a shared delay for tooltips so that once a tooltip is shown, the rest of the tooltips in the group don't wait for the delay before showing. You can wrap this globally, or around an individual group of tooltips anywhere in your React tree (or both).

<Tooltip.Provider>
  <App />
</Tooltip.Provider>
'use client';
import * as React from 'react';
import { Tooltip } from '@base_ui/react/Tooltip';
import { styled } from '@mui/system';

export default function UnstyledTooltipDelayGroup() {
  return (
    <div style={{ display: 'flex', gap: 12 }}>
      <Tooltip.Provider delay={500} closeDelay={500}>
        <Tooltip.Root>
          <AnchorButton>Anchor A</AnchorButton>
          <Tooltip.Positioner sideOffset={5}>
            <TooltipPopup>Tooltip A</TooltipPopup>
          </Tooltip.Positioner>
        </Tooltip.Root>
        <Tooltip.Root>
          <AnchorButton>Anchor B</AnchorButton>
          <Tooltip.Positioner sideOffset={5}>
            <TooltipPopup>Tooltip B</TooltipPopup>
          </Tooltip.Positioner>
        </Tooltip.Root>
      </Tooltip.Provider>
    </div>
  );
}

const blue = {
  400: '#3399FF',
  600: '#0072E6',
  800: '#004C99',
};

export const TooltipPopup = styled(Tooltip.Popup)`
  ${({ theme }) => `
    background: ${theme.palette.mode === 'dark' ? 'white' : 'black'};
    color: ${theme.palette.mode === 'dark' ? 'black' : 'white'};
    padding: 4px 6px;
    border-radius: 4px;
    font-size: 95%;
    cursor: default;
  `}
`;

export const AnchorButton = styled(Tooltip.Trigger)`
  border: none;
  background: ${blue[600]};
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 16px;

  &:focus-visible {
    outline: 2px solid ${blue[400]};
    outline-offset: 2px;
  }

  &:hover,
  &[data-popup-open] {
    background: ${blue[800]};
  }
`;

Accessibility

Tooltips are only for sighted users with access to a pointer with hover capability or keyboard focus. This means you must supply an accessible name via aria-label to trigger elements that don't contain text content, such as an icon button.

<Tooltip.Root>
  <Tooltip.Trigger aria-label="Edit">
    <EditIcon />
  </Tooltip.Trigger>
  <Tooltip.Positioner>
    <Tooltip.Popup>Edit</Tooltip.Popup>
  </Tooltip.Positioner>
</Tooltip.Root>

Your aria-label and tooltip content should closely match or be identical so that screen reader users and sighted users receive the same information.

Tooltips should ideally also be secondary in nature, because touch users cannot see them. They are most useful as progressive enhancement in high-density desktop applications that have many icon buttons where visual labels are impractical to use.

Placement

By default, the tooltip is placed on the top side of its trigger, the default anchor. To change this, use the side prop:

<Tooltip.Root>
  <Tooltip.Trigger />
  <Tooltip.Positioner side="right">
    <Tooltip.Popup>Tooltip</Tooltip.Popup>
  </Tooltip.Positioner>
</Tooltip.Root>

You can also change the alignment of the tooltip in relation to its anchor. By default, it is centered, but it can be aligned to an edge of the anchor using the alignment prop:

<Tooltip.Positioner side="right" alignment="start">
  <Tooltip.Popup>Tooltip</Tooltip.Popup>
</Tooltip.Positioner>

Due to collision detection, the tooltip may change its placement to avoid overflow. Therefore, your explicitly specified side and alignment props act as "ideal", or preferred, values.

To access the true rendered values, which may change as the result of a collision, the content element receives data attributes:

// Rendered HTML (simplified)
<div>
  <div data-side="left" data-alignment="end">
    Tooltip
  </div>
</div>

This allows you to conditionally style the tooltip based on its rendered side or alignment.

Offset

The sideOffset prop creates a gap between the anchor and tooltip popup, while alignmentOffset slides the tooltip popup from its alignment, acting logically for start and end alignments.

<Tooltip.Positioner sideOffset={10} alignmentOffset={10}>

Delay

To change how long the tooltip waits until it opens or closes, use the delay and closeDelay props, which represent how long the tooltip waits after the cursor rests on the trigger to open, or moves away from the trigger to close, in milliseconds:

<Tooltip.Root delay={200} closeDelay={200}>

Controlled

To control the tooltip with external state, use the open and onOpenChange props:

function App() {
  const [open, setOpen] = React.useState(false);
  return (
    <Tooltip.Root open={open} onOpenChange={setOpen}>
      {/* Subcomponents */}
    </Tooltip.Root>
  );
}

Arrow

To add an arrow (caret or triangle) inside the tooltip content that points toward the center of the anchor element, use the Tooltip.Arrow component:

<Tooltip.Positioner>
  <Tooltip.Popup>
    Tooltip
    <Tooltip.Arrow />
  </Tooltip.Popup>
</Tooltip.Positioner>

It automatically positions a wrapper element that can be styled or contain a custom SVG shape.

Cursor following

The tooltip can follow the cursor on both axes or one axis using the trackCursorAxis prop on Tooltip.Root. Possible values are: none (default), both, x, or y.

'use client';
import * as React from 'react';
import { Tooltip } from '@base_ui/react/Tooltip';
import { styled } from '@mui/system';

export default function UnstyledTooltipFollowCursor() {
  return (
    <div style={{ display: 'flex', gap: 12 }}>
      <Tooltip.Root trackCursorAxis="both">
        <AnchorButton>Anchor</AnchorButton>
        <Tooltip.Positioner sideOffset={5}>
          <TooltipPopup>Tooltip</TooltipPopup>
        </Tooltip.Positioner>
      </Tooltip.Root>
    </div>
  );
}

const blue = {
  400: '#3399FF',
  600: '#0072E6',
  800: '#004C99',
};

export const TooltipPopup = styled(Tooltip.Popup)`
  ${({ theme }) => `
    background: ${theme.palette.mode === 'dark' ? 'white' : 'black'};
    color: ${theme.palette.mode === 'dark' ? 'black' : 'white'};
    padding: 4px 6px;
    border-radius: 4px;
    font-size: 95%;
    cursor: default;
  `}
`;

export const AnchorButton = styled(Tooltip.Trigger)`
  border: none;
  background: ${blue[600]};
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 16px;

  &:focus-visible {
    outline: 2px solid ${blue[400]};
    outline-offset: 2px;
  }

  &:hover,
  &[data-popup-open] {
    background: ${blue[800]};
  }
`;

Anchoring

By default, the Trigger acts as the anchor, but this can be changed to another element.

<Tooltip.Positioner anchor={anchorNode}>
<Tooltip.Positioner anchor={anchorRef}>
<Tooltip.Positioner
  anchor={{
    getBoundingClientRect: () => DOMRect,
    // `contextElement` is an optional but recommended property when `getBoundingClientRect` is
    // derived from a real element, to ensure collision detection and position updates work as
    // expected in certain DOM trees.
    contextElement: domNode,
  }}
>

Styling

The Tooltip.Positioner element receives the following CSS variables, which can be used by Tooltip.Popup:

By default, maxWidth and maxHeight are already specified on the positioner using --available-{width,height} to prevent the tooltip from being too big to fit on the screen.

Animations

The tooltip can animate when opening or closing with either:

CSS transitions

Here is an example of how to apply a symmetric scale and fade transition with the default conditionally-rendered behavior:

<Tooltip.Popup className="TooltipPopup">Tooltip</Tooltip.Popup>
.TooltipPopup {
  transform-origin: var(--transform-origin);
  transition-property: opacity, transform;
  transition-duration: 0.2s;
  /* Represents the final styles once exited */
  opacity: 0;
  transform: scale(0.9);
}
 
/* Represents the final styles once entered */
.TooltipPopup[data-open] {
  opacity: 1;
  transform: scale(1);
}
 
/* Represents the initial styles when entering */
.TooltipPopup[data-entering] {
  opacity: 0;
  transform: scale(0.9);
}

Styles need to be applied in three states:

'use client';
import * as React from 'react';
import { Tooltip } from '@base_ui/react/Tooltip';
import { styled } from '@mui/system';

export default function UnstyledTooltipTransition() {
  return (
    <div style={{ display: 'flex', gap: 12 }}>
      <Tooltip.Root>
        <AnchorButton>Anchor</AnchorButton>
        <Tooltip.Positioner sideOffset={5}>
          <TooltipPopup>Tooltip</TooltipPopup>
        </Tooltip.Positioner>
      </Tooltip.Root>
    </div>
  );
}

const blue = {
  400: '#3399FF',
  600: '#0072E6',
  800: '#004C99',
};

export const TooltipPopup = styled(Tooltip.Popup)`
  ${({ theme }) => `
    background: ${theme.palette.mode === 'dark' ? 'white' : 'black'};
    color: ${theme.palette.mode === 'dark' ? 'black' : 'white'};
    padding: 4px 6px;
    border-radius: 4px;
    font-size: 95%;
    cursor: default;
    transition-property: opacity, transform;
    transition-duration: 0.2s;
    opacity: 0;
    transform: scale(0.9);
    transform-origin: var(--transform-origin);

    &[data-open] {
      opacity: 1;
      transform: scale(1);
    }

    &[data-entering] {
      opacity: 0;
      transform: scale(0.9);
    }
  `}
`;

export const AnchorButton = styled(Tooltip.Trigger)`
  border: none;
  background: ${blue[600]};
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 16px;

  &:focus-visible {
    outline: 2px solid ${blue[400]};
    outline-offset: 2px;
  }

  &:hover,
  &[data-popup-open] {
    background: ${blue[800]};
  }
`;

In newer browsers, there is a feature called @starting-style which allows transitions to occur on open for conditionally-mounted components:

/* Base UI API - Polyfill */
.TooltipPopup[data-entering] {
  opacity: 0;
  transform: scale(0.9);
}
 
/* Official Browser API - no Firefox support as of May 2024 */
@starting-style {
  .TooltipPopup[data-open] {
    opacity: 0;
    transform: scale(0.9);
  }
}

CSS animations

CSS animations can also be used, requiring only two separate declarations:

@keyframes scale-in {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
}
 
@keyframes scale-out {
  to {
    opacity: 0;
    transform: scale(0.9);
  }
}
 
.TooltipPopup {
  animation: scale-in 0.2s forwards;
}
 
.TooltipPopup[data-exiting] {
  animation: scale-out 0.2s forwards;
}

JavaScript animations

The keepMounted prop lets an external library control the mounting, for example framer-motion's AnimatePresence component.

function App() {
  const [open, setOpen] = useState(false);
  return (
    <Tooltip.Root open={open} onOpenChange={setOpen}>
      <Tooltip.Trigger>Trigger</Tooltip.Trigger>
      <AnimatePresence>
        {open && (
          <Tooltip.Positioner keepMounted>
            <Tooltip.Popup
              render={
                <motion.div
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  exit={{ opacity: 0 }}
                />
              }
            >
              Tooltip
            </Tooltip.Popup>
          </Tooltip.Positioner>
        )}
      </AnimatePresence>
    </Tooltip.Root>
  );
}

Animation states

Four states are available as data attributes to animate the popup, which enables full control depending on whether the popup is being animated with CSS transitions or animations, JavaScript, or is using the keepMounted prop.

Instant animation

Animations can be removed under certain conditions using the data-instant attribute on Tooltip.Popup. This attribute can be used unconditionally, but it also has different values for granular checks:

In most of these cases, you'll want to remove any animations:

.TooltipPopup[data-instant] {
  transition-duration: 0s;
}

Overriding default components

Use the render prop to override the rendered elements with your own components.

// Element shorthand
<Tooltip.Popup render={<MyTooltipPopup />} />
// Function
<Tooltip.Popup render={(props) => <MyTooltipPopup {...props} />} />

API Reference

TooltipProvider

Provides a shared delay for tooltips so that once a tooltip is shown, the rest of the tooltips in the group will not wait for the delay before showing.

PropTypeDefaultDescription
closeDelaynumberThe delay in milliseconds until tooltips within the group are closed.
delaynumberThe delay in milliseconds until tooltips within the group are open.
timeoutnumber400The timeout in milliseconds until the grouping logic is no longer active after the last tooltip in the group has closed.

TooltipRoot

The foundation for building custom-styled tooltips.

PropTypeDefaultDescription
animatedbooltrueWhether the tooltip can animate, adding animation-related attributes and allowing for exit animations to play. Useful to disable in tests to remove async behavior.
closeDelaynumber0The delay in milliseconds until the tooltip popup is closed.
defaultOpenboolfalseWhether the tooltip popup is open by default. Use when uncontrolled.
delaynumber600The delay in milliseconds until the tooltip popup is opened.
hoverablebooltrueWhether the user can move their cursor from the trigger element toward the tooltip popup element without it closing using a "safe polygon" technique.
onOpenChangefuncCallback fired when the tooltip popup is requested to be opened or closed. Use when controlled.
openboolfalseWhether the tooltip popup is open. Use when controlled.
trackCursorAxisenum'none'Determines which axis the tooltip should track the cursor on.

TooltipTrigger

Renders a trigger element that opens the tooltip.

PropTypeDefaultDescription
classNameunionClass names applied to the element or a function that returns them based on the component's state.
renderunionA function to customize rendering of the component.

TooltipPositioner

The tooltip positioner element.

PropTypeDefaultDescription
alignmentenum'center'The alignment of the tooltip element to the anchor element along its cross axis.
alignmentOffsetnumber0The offset of the tooltip element along its alignment axis.
anchorunionThe element to which the tooltip element is anchored to.
arrowPaddingnumber5Determines the padding between the arrow and the tooltip edges. Useful when the tooltip element has rounded corners via border-radius.
classNameunionClass names applied to the element or a function that returns them based on the component's state.
collisionBoundaryunion'clippingAncestors'The boundary that the tooltip element should be constrained to.
collisionPaddingunion5The padding between the tooltip element and the edges of the collision boundary to add whitespace between them to prevent them from touching.
containerunionThe container element the tooltip positioner is appended to.
hideWhenDetachedboolfalseWhether the tooltip element is hidden if it appears detached from its anchor element due to the anchor element being clipped (or hidden) from view.
keepMountedboolfalseWhether the tooltip remains mounted in the DOM while closed.
positionMethodenum'absolute'The CSS position strategy for positioning the tooltip element.
renderunionA function to customize rendering of the component.
sideenum'top'The side of the anchor element that the tooltip element should be placed at.
sideOffsetnumber0The gap between the anchor element and the tooltip element.
stickyboolfalseWhether to allow the tooltip to remain stuck in view while the anchor element is scrolled out of view.

TooltipPopup

The tooltip popup element.

PropTypeDefaultDescription
classNameunionClass names applied to the element or a function that returns them based on the component's state.
renderunionA function to customize rendering of the component.

TooltipArrow

Renders an arrow that points to the center of the anchor element.

PropTypeDefaultDescription
classNameunionClass names applied to the element or a function that returns them based on the component's state.
hideWhenUncenteredboolfalseIf true, the arrow will be hidden when it can't point to the center of the anchor element.
renderunionA function to customize rendering of the component.

Contents