When updating a fragment, you control where Unpoly places the focus
by setting an [up-focus] attribute or by passing a { focus } option.
By default, following a link focuses the new content like a full page load would, and a minor update keeps the focus where it is.
Placing the focus matters for accessibility. A screen reader starts reading from the focused position, and keyboard users continue tabbing from there. Without focus management, a user who activated a link would be left somewhere in the old content, unaware that the page has changed.
When navigating, Unpoly tries a sequence of focus strategies that works for most cases:
#hash, focus the element matching that hash.[autofocus] element in the new fragment.You can change this sequence in up.fragment.config.autoFocus.
Following links and submitting forms are considered navigation, so the default strategy is applied without further configuration.
You can also enable it explicitly by setting [up-focus="auto"]:
<a href="/details" up-follow up-focus="auto">Show more</a> <!-- mark: up-focus="auto" -->
From JavaScript, pass { focus: 'auto' }:
up.render({ url: '/path', focus: 'auto' }) // mark: focus: 'auto'
When rendering without navigation, e.g. with up.render(), focus is not changed by default.
up.reload() and [up-validate] keep the current focus.
To focus the updated fragment, set [up-focus="target"]:
<a href="/details" up-target="#details" up-focus="target">Show more</a> <!-- mark: up-focus="target" -->
If the fragment isn't focusable by itself, Unpoly gives it a [tabindex="-1"] attribute.
This makes it focusable from JavaScript without adding it to the keyboard's tab sequence.
Focusing an element does not scroll it into view. To also reveal the fragment, set [up-scroll="target"].
To focus a matching element, set [up-focus] to any CSS selector string:
<a href="/advanced-search" up-target=".content" up-focus="input[type=search]">Advanced search</a> <!-- mark: up-focus="input[type=search]" -->
Unpoly prefers a match within the updated fragment, but the selector may match anywhere in the fragment's layer.
From JavaScript you can also pass the Element object that should be focused:
up.render({ target: '.content', url: '/advanced-search', focus: searchField })
#hash targetTo focus the element matching the #hash in the updated URL, set [up-focus="hash"]:
<a href="/product/12#features" up-follow up-focus="hash">Show features</a> <!-- mark: #features -->
When the URL has no hash, or no element matches it, focus is not changed.
To focus the updated layer's main fragment, set [up-focus="main"]:
<a href="/contact" up-follow up-focus="main">Contact us</a> <!-- mark: up-focus="main" -->
To focus the container element of the updated layer, set [up-focus="layer"]:
<a href="/details" up-follow up-focus="layer">Show more</a> <!-- mark: up-focus="layer" -->
In an overlay, this focuses the overlay element, which screen readers announce as a dialog.
To preserve focus-related state through a fragment update, set [up-focus="keep"]:
<a href="/list" up-follow up-focus="keep">Reload list</a> <!-- mark: up-focus="keep" -->
For instance, when a focused <input> element is swapped out with a new fragment,
[up-focus="keep"] causes the same input to be focused after the update.
The following properties are preserved:
Focus can only be preserved when the focused element has a derivable target selector,
e.g. by setting an [id] or [name] attribute.
up.reload() uses this strategy by default.
When revisiting an earlier page, you may want to restore the focus from that visit.
Set [up-focus="restore"] to restore the last known focus state for the updated URL:
<a href="/list" up-follow up-focus="restore">Back to list</a> <!-- mark: up-focus="restore" -->
Before every fragment update, Unpoly saves focus-related state for the current URL.
You can prevent this by setting [up-save-focus="false"].
Focus state is remembered for each layer until the next full page load.
When nothing is known for the URL, focus is not changed. Add a fallback with [up-focus="restore, target"].
When the user presses the back button, Unpoly restores focus the same way. See Restoring history.
If you don't want Unpoly to touch the focus, set [up-focus="false"].
For example, this link opts out of the default focus strategy that
navigation would apply:
<a href="/details" up-follow up-focus="false">Show more</a> <!-- mark: up-focus="false" -->
Note that even with [up-focus="false"] the focus may change during a fragment update.
For instance, when a fragment contains focus and is then swapped, focus reverts to the <body> element.
To actively preserve focus, use [up-focus="keep"] instead.
To attempt multiple focus strategies, separate options with a comma. Unpoly uses the first strategy that finds an element to focus:
<a href="/path#section" up-follow up-focus="hash, target">Link label</a> <!-- mark: up-focus="hash, target" -->
This first tries to focus an element matching the URL's #hash.
If the URL has no hash (or if no element matches), the new fragment is focused.
In JavaScript you can pass an array of options:
up.render({ url: '/path#section', focus: ['hash', 'target'] }) // mark: focus: ['hash', 'target']
The default strategy is such a sequence.
To only focus when a main target is updated,
append -if-main to any string option on this page:
<a href="/details" up-follow up-focus="target-if-main">Show more</a> <!-- mark: up-focus="target-if-main" -->
To only focus when the focus was lost with the old fragment, append -if-lost instead:
<a href="/list" up-follow up-focus="target-if-lost">Reload list</a> <!-- mark: up-focus="target-if-lost" -->
This focuses the new fragment, but only if the update removed the previously focused element. When the user is typing in a field outside the updated fragment, their focus is left alone.
To implement other conditions, pass a function instead.
To implement your own focus logic, pass a function as { focus } option.
The function is called with the updated fragment and an options object. The function is expected to either:
{ scroll } option.
Both Element#focus() and up.focus()
accept a { preventScroll: true } option to prevent scrolling.Here is a focus function that focuses the first field in the new fragment:
up.render({
target: 'form',
url: '/users/new',
focus: (fragment) => {
let field = fragment.querySelector('input')
if (field) up.focus(field, { preventScroll: true }) // mark-line
}
})
Outside a fragment update, call up.focus() to focus any element.
Unlike Element#focus(), it also reveals the element,
honoring fixed layout elements,
and assigns the focus visibility classes that your CSS can style:
let field = document.querySelector('input[type=search]')
up.focus(field)
When opening an overlay, the first applicable strategy is used:
[autofocus] element in the new overlay.To focus something else, set an [up-focus] attribute on the opening link:
<a href="/users/new" up-layer="new" up-focus="input[name=email]">Add user</a> <!-- mark: up-focus="input[name=email]" -->
By default focus is trapped for all overlays except popups. Moving a trapped focus outside the overlay will immediately re-focus it. This prevents keyboard users from accidentally tabbing into content from other layers.
Occasionally the focus trap conflicts with overlays opened by other JavaScript libraries.
You can address this by configuring up.layer.config.foreignOverlaySelectors.
To disable focus trapping entirely, configure up.layer.config.overlay.trapFocus = false
or open the overlay with { trapFocus: false }.
When updating a fragment within an existing overlay, all focus strategies on this page can be used.
When a closed overlay had focus, the link that originally opened the overlay is re-focused.
If the overlay was opened programmatically without an { origin } option, the opening link is unknown.
In that case, the parent layer is focused.
Tip Your coding agent can search these docs locally with the Unpoly agent skill