How to prevent auto-focus on first button in dialog?
How can I prevent the first button in a dialog to be automatically focused on open?
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
truefor default behaviorfalse/undefinedto 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
| Value | Behavior |
|---|---|
false | Do not move focus |
true | Default behavior (first tabbable element or popup) |
RefObject | Focus the referenced element |
function | Called with interaction type, returns element/boolean |
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.
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:
- Focus stays on the trigger (or wherever it was before)
- The buttons remain fully focusable - users can Tab to them or click them normally
- 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
});
});
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.
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:
| Value | Focus Trapped | Scroll Locked | Outside 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
Claude Opus 4.5