csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[css-scroll-snap-1] Avoid page scrolling skipping past snappable items

Open flackr opened this issue 1 year ago • 9 comments

This originally came up from @johannesodland as a potential issue with scroll-buttons in https://github.com/w3c/csswg-drafts/issues/10722#issuecomment-2353648101 however it's a pre-existing generic issue with scroll-snap, so filing it separately.

When a user pages down, usually by pressing the page-down key, they expect to scroll no further than one page if that is a valid scroll location. However, some browsers can select the next snap point as the target resulting in a scroll of more than a scrollport in length.

E.g. if you have a scrollport height of 1000px, most browsers will try to scroll by 850px. If however the nearest snappable element has a snap alignment that is further, then it can be selected, resulting in scrolls even greater than 1000px and an experience where content has been skipped over - even when that content itself defined a valid snap area. We should fix this so that it's easy to make an experience where

Codepen demo using page-down key: https://codepen.io/flackr/pen/abgeOKY Original demo from @johannesodland using scrollTo api: https://codepen.io/johannesodland/pen/WNqmoYy

flackr avatar Sep 18 '24 18:09 flackr

Naively for built-in interactions, the UA could do something like select a snap area which scrolls no further than 100% of the scrollport if possible, though I wonder if there's some way we could make this work for programmatic scrolls as well as in the scrollTo example?

E.g. we could say that for any scrolls less than or equal to 1 scrollport, we try to select a snap area which keeps the scroll less than that distance. I say "try" because developers can position snap areas arbitrarily far apart and we should make some progress in the requested scrolling direction.

flackr avatar Sep 18 '24 18:09 flackr

Alternately a more generic option could be to snap in a way that scrolls no more than the requested distance if possible. This would ensure that page-down never scrolls more than 85% unless that is necessary to go to the next snap area.

flackr avatar Sep 18 '24 19:09 flackr

The CSS Working Group just discussed [css-scroll-snap-1] Avoid page scrolling skipping past snappable items, and agreed to the following:

  • RESOLVED: pageUp/pageDown type operations should never scroll by more than a page, unless that's the only valid snap area
  • ACTION: flackr to sketch out a scroll-by-page API
The full IRC log of that discussion <fantasai> flackr: context of scroll buttons , but pre-existing issue with snapping
<fantasai> flackr: currently if you pgae down, the browser can select a snap area that is more than one page away
<fantasai> flackr: even though there's a possible snap area that's not that far away
<fantasai> flackr: I propose putting wording in the spec that, at least for explicit actions like page down, find a snap area that is less than one page away
<fantasai> flackr: so that you don't skip over content
<fantasai> flackr: then I had a further open question of whether we need to solve this for scrolling APIs
<fantasai> flackr: original issue was calling .scrollTo
<fantasai> flackr: to mimic same behavior
<dholbert> fantasai: I think yes, page-up page-down should scroll <= viewport
<dholbert> fantasai: might be exactly equal, but could be <=
<dholbert> fantasai: for programmatic API, those are supposed to [...]
<Rossen6> ack fantasai
<dholbert> fantasai: ...have the same behavior as a person scrolling
<dholbert> fantasai: if scroll-snap is going to interfere with a person scrolling, then programmatic aPI should get that same interference
<dholbert> flackr: if I make a page-down button, as a web developer, and add an event listener that calls scrollTo(currentPos + 85% of scrollport)
<dholbert> flackr: the same problem as pressing pagedown on keyboard exists, that browser could select something further away
<dholbert> flackr: should we try to scroll less?
<dholbert> fantasai: round-down version?
<dholbert> flackr: most expressive version is to say "don't want to scroll more than 1 page"
<dholbert> fantasai: ...
<dholbert> flackr: that's scrollIntoView
<dholbert> flackr: this is for APIs where you specify how far you want to scroll, or a position
<dholbert> fantasai: do we want to add a scrollByPages API?
<dholbert> flackr: that would be reasonable
<dholbert> fantasai: or do we give a "bias-towards" parameter?
<dholbert> fantasai: which of those would make more sense
<dholbert> flackr: I feel like we'd be more likely to do what the dev expects, if the API was specifically "scroll by a page"
<dholbert> fantasai: I guess then we have 2 resolutions?
<dholbert> fantasai: (1) pageUp/pageDown type operations should never scroll by more than a page, unless that's the only valid snap area
<dholbert> PROPOSED RESOLUTION: pageUp/pageDown type operations should never scroll by more than a page, unless that's the only valid snap area
<dholbert> RESOLVED: pageUp/pageDown type operations should never scroll by more than a page, unless that's the only valid snap area
<dholbert> fantasai: (2) we want to add some kind of scroll-by-page type API, to be sketched out by flackr
<dholbert> flackr: I can sketch that out
<dholbert> Rossen6: no need to resolve on that now
<fantasai> ACTION: flackr to sketch out a scroll-by-page API
<dholbert> FWIW there is a scrollByPages API in Firefox, non-standard: https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollByPages

