How to prevent auto-focus on first button in dialog?

master
a0f6cf5[internal] Iterate on resolveMultipleLabels in one loop (#3709)

How can I prevent the first button in a dialog to be automatically focused on open?

Avatar of Claude Opus 4.5Claude Opus 4.5commented

I'll help you find how to control initial focus behavior in Base UI's Dialog component. Let me search the codebase for relevant information.

I found that the Dialog.Popup component has an initialFocus prop. Let me look at the Dialog component implementation and its tests to give you a complete answer.

Now let me see the test for the initialFocus={false} case specifically:

I have all the information needed. Here's how to prevent automatic focus in a Dialog:

##Preventing Automatic Focus in Dialog

By default, when a Dialog opens, Base UI focuses the first focusable element inside the popup. You can control this behavior using the initialFocus prop on <Dialog.Popup>.

###Option 1: Disable automatic focus completely

Set initialFocus={false} to prevent moving focus when the dialog opens:

<Dialog.Root>
  <Dialog.Trigger>Open</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Popup initialFocus={false}>
      <button>First Button</button>
      <button>Second Button</button>
    </Dialog.Popup>
  </Dialog.Portal>
</Dialog.Root>

###Option 2: Focus a specific element

Pass a ref to focus a specific element instead of the first one:

function MyDialog() {
  const secondButtonRef = React.useRef(null);
  
  return (
    <Dialog.Root>
      <Dialog.Trigger>Open</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Popup initialFocus={secondButtonRef}>
          <button>First Button</button>
          <button ref={secondButtonRef}>Second Button (focused)</button>
        </Dialog.Popup>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

###Option 3: Conditional focus based on interaction type

Pass a function that receives the interaction type ('mouse', 'touch', 'pen', or 'keyboard') and return:

  • An element to focus
  • true for default behavior
  • false/undefined to do nothing
<Dialog.Popup 
  initialFocus={(interactionType) => {
    if (interactionType === 'keyboard') {
      return someRef.current; // Focus specific element for keyboard users
    }
    return false; // Don't move focus for mouse/touch
  }}
>
  {/* ... */}
</Dialog.Popup>

###Summary of initialFocus values

ValueBehavior
falseDo not move focus
trueDefault behavior (first tabbable element or popup)
RefObjectFocus the referenced element
functionCalled with interaction type, returns element/boolean
Avatar of Santiago GalánSantiago Galáncommented

I don't want the buttons to be focused on open but I do want to maintain the focus ability, if the users tabs or move focus in other way it should let them focus the buttons.

Avatar of Claude Opus 4.5Claude Opus 4.5commented

That's exactly what initialFocus={false} does! It only prevents the automatic focus movement when the dialog opens, but it doesn't disable focus capability on the elements inside.

Users can still:

  • Tab into the dialog and focus the buttons
  • Click on buttons to focus them
  • Use any other method to move focus to the buttons

Here's the solution:

<Dialog.Root>
  <Dialog.Trigger>Open</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Popup initialFocus={false}>
      <button>First Button</button>
      <button>Second Button</button>
    </Dialog.Popup>
  </Dialog.Portal>
</Dialog.Root>

With initialFocus={false}, when the dialog opens:

  1. Focus stays on the trigger (or wherever it was before)
  2. The buttons remain fully focusable - users can Tab to them or click them normally
  3. All normal keyboard navigation continues to work

This is confirmed by the test case in the source code, which shows that after opening with initialFocus={false}, the focus simply remains on the trigger rather than being moved into the dialog:

// From DialogPopup.test.tsx
it('should not move focus when initialFocus is false', async () => {
  // ...
  await user.click(trigger);
  await waitFor(() => {
    expect(trigger).toHaveFocus(); // Focus stays on trigger
  });
});
Avatar of Santiago GalánSantiago Galáncommented

I did that and since the focus remains on the trigger, if I tab I don't focus the dialog buttons, but other elements under the dialog.

Avatar of Claude Opus 4.5Claude Opus 4.5commented

Ah, I see the issue! When focus stays on the trigger and the dialog is non-modal, tabbing will naturally move to the next elements in the DOM order, not into the dialog.

Let me check how modal dialogs handle focus trapping:

This reveals the issue. When using a modal dialog (the default, or modal={true} or modal="trap-focus"), focus is trapped inside the dialog. But if you're using modal={false}, focus can escape to elements outside.

Two solutions:

###Solution 1: Use a modal dialog (recommended for most cases)

If you want focus to be trapped inside the dialog, make sure you're using a modal dialog (which is the default):

<Dialog.Root modal={true}> {/* or just <Dialog.Root> since true is default */}
  <Dialog.Trigger>Open</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Popup initialFocus={false}>
      <button>First Button</button>
      <button>Second Button</button>
    </Dialog.Popup>
  </Dialog.Portal>
</Dialog.Root>

With a modal dialog, even though initialFocus={false} keeps focus on the trigger initially, pressing Tab will move focus into the dialog because focus is trapped within it.

###Solution 2: Use modal="trap-focus"

If you want focus trapping but still want to allow page scrolling and pointer interactions outside:

<Dialog.Root modal="trap-focus">
  <Dialog.Trigger>Open</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Popup initialFocus={false}>
      <button>First Button</button>
      <button>Second Button</button>
    </Dialog.Popup>
  </Dialog.Portal>
</Dialog.Root>

###Summary of modal options:

ValueFocus TrappedScroll LockedOutside Pointer Disabled
true (default)
"trap-focus"
false

Are you currently using modal={false}? If so, switching to modal={true} or modal="trap-focus" should fix the tabbing issue while still respecting your initialFocus={false} preference.


END OF POST

Log in to add a comment
Claude Opus 4.5
How to prevent auto-focus on first button in dialog? — mui/base-ui