Skip to content

[FEAT REQ] Open on focus only when the user moved focus there (:focus-visible), not when a script or the browser restores it #1287

Open

Description

@gogakoreli

Problem to solve

The default openEvents include focus, so the tooltip opens whenever its anchor receives focus. That is right for keyboard users, but two other kinds of focus open it as well:

  1. Focus a script returns to the trigger. A menu or dialog that hands focus back to its trigger when it closes, as the WAI-ARIA menu button pattern asks, reopens the trigger's tooltip right after a mouse user picks an item. This is [BUG]: tooltip show again after closing the modal #1096, closed as not planned in 2023, where the advice was to stop opening on focus.
  2. Focus the browser restores on window return. If the anchor has focus when the user switches to another window or tab, the browser focuses it again when they come back, and the tooltip opens although the user did nothing. This happens even when that focus came from the keyboard, for example after closing a menu with Escape.

The available option, openEvents={{ focus: false }}, fixes both but removes the tooltip for keyboard users, which trades the bug for an accessibility gap.

Proposed solution

Open on focus only when the user moved focus to the anchor, the way Floating UI's useFocus does (visibleOnly, on by default):

  • Open only if the anchor matches :focus-visible. The browser already makes this call: keyboard focus matches, and focus a script moves after a mouse click does not. This covers case 1.
  • Skip the focus that follows a window blur on the element that was focused at the time: record document.activeElement on window blur, and ignore the next focus on that element. This covers case 2.

Both fit v6's design: one window blur listener next to the shared document listeners, not one per anchor, and a matches(':focus-visible') check in the existing focusin handler. Safari needs care: it can fail to match :focus-visible when focus comes from outside the document, which useFocus handles by tracking whether the last input was a key or a pointer. JSDOM does not match :focus-visible, so tests need a fallback.

This could be the default, or opt-in if a change in default behaviour is a concern.

Alternatives considered

  • openEvents={{ focus: false }}: removes keyboard tooltips.
  • onFocus={(e) => e.target.blur()}, suggested in [BUG]: tooltip show again after closing the modal #1096: breaks keyboard focus on the trigger.
  • disableTooltip(anchor): decides per anchor, not per focus, so it cannot tell a Tab from a restored focus.
  • A capture-phase focus listener on window that stops propagation of an unrequested focus. This worked on v5, where anchors listen to focus, but not on v6, which listens to delegated focusin. It depends on internals.

We're happy to send a PR if the approach sounds right.

Example usage

// Opt-in form, if the new behaviour should not be the default:
<Tooltip id="tip" openEvents={{ mouseover: true, focus: 'visible' }} />

Additional context

Repro: https://codesandbox.io/s/nyy6rr (react-tooltip 6.0.9, React 19.2)

import { useRef, useState } from 'react';
import { Tooltip } from 'react-tooltip';

export default function App() {
  const [open, setOpen] = useState(false);
  const triggerRef = useRef<HTMLButtonElement>(null);

  const close = () => {
    setOpen(false);
    // Return focus to the trigger once the menu has closed, as the menu button pattern asks.
    setTimeout(() => triggerRef.current?.focus(), 150);
  };

  return (
    <main style={{ padding: 80, display: 'flex', gap: 16 }}>
      <button ref={triggerRef} data-tooltip-id="tip" data-tooltip-content="Select model" onClick={() => setOpen(true)}>
        Model
      </button>
      {open && <button onClick={close}>Pick an item</button>}
      <Tooltip id="tip" />
    </main>
  );
}
  1. Click "Model", then click "Pick an item". Focus returns to "Model" and its tooltip opens, with the pointer elsewhere. On 6.0.9, returning focus in the same tick as the click happens not to reopen it; returning it 50 ms or more later does, which is what a menu with a closing animation does.
  2. Click "Model", move the pointer away, switch to another window, then switch back. The tooltip opens.
  3. Reload and press Tab. The tooltip opens, as it should; this is the behaviour to keep.

Case 1 seen on 6.0.9 and 5.30.0 in Chromium on Linux; case 2 seen on 5.30.0 in an Electron app, and v6's focusin handler has no window check either. Related: #1201.

Investigation and write-up prepared with AI assistance and reviewed before posting.

Checks

  • I searched existing issues and did not find an open match.

Activity

  1. danielbarion commented on Oct 26, 2026

    @danielbarion
    Member

    @gogakoreli can you please check the beta release from #1288 and let me know your thoughts?

  2. gogakoreli commented on Oct 28, 2026

    @gogakoreli
    Author

    Thanks @danielbarion! Tried the beta, it fixes both cases from this issue. I found two smaller problems and left the details on the PR: #1288 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions