livewire #10317: [4.x] Fix autofocus after `wire:navigate`
Contributed by joshhanley
Part of the livewire v4.4.7 release
Fix · Livewire v4.x · PR #10317 · fixes #10299
wire:navigate pages that rely on the autofocus attribute have long had a silent gap: the attribute worked on the first full page load, but was ignored on every client-side navigation after that. PR #10317 fixes this by finally enabling a helper that has been sitting dead in the navigate plugin's code, so any element marked autofocus now receives focus after a wire:navigate swap — including when you navigate back to a cached page. There is nothing to configure; the behavior simply starts working when you pick up this change.
What changed
Inside js/plugins/navigate/index.js, the navigate plugin already contained an autofocusElementsWithTheAutofocusAttribute() helper. It was wired up in both navigation paths — the normal swap path and the "restore from page cache" path (back/forward buttons) — but both call sites were gated behind a module-level let autofocus = false flag that was never flipped to true. The helper ran, but the condition made it a no-op.
This PR does three things:
- Removes the dead
autofocus = falseflag entirely. - Calls
autofocusElementsWithTheAutofocusAttribute()unconditionally in both navigation paths. - Reorders the calls slightly: focus is now applied after Alpine is initialized on the new page (
nowInitializeAlpineOnTheNewPage(Alpine)comes first), and the oldsetTimeout(...)wrapper is gone.
That last point matters for correctness. Running the autofocus helper after Alpine initialization ensures Alpine has finished setting up the new DOM before focus is moved, avoiding any race with initialization that could pull focus elsewhere.
Why it matters
Browsers only honor native autofocus during the initial document load. When wire:navigate swaps in a new page, the document has long since processed autofocus, so newly inserted elements with the attribute were simply skipped. The practical symptom: a search page or form whose input focuses itself on a hard load does nothing when you arrive there via wire:navigate.
This made autofocus effectively useless in any SPA-style Livewire app, forcing developers into manual workarounds — setTimeout(() => el.focus()) in an alpine:navigated listener, or similar. Notably, the contributor dug through the git history of both Livewire's and Alpine's navigate plugins looking for an intentional reason the flag was disabled, and found none. It appears to have been dead code from the start rather than a deliberate guard, which made enabling it safe.
Seeing it in practice
use Livewire\Component;
class FirstAutofocusPage extends Component
{
public function render(): string
{
return <<<'HTML'
<div>
<input id="first-autofocus" autofocus type="text">
<a href="/second-autofocus" wire:navigate>Go to next page</a>
</div>
HTML;
}
}
class SecondAutofocusPage extends Component
{
public function render(): string
{
return <<<'HTML'
<div>
<textarea id="second-autofocus" autofocus></textarea>
</div>
HTML;
}
}
With this fix in place:
- Visiting
/first-autofocusfocuses the first input (native behavior, unchanged). - Clicking the
wire:navigatelink swaps in the second page, and the textarea now receives focus. - Pressing the back button restores the cached first page — and the first input is focused again, because the cache-restore path now also applies autofocus.
If you need to override the behavior for a specific page, you still can: alpine:navigated fires after autofocus runs, so a listener can redirect focus wherever you like:
document.addEventListener('alpine:navigated', () => {
document.querySelector('#my-input')?.focus()
})
Upgrading from v3.8.10
If you're coming from v3.8.10, the gap existed there too — the disabled flag goes back through the navigate plugin's history in both projects. So this arrives as a behavior change bundled with the v4.x line, and the impact is:
- No API or config changes. There's no new directive, no flag to enable. Existing
autofocusattributes just start working after navigation. - Audit pages that intentionally left focus alone. If any navigation flow silently depended on nothing being focused after a swap (e.g., to keep a keyboard shortcut handler or a global search trigger active), adding
autofocusis now honored — simply remove the attribute from elements where you don't want focus moved. - Delete your workarounds. If you wrote custom JS to focus an element after
wire:navigate(a common pattern to work around #10299), that code is now redundant and can be removed. - Accessibility is, if anything, improved. Moving focus to a newly rendered interactive element is generally what assistive technologies expect after a page-level content change.
The change is covered by two new browser tests in SupportNavigate/BrowserTest.php: one verifying forward navigation focuses the target page's autofocus element even when the previous page also had one, and one verifying focus is restored when navigating back to a cached page.
Takeaways
autofocusnow works afterwire:navigate, including on back/forward navigation to cached pages — fixing a long-standing gap (#10299).- The fix enables a previously dead helper and runs it after Alpine initialization, before
alpine:navigatedfires. - No configuration or template changes are required; remove
autofocuswhere you don't want focus moved, or listen foralpine:navigatedto override focus. - If you built a manual focus workaround after navigation, you can safely delete it.