css-meeting-bot avatar Sep 27 '24 21:09 css-meeting-bot

From @dholbert:

FWIW there is a scrollByPages API in Firefox, non-standard: https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollByPages

flackr avatar Sep 27 '24 21:09 flackr

From @dholbert:

FWIW there is a scrollByPages API in Firefox, non-standard: https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollByPages

Indeed; I'm pretty sure this is how we implement the Space/PageDown and PageUp scrolling actions under-the-hood (as calls to scrollByPages(1) and scrollByPages(-1), from Firefox's frontend JS). But the JS API is also exposed to the web, too, for historical reasons I guess.

The scrollByPages() implementation is here if anyone's curious to see it. That function calls a generic C++ ScrollBy() function that's shared by a bunch of programmatic scrolling APIs, and the most relevant piece there for scrollByPages is the GetPageScrollAmount() function, which defines what it means to scroll by a page.

dholbert avatar Sep 30 '24 05:09 dholbert

could we make snapping more conservative and choose the nearer of the points instead of the further?

argyleink avatar Mar 28 '25 18:03 argyleink

The scrollByPages() implementation is here if anyone's curious to see it. That function calls a generic C++ ScrollBy() function that's shared by a bunch of programmatic scrolling APIs, and the most relevant piece there for scrollByPages is the GetPageScrollAmount() function, which defines what it means to scroll by a page.

In this case we want to be a bit smarter about what it means to scroll by a page - in particular, avoiding scrolling further than a full page as a result of snapping to avoid skipping content.

Currently, the algorithm we have in chrome to do this does so by having three constraints - an optimal scroll distance which seems equivalent to GetPageScrollAmount() you pointed to above, as well as a minimum and maximum (a full visible screen of content). Then, this is used to find a snap target that is as close to the optimal distance but not further than the maximum (avoiding skipped content). If this is not possible (e.g. the next snap target is more than a page away), then it will skip further.

flackr avatar Nov 06 '25 15:11 flackr

The simple approach which matches implementations would seem to be to add a granularity to ScrollToOptions. If you passed a page granularity then it could interpret the left and top values as # of pages. Happy to bikeshed on names here and/or other options (e.g. it's common to scroll by lines).

However, given the algorithm for finding a snap area https://github.com/w3c/csswg-drafts/issues/10914#issuecomment-3497842094, if we naively use the given value, you may get different results if you scrollBy({top: 1, granularity: 'page'}); scrollBy({top: 1, granularity: 'page'}); than if you scrollBy({top: 2, granularity: 'page'}); as the first scroll in the first example may have chosen a snap area that is less than or more than the optimal page scroll distance resulting in a different range for the second scroll. I don't think this is necessarily a problem, just wanted to call it out.

If we wanted, we could expose a scroll by operation which took the 3 constraints we use in this algorithm for full generality and customizability, however I think it's fine to leave this as an internal detail of what it means to scroll by pages.

flackr avatar Nov 06 '25 15:11 flackr

@flackr

In the same setup, scroll-marker scrolls to the expected snap-aligned position, but scroll-button consistently scrolls only ~85% of the required distance, leaving the target slide partially visible.

Since both scroll-marker and scroll-button are part of the same CSS Carousel mechanism, they should resolve to the same scroll snap positions and produce identical final scroll offsets.

The current behavior makes scroll-button unreliable, even though the snap points themselves are correct (as demonstrated by scroll-marker).

From an authoring perspective, this inconsistency makes it impossible to rely on CSS carousel controls without JavaScript fallbacks, especially for predictable and accessible navigation.

I can also reproduce this using the demo below. Tested in Chrome Version 143.0.7499.41.

This highlights an inconsistency between scroll-marker and scroll-button, which should both resolve to the same scroll snap positions.

https://codepen.io/shan071/pen/gbrNMXg

shanthambi avatar Dec 17 '25 10:12 shanthambi