When updating a fragment, you control how Unpoly scrolls by setting an [up-scroll] attribute
or by passing a { scroll } option.
By default, following a link scrolls like a full page load would, and a minor update leaves all scroll positions alone.
When navigating, Unpoly tries a sequence of scroll strategies that works for most cases:
#hash, scroll to the element matching that hash.You can change this sequence in up.fragment.config.autoScroll.
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-scroll="auto"]:
<a href="/details" up-follow up-scroll="auto">Show more</a> <!-- mark: up-scroll="auto" -->
From JavaScript, pass { scroll: 'auto' }:
up.render({ url: '/path', scroll: 'auto' }) // mark: scroll: 'auto'
When rendering without navigation, e.g. with up.render() or [up-validate], nothing is scrolled by default.
up.reload() keeps the current scroll positions.
We often want to draw attention to updated elements, by making sure they are visible in the viewport. This is called revealing an element.
Tip
When revealing an element, consider also focusing it to set the reading position for screen readers.
To scroll to the updated fragment, set [up-scroll="target"]:
<a href="/details" up-follow up-scroll="target">Show more</a> <!-- mark: up-scroll="target" -->
The fragment's viewport is scrolled as little as necessary to make the new fragment visible. If the fragment is already visible, nothing scrolls.
You can control how far to scroll, how to handle large elements, or apply a scroll margin.
See Tuning the scroll behavior for details.
To reveal a matching element, set [up-scroll] to any CSS selector string:
<a href="/details" up-follow up-scroll="h1">Show more</a> <!-- mark: up-scroll="h1" -->
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 revealed:
up.render({ url: '/path', scroll: document.body })
#hash targetTo reveal the element matching the #hash in the updated URL, set [up-scroll="hash"]:
<a href="/product/12#features" up-follow up-scroll="hash">Show features</a> <!-- mark: #features -->
This works like the browser's own hash scrolling, but honors fixed layout elements that would otherwise obstruct the revealed element. The element is aligned with the top edge of the viewport.
When the URL has no hash, or no element matches it, nothing scrolls.
To reveal the container element of the updated layer, set [up-scroll="layer"]:
<a href="/details" up-layer="new" up-scroll="layer">Show more</a> <!-- mark: up-scroll="layer" -->
Most layer modes bring their own scrollbar, and will effectively be scrolled to the top.
A popup has no scrollbar, so its parent layer will scroll to reveal the popup frame.
To reveal the updated layer's main fragment, set [up-scroll="main"]:
<a href="/contact" up-follow up-scroll="main">Contact us</a> <!-- mark: up-scroll="main" -->
To reset scroll positions to the top, set [up-scroll="top"]:
<a href="/list" up-follow up-scroll="top">Back to list</a> <!-- mark: up-scroll="top" -->
This affects the closest viewport around the updated fragment, and all viewports within it.
To scroll to the bottom, set [up-scroll="bottom"]:
<a href="/chat" up-follow up-scroll="bottom">Open chat</a> <!-- mark: up-scroll="bottom" -->
This affects the closest viewport around the updated fragment, and all viewports within it.
To scroll to a specific pixel position from the top, set a number value:
<a href="/list" up-follow up-scroll="35">Back to list</a> <!-- mark: up-scroll="35" -->
To scroll to the bottom, but leave a margin of some pixels, set a negative number value:
<a href="/messages" up-follow up-scroll="-40">Latest messages</a> <!-- mark: up-scroll="-40" -->
If you don't want Unpoly to touch any scroll positions, set [up-scroll="false"].
For example, this link opts out of the default scrolling strategy that
navigation would apply:
<a href="/details" up-follow up-scroll="false">Show more</a> <!-- mark: up-scroll="false" -->
Note
DOM mutations may still cause scrolling, even when Unpoly does not touch scroll positions.
Set[up-scroll="keep"]to actively preserve scroll positions.
Scroll positions reset when you insert a new viewport element (as opposed to updating a child element). This is default browser behavior for newly inserted elements.
You can ask Unpoly to preserve the scroll positions of all viewports around the updated fragment.
To do so, set [up-scroll="keep"]:
<a href="/list" up-follow up-scroll="keep">Reload list</a> <!-- mark: up-scroll="keep" -->
Internally, Unpoly measures scroll positions before the update, and restores the same positions after the update.
For this to work, viewports must have a derivable target selector,
e.g. by setting an [id] attribute.
up.reload() uses this strategy by default, so a reloaded list stays where the user left it.
When revisiting an earlier page, you may want to restore the scroll positions from that visit.
Set [up-scroll="restore"] to restore the last known scroll positions for the updated URL:
<a href="/list" up-follow up-scroll="restore">Back to list</a> <!-- mark: up-scroll="restore" -->
Before every fragment update, Unpoly saves the scroll positions for the current URL.
You can prevent this by setting [up-save-scroll="false"].
Scroll positions are remembered for each layer until the next full page load.
When no positions are known for the URL, nothing scrolls. Add a fallback with [up-scroll="restore, top"].
When the user presses the back button, Unpoly restores scroll positions the same way. See Restoring history.
To attempt multiple scroll strategies, separate options with a comma:
<a href="/path#section" up-follow up-scroll="hash, top">Link label</a> <!-- mark: up-scroll="hash, top" -->
This first tries to reveal an element matching the URL's #hash.
If the URL has no hash (or if no element matches), the viewport is scrolled to the top.
In JavaScript you can pass an array of options:
up.render({ url: '/path#section', scroll: ['hash', 'top'] }) // mark: scroll: ['hash', 'top']
The default strategy is such a sequence.
To only scroll when a main target is updated,
append -if-main to any string option on this page:
<a href="/list" up-follow up-scroll="top-if-main">Back to list</a> <!-- mark: up-scroll="top-if-main" -->
This scrolls to the top, but only if the updated fragment contains the main fragment. When a link targets a minor fragment, nothing scrolls.
To implement other conditions, pass a function instead.
Unpoly scrolls the viewport closest to the updated fragment.
By default the only viewport is the document itself.
An app with its own scrolling panels marks each panel with an [up-viewport] attribute.
When updating multiple fragments, any [up-scroll] attribute (or { scroll } option)
is only applied to the first fragment. This is what you want when you only have a single viewport,
and you don't want secondary fragments to influence the one scroll bar.
However, when you update fragments within multiple viewports, you can only scroll once that way:
<a href="/dashboard" up-target="#fragment1, #fragment2" up-scroll="bottom"> <!-- mark: up-target="#fragment1, #fragment2" -->
Update fragments
</a>
<div id="viewport1" up-viewport style="overflow-y: scroll">
<!-- chip: ✔ Will be scrolled -->
<div id="fragment1">…</div>
</div>
<div id="viewport2" up-viewport style="overflow-y: scroll">
<!-- chip: ❌ Will not be scrolled -->
<div id="fragment2">…</div>
</div>
To scroll multiple viewports, set an [up-scroll-map] attribute.
Its value is a relaxed JSON object mapping selectors to scroll options:
<a
href="/dashboard"
up-target="#fragment1, #fragment2"
up-scroll-map="{ '#viewport1': 'bottom', '#viewport2': 'bottom' }"
>
Update fragments
</a>
You can also select viewports by any contained fragment. The following two scroll maps have the same effect:
<a … up-scroll-map="{ '#fragment1': 'bottom', '#fragment2': 'bottom' }">…</a>
<a … up-scroll-map="{ '#viewport1': 'bottom', '#viewport2': 'bottom' }">…</a>
In JavaScript you can pass an object as a { scrollMap } option:
up.render({
url: '/dashboard',
target: '#fragment1, #fragment2',
scrollMap: { '#viewport1': 'bottom', '#viewport2': 'bottom' }
})
When a scroll map is given, the [up-scroll] attribute or { scroll } option is ignored.
To implement your own scrolling logic, pass a function as { scroll } option.
The function is called with the updated fragment and is expected to either:
Here is a scrolling function that scrolls to 15 pixels from the top:
up.render({
target: '#target',
url: '/path',
scroll: (fragment) => document.documentElement.scrollTop = 15 // mark-line
})
Outside a fragment update, call up.reveal() to scroll an element into view.
It uses the same logic as [up-scroll="target"], including the handling of fixed layout elements:
up.reveal('#comments')
You can animate the scroll motion, declare fixed navigation bars that obstruct the viewport,
limit how far a large element is revealed, apply a scroll margin, or snap to the screen edge.
See Tuning the scroll behavior for details.
Tip Your coding agent can search these docs locally with the Unpoly agent skill