w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.47k stars 659 forks source link

[css-scroll-snap-1] Compat between webkit and blink/gecko regarding "implicit" scroll boundary snap positions #4037

Closed jonjohnjohnson closed 3 years ago

jonjohnjohnson commented 5 years ago

https://drafts.csswg.org/css-scroll-snap-1/#choosing

Regarding https://codepen.io/anon/pen/XLwybE

scroll {
    overflow-y: auto;
    height: 100px;
    scroll-snap-type: y mandatory;
}
block {
    height: 50px;
    margin: 150px 0;
    scroll-snap-align: center;
}
<scroll>
  <block></block>
</scroll>

All major vendors, of course, load in with a scroll position of 0. However, webkit is the only implementation where once scrolled, a user is allowed to snap back to the 0 scroll position, where "implicit" snap positions are created at the scroll boundaries.

Aside from language like...

A naïve algorithm for selecting a snap position can produce behavior that is unintuitive for users, so care is required when designing a selection algorithm.

User agents must ensure that a user can “escape” a snap position

...the current spec mostly eludes to matching the blink/gecko behavior. If no elements create a snap position for the "scrollmin" or "scrollmax" scroll positions of their scroll container, then a user can never snap/resolve to a scroll boundary without being "bounced" back, almost with pseudo-rubberbanding visual.

https://bugs.chromium.org/p/chromium/issues/detail?id=953979 https://bugzilla.mozilla.org/show_bug.cgi?id=1545316

Are blink/gecko correct? Should the spec be made clearer? Or changed to agree with webkit?

justingrant commented 5 years ago

TL;DR: creating implicit scroll positions at scrollmin/scrollmax breaks a popular web-performance technique: Virtual Scrolling. If we don't standardize on "no implicit scroll positions" (the current Blink/Gecko behavior), then IMHO we'd need another way to enable Virtual Scrolling to work with scroll snapping. Details are below.

The current WebKit behavior (creating implicit snap positions at scrollmin/scrollmax of the container) breaks "Virtual Scrolling" implementations like Facebook's react-window that use JavaScript to simulate a long, scrollable list of thousands of items without the overhead of creating thousands of DOM nodes. This technique has been popular for many years because it's a good way to reduce the initial render time (and client RAM footprint and server data-fetching) of long lists without the complexity of having to roll your own scrollbar UX and scroll behavior, which is really hard to get right and is expensive to maintain. Especially on touch devices.

Virtual Scrolling implementations work like this:

You can see an example of Virtual Scrolling here: https://codesandbox.io/s/happy-architecture-m7pn5. The top list uses Virtual Scrolling.

Unfortunately, WebKit's behavior that defines implicit snap positions at the scrollmin/scrollmax breaks these virtual list components if scroll snap is enabled. A fast (flick/momentum) scroll on Safari will scroll to the end of the content before the Virtual Scrolling script has a chance to place new Item elements at the user's new scroll position. Repro steps are here: https://github.com/bvaughn/react-window/issues/290.

I can think of many possible ways to address this problem:

  1. Standardize Blink's/Gecko's behavior: don't create implicit scroll points at scrollmin/scrollmax. I like this option because it seems like the easiest way for developers to add scroll snapping to a Virtual Scrolling list.
  2. Add a new CSS property for scroll containers, e.g. scroll-snap-implicit or scroll-snap-edge with possible values start, end, both (current Blink/Gecko behavior), and none (current Blink/Gecko behavior). I'd lobby for the default to be none because it'd minimize changes needed to existing code, but adapting existing code to a default of both wouldn't be a huge problem.
  3. Extend scroll-snap-type with the values from (2) instead of adding a new property, e.g. scroll-snap-type: x mandatory none or scroll-snap-type: x mandatory both.
  4. Add a new CSS property to describe how children of a parent element are expected to be populated, e.g. child-population with values like: a) sparse-dynamic - virtual scrolling list (don't create implicit scroll points at scrollmin/scrollmax) b) dynamic - dynamically populated but non-virtual list, aka "infinite scroll" like on Twitter. c) static - no dynamic population expected d) default- no assumption about how child elements will be populated The idea would be to provide UAs with higher-level metadata about how a container is expected to be populated, and UAs could optimize various features accordingly (e.g. batched rendering for dynamic lists). Honestly this feels like overkill to fix a scroll snapping problem, but I'm including for completeness.
  5. Some other workaround I haven't thought of that leverages existing CSS properties.

I'm sure there are other options too. The only option that is IMHO not OK: requiring implicit scroll points at scrollmin/scrollmax (the current WebKit behavior) without some other way to ensure that Virtual Scrolling containers will work with scroll snapping enabled.

What do you think?

jonjohnjohnson commented 5 years ago

@justingrant but do you have any issue with webkit creating an implicit scrollmin snap position? The position where the scroll container loads in on? Do you really think that should be configurable having seen the ux when not creating that implicit position in blink/gecko?

justingrant commented 5 years ago

Hi @jonjohnjohnson - My main concern is finding a solution that won't break virtual scrolling implementations. An implicit scrollmin snap position breaks virtual scrolling lists when the user is scrolling backwards, so I wouldn't support standardizing the WebKit behavior (even just for scrollmin) unless there were some way to turn it off so that virtual scrolling would work OK.

The gecko behavior is definitely weird the way it scrolls all the way to the end and snaps all the way back. That should definitely be fixed. But I think the "fix" could just match the blink behavior where scrolls are not allowed to escape the first/last snap-aligned child element. I'm not saying it's good UX to be unable to return to the initial on-loaded scroll position, but I'm also not sure that this is a common-enough use case to be worth worrying about.

Other than virtual scrolling, what are common use-cases that would combine scroll snapping with needing to navigate beyond the first/last snapped element? If we think that developers making mistakes in CSS is a common reason we'd see this, then I'd also support a solution that would auto-scroll to the first snap position instead of starting from zero. This would eliminate the "can't go back home" problem and would make CSS bugs easier to find and fix because the bad UX would show up immediately on load instead of requiring user interaction. Doing this might be expensive to implement or expensive at runtime though; I'm not familiar enough to know.

If there are common use-cases (not just developer errors) where users need to scroll to empty space in a container or to un-snap-aligned child elements, then I'd support an option to enable the WebKit implicit scroll positions, as long as there's a way for virtual scrolling implementations to turn them off.

jonjohnjohnson commented 5 years ago

@justingrant I feel as though you don’t realize that scroll containers load in on the “scrollmin” position in the first place? So what’s a scenario you can imagine where the initial scroll position should not be reachable after scrolling, even in what I consider the uncommon case of virtual scrolling? You refer to “empty space” but there can be content in the “space” that just hasn’t declared a choosable/reachable snap alignment based upon the size of the snapport, content, and aligned edge? Have you scrolled the original posts test case?

But I think the "fix" could just match the blink behavior where scrolls are not allowed to escape the first/last snap-aligned child element.

I don’t think you’re realizing that the load in scroll position is “escaped” from what you propose, on top of being hostile towards users.

justingrant commented 5 years ago

Hi @jonjohnjohnson - Thanks for quick reply! Other than the priority of virtual scrolling, I suspect that we're pretty much in agreement on most points. Here's some responses and notes, let me know what you think.

I feel as though you don’t realize that scroll containers load in on the “scrollmin” position in the first place?

Nope, I understand this. Every time I have to write code to set a non-zero scroll position on page load, I'm reminded of the default. ;-)

So what’s a scenario you can imagine where the initial scroll position should not be reachable after scrolling, even in what I consider the uncommon case of virtual scrolling?

I don't know any case, including virtual scrolling, where a developer would deliberately want to make the initial position unreachable for the user. In a virtual list, users can scroll back to the initial position. The problem is a race condition: WebKit will scroll all the way back to the beginning or end of the list any time you flick/momentum-scroll because, at the time the user flicks, the list is mostly empty other than a few items under and around the scrollport.

For example, imagine a user is currently viewing the 20,000th item in a virtual list and they want to go back 20 items. So they "flick right" hard enough to momentum-scroll for 20 items' width. But in the DOM there are currently only 2 items to the left of the scrollport, which means that a 20-item-width scroll will snap to the next snap point: scrollmin. Then the content starts scrolling in a blur for a few seconds, with display flashing as the virtual scrolling script madly tries to add new elements in front of the scroll position. Finally, the user ends up at the first item. Instead of scrolling 20 items, it scrolled 20,000. On blink, this works fine except momentum scroll is relatively slow and high-friction unless you add a lot more buffer items around the scrollport.

what I consider the uncommon case of virtual scrolling

I don't have any stats on runtime usage, but here's a few data points about usage from the developer side. What level of developer usage do you think qualifies as not-uncommon?

Every major JS framework has several popular implementations of virtual scrolling because it's usually the only performant and easy-to-implement way to display long lists that are scrollable. We may not hear much about this usage because much of it is in "enterprisey" reporting or line-of-business apps. But it's fairly common, esp. in newer B2B/SaaS and IT tools that frequently have a need to show scrollable lists and reports with 1000+ rows.

You refer to “empty space” but there can be content in the “space” that just hasn’t declared a choosable/reachable snap alignment based upon the size of the snapport, content, and aligned edge?

Yep, understood. Was using "empty space" as a shorthand for something like "no reachable snap-aligned elements" because I was assuming that "no elements" would be the most common reason.

Do you think the "no reachable snap-aligned elements near scrollmin/scrollmax" case is a common, intended case? Or is it likely to be a developer error like forgetting to snap-align some child elements?

Have you scrolled the original posts test case?

Yep! I agree 100% with you that that test case is confusing UX on Chrome. I'm not opposed a solution that makes that case better, as long as that solution doesn't break virtual scrolling.

While we're talking test cases, have you scrolled the top list on https://codesandbox.io/s/happy-architecture-m7pn5 on Safari? Let me know if you can think of another way to prevent the awful UX that happens when you flick/momentum-scroll that list on Safari.

I don’t think you’re realizing that the load in scroll position is “escaped” from what you propose, on top of being hostile towards users.

Nope, I get it. It's weird. I suspect it's uncommon, but it's still weird to be unable to return home. I'd definitely support a solution that fixes this case as long as it won't break virtual scrolling.

BTW, one other nice thing about the blink behavior is that achieving the webkit behavior is trivially easy for a developer or designer. On blink, to enable webkit-like snapping at scrollmin/scrollmax, simply put a snap-aligned element at scrollmin/scrollmax. No script required. For the reverse (preventing webkit from snapping at scrollmin/scrollmax) the only option, as far as I know, is to write a Javascript replacement for CSS scroll snapping. This is orders of magnitude harder... and hurts performance and UX too.

jonjohnjohnson commented 5 years ago

While we're talking test cases, have you scrolled the top list on https://codesandbox.io/s/happy-architecture-m7pn5 on Safari?

Yes, in webkit as well as blink. I find the behavior for the third scroller strange in both. Both seem broken. Neither seems to tell a user that something is going all that well. Which I imagine is simply a byproduct of pigeon-holing virtualization into scroll-snapping.

Since I am guessing the use of a simple overflow: scroll is immensely more common than virtualized scrolling, even with all the github stars, I think the default behavior shouldn’t cater to the needs of virtualized scrolling.

As far as scroll-snapping behavior for content that is programmatically curated like what happens with virtualized scrolling, I have found that writing scroll/touch handlers on top of pointer events or things like hammer.js and the transform serves those gestures far better. I’ve written variations of those handlers many times and find better perf than what I’m seeing in https://codesandbox.io/s/happy-architecture-m7pn5.

Yep, understood. Was using "empty space" as a shorthand for something like "no reachable snap-aligned elements" because I was assuming that "no elements" would be the most common reason.

I think with responsive scroll containers and content “no elements” wouldn’t be the common reason as much as the element that defines the nearest scroll boundary alignment is attempting to align to the opposite edge of the scroll boundary, but is too large. Also, if the first piece of content has a margin on that start boundary you’re then asking to rework the other stylings to account for scroll-margin or putting a pseudo element in to add in an extra “scrollmin” alignment? You might be thinking of scroll-snap in the limited scope of uniform lists, but it has many more uses.

justingrant commented 5 years ago

As long as implicit snap positions at scrollmin/scrollmax can be turned off somehow via CSS or JS, I don't have a strong opinion about the default behavior.

Sounds like we're not going to agree on the importance of supporting scroll snap with virtual scrolling implementations. Thanks for your thoughts here; you've helped me understand other facets of this problem.

I have found that writing scroll/touch handlers on top of pointer events or things like hammer.js and the transform serves those gestures far better

Yep, that's what I'll have to do on my current project. Even if WebKit provides a no-implicit-edge-snapping option, it'll be too late for a Summer '19 release that I'm committed to. It'd still be good for other developers and my own future work to just be able to rely on CSS only for snapped scrolling.

if the first piece of content has a margin on that start boundary you’re then asking to rework the other stylings

Yep, agreed. Still orders of magnitude easier than building JS-based scrolling from scratch, though. But regardless, I'm supportive of the option you're looking for, and OK if it's the default as long as there's a way to opt out.

I think with responsive scroll containers and content “no elements” wouldn’t be the common reason as much as the element that defines the nearest scroll boundary alignment is attempting to align to the opposite edge of the scroll boundary, but is too large.

Makes sense, thanks for clarifying. Is "element too big for desired scroll snap" also a problem for elements in the middle, or only at the edges of the content? If the former, how would the developer fix it?

I find the behavior for the third scroller strange in both.

Yep, "strange" was the point of the third example. It's just a plain-HTML/CSS (not virtual) list meant to highlight the problem behavior, not to represent realistic HTML that anyone would want to use. Only the top list is virtual.

jonjohnjohnson commented 5 years ago

Is "element too big for desired scroll snap" also a problem for elements in the middle, or only at the edges of the content?

Even if mandatory is set, I believe all implementations still allow for content be reached in the middle based upon language here -> https://drafts.csswg.org/css-scroll-snap-1/#choosing. But again, I’m concerned about hostility against reaching “home” as you say, especially when a box’s content edge might be pushed further inside the scrollers edge with margin, so the top of the text doesn’t meet/smash against the scroller edge.

Yep, "strange" was the point of the third example. It's just a plain-HTML/CSS (not virtual) […] Only the top list is virtual.

The top list behaves quite normally for me in webkit, actually better than blink. In blink, I can only scroll a few past a few items at a time no matter how “hard” I attempt a scroll to move me far in either direction. In webkit, it’s affording me short scrolls to snap a few forwards or backwards as well as a “hard” scroll to reach the beginning or the end. Somehow webkit feels more intuitive for gestural (track/touch) scrolling, though I have not attempted wheel scrolling. And in webkit even if I’m scrolling quite far, I’m still able to stop mid snap anywhere want simply with a new scroll gesture and I’m not forced to resolve at the scrollmin/scrollmax. In blink it feels quite stunted that I cannot scroll far no matter how “hard” I attempt, though funnily enough, I can grab the scrollbar and go all the way from min to max with ease. Ya, if anything blink feels quite off in the top row compared to webkit (12.1.1).

justingrant commented 5 years ago

In webkit, it’s affording me short scrolls to snap a few forwards or backwards as well as a “hard” scroll to reach the beginning or the end

Yeah, it's the "hard scroll to reach the beginning or the end" that's the problematic UX on webkit. The second scroller (which is almost the same HTML/CSS as the top one, except it's static HTML not virtual) has IMHO expected behavior, which is that the hardest possible scroll (on my MacBook Pro trackpad) will scroll 80-100 items on both blink and webkit. But the particular project I'm working on has 10K+ items in the virtual list, so having a mild flick go all the way to the end is really unexpected for the user.

The blink behavior on the top scroller is definitely slower because it won't scroll beyond the items that are already in the DOM. This behavior is fine for my current project but may not be ideal for others, but that can probably be worked around by adding more "buffer" elements on either side of the scrollport in the virtual list implementation.

majido commented 5 years ago

Paging in spec editors @fantasai @tabatkins

I agree that this is an interop issue.

Per current spec adding implicit start/end snap position is not correct. This is what Blink/Gecko implemented but safari does add implicit snap position at start/end of scrolling content.

Here is some initial thoughts:

In any case, I think we should drive to a consensus here since if we don't remove the existing load at 0 behavior the issue will get harder to deal with over time.

justingrant commented 5 years ago

@majido - The approach you outline above sounds right. FWIW, it was very annoying to waste time this summer rolling my own JS scroll snapping implementation when the as-is web platform was so close to working properly for virtual lists. I look forward to being able to depend on the platform for snapping in future projects!

tabatkins commented 5 years ago

Agree with Majid on all points. ^_^ In particular that if 0 isn't a valid scroll position due to snapping, it shouldn't initialize to that offset.

I also don't think we should auto-add start/end snap positions. The thread has given good reasons to avoid it, and it's easy to ensure there is a useful snap point there if you need it.

fantasai commented 5 years ago

Also agree with @majido on all points. FWIW, these implicit snap points were explicitly discussed rejected, see issue tracking https://drafts.csswg.org/css-scroll-snap-1/issues-by-issue#issue-44 tpac minutes https://lists.w3.org/Archives/Public/www-style/2015Nov/0266.html

Since the spec does not define that they exist, I think it should be sufficiently clear that, per spec, they don't exist... If that needs further clarification, we can add a note. “Note: BTW! The only snap positions that exist are the ones defined by scroll-snap-align; the UA can't just make up new ones wherever it wants because it feels like it.” :)

As for the initial scroll position, the definition of mandatory makes it clear that it's not valid to hang out there if it's not a valid snap position. “If specified on a scroll container, the scroll container is required to be snapped to a snap position when there are no active scrolling operations.” https://www.w3.org/TR/css-scroll-snap-1/#snap-strictness (For proximity snapping, it's OK: it's not a snap position, but it's valid to leave the scroller at such positions.) The general principle is, if the user can't scroll to a position and stop there, neither can the UA.

I don't think there's anything left for the spec to clarify here, so afaict unless we need to change something due to Web-compat we should all be aligning implementations on https://github.com/w3c/csswg-drafts/issues/4037#issuecomment-538060075 @majido Can you take the lead on trying to fix the initial load position in Chrome?

majido commented 5 years ago

@fantasai thanks for additional background.

Yes we have already started the work to implement this in Blink as part of snap-after-layout feature. Once we are have that implemented we will do the work to find out if we can also safely enable it for initial load as well. I will report back with our findings.

majido commented 4 years ago

To close the loop on this, our stats suggest that the initial load snapping is very safe change to make. Basically a very very small number of snap containers would scroll as a result of initial load snapping which means by making the scroller snap on initial load we should not see lots of visible behavior change.

So we are proceeding with this change in Chrome M81.

majido commented 4 years ago

BTW here are the tests we have added to WPT for this feature as well.

majido commented 4 years ago

Snap-after-layout is now shipped in Chrome M81 (blog post) which also includes snap on initial load.

fantasai commented 4 years ago

Agenda+ to close out this issue as no change to the spec based on Majid's reasoning (which Tab and I fully agree with) in https://github.com/w3c/csswg-drafts/issues/4037#issuecomment-538060075 and subsequent implementation and compat data from Chrome.

justingrant commented 4 years ago

Is there a timeline for when this change will make it into WebKit and Safari?

fantasai commented 4 years ago

@justingrant Pretty sure “Apple does not comment on future product releases.” But you might make sure there's a bug filed against WebKit.

justingrant commented 4 years ago

I searched through WebKit's Bugzilla since 2014 and was unable to find a bug report that looks like this issue. But admittedly I may not know the right terms to find the correct match.

@majido - in 2019 you helpfully opened a Safari bug about a different scroll snap issue (https://bugs.webkit.org/show_bug.cgi?id=197744#c0). Would you be willing to open one for this issue?

I'd do it myself but:

I'm happy to add comments to a WebKit bug with use cases and other justification, but it'd be great to have someone more authoritative than me to kick it off.

Would that work? Thanks!

fantasai commented 4 years ago

@justingrant I think you may be underestimating the weight of a bug report from a Real Live Web Developer with a Real Use Case, particularly one backed by both the spec and a Chrome implementation. ;)

smfr commented 4 years ago

WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=216882

css-meeting-bot commented 4 years ago

The CSS Working Group just discussed [css-scroll-snap-1] Compat between webkit and blink/gecko regarding "implicit" scroll boundary snap positions, and agreed to the following:

The full IRC log of that discussion <dael> Topic: [css-scroll-snap-1] Compat between webkit and blink/gecko regarding "implicit" scroll boundary snap positions
<Rossen_> https://github.com/w3c/csswg-drafts/issues/4037#issuecomment-590372507
<dael> github: https://github.com/w3c/csswg-drafts/issues/4037#issuecomment-590372507
<dael> Rossen_: Reopened with additional thoughts
<fantasai> https://github.com/w3c/csswg-drafts/issues/4037#issuecomment-538060075
<dael> fantasai: Prop is close no change based on majid's reasoning based on ^ comment
<fantasai> per that comment, close as no change
<dael> fantasai: Means WebKit needs to fix to not have implied start and stop points. That's how I propose to close.
<dael> Rossen_: I see smfr added a tracking bug?
<dael> smfr: This is on my radar and fine
<dael> Rossen_: Objections to close no change?
<dael> RESOLVED: Close no change based on reasoning in https://github.com/w3c/csswg-drafts/issues/4037#issuecomment-538060075