Skip to content

Combination of Toolbar and Tooltip causes glitching on Tooltip #10496

Description

@KoenCa

Provide a general summary of the issue here

I have a use case where I use the Toolbar component with buttons that each have a tooltip. When I tab into the toolbar and then tab again to exit it, then the tooltip of the last button flashes briefly and the tooltip of the button that had focus is rendered at the top left of the viewport.

🤔 Expected Behavior?

When I tab out of the toolbar the last tooltip should not be shown and the tooltip of the first button should not be shown in the top left corner of the viewport.

😯 Current Behavior

I've created a reproduction in Stackblitz: reproduction

I also made a screen recording showcasing the issue:

CleanShot.2026-08-21.at.12.32.58.mp4

💁 Possible Solution

I analysed the issue with AI and it came up with following explanation and work around. The work around is visual and doesn't fix the issue, but makes it less noticeable at least.

Cause

useToolbar's Tab handler calls focusManager.focusLast() and returns without
preventDefault, so the last control is focused before the browser carries focus onward.
The tooltip's focus path opens immediately, useTooltipTrigger.onFocus calls
state.open(true), and the subsequent blur closes it, so an open and a close arrive back
to back within one keydown.

A tooltip overlay then ends up rendered while the trigger state is closed. In that state
useOverlayPosition.updatePosition returns early on !isOpen, and placement is
position?.placement ?? null, so the overlay is never measured. That produces exactly the
observed DOM: no inline position, no data-placement, painted at the document origin.

What keeps the node alive afterwards is not confirmed. One candidate is useExitAnimation,
whose useAnimation layout effect has deps [ref, isActive, onEnd]. All three are stable,
so the effect runs at most once per exit and a missed onEnd would strand the node.

Workaround

Hide the overlay whenever the placement render prop is null, which is exactly the
condition "mounted but never measured". React Aria already treats placement as the
readiness signal, isEntering is useEnterAnimation(tooltipRef, !!placement).

<Tooltip className={({ placement }) => (placement ? "" : "invisible")} />

Use visibility: hidden, not display: none. updatePosition measures the overlay with
getBoundingClientRect, so removing it from layout gives a wrong position on the normal
open path, where the first commit also has placement === null for one frame.

This suppresses the misplaced tooltip in both presentations. It does not address the
underlying state, and the last control's tooltip still flashes briefly on the way out,
since that one is genuinely measured before it closes.

CleanShot.2026-08-21.at.12.39.30.mp4

🔦 Context

For work I'm building a "bulk action bar" that will be overlayed over a table when multiple items are selected. The buttons are small icon buttons that get a tooltip to explain the behavior of each button.

🖥️ Steps to Reproduce

I've created a reproduction in Stackblitz: reproduction

Version

react-aria-components 1.20.0

What browsers are you seeing the problem on?

Chrome

If other, please specify.

No response

What operating system are you using?

MacOS

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions