-
Notifications
You must be signed in to change notification settings - Fork 715
[css-ui][selectors][mediaqueries] Expose current scrolling direction #6400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Because we do have both physical and logical dimensions to deal with in these scenarios, I wonder if it's worth recommending this as a functional pseudo-class that takes a direction/state of the scrolling element as a parameter?
I've found myself disabling In general, I do wonder about some circularity when this pseudo class matches and then layout/flow changes so the scrollHeight/scrollWidth is changed in a way where the browser must manipulate the scroll position and now the pseudo class wouldn't match? |
This was just mentioned as an alternative solution to #5670 :) |
Recently also came up in a discussion where an author wants to hide certain toolbars as the user scrolls down, but re-show those toolbars once they scroll back up again - a pretty common pattern which was already acknowledged as such by the WG here A solution I was thinking of, was to use media queries. @media (scroll-direction: block | block-start | block-end | inline | inline-start | inline-end ) { … } Values are:
When you reach the start or end of a scroller, the values would still keep on being truthy: say you're at the bottom of the page and are overscrolling, By using a MQ, one could also easily nest them once css-nesting lands: #navbar {
position: sticky;
top: 0;
transition: transform 0.25s ease-in;
/* Hide when scrolling towards the bottom */
@media (scroll-direction: block-end) {
transition-delay: 250ms;
transform: translateY(-95%);
}
} |
What if the developer wants to only bring in or out a toolbar after a delay or certain number of pixels scrolled? For that reason I think an event might work better than a CSS feature in practice. |
Authors can use
This would not be covered by the proposed feature, but would be something for the future scroll-triggered animations spec (not to be confused with the current scroll-linked animations spec). I do expect implementers to include some cleverness when detecting the scroll-direction, such as using some kind of (small) threshold before triggering a direction change. Thinking out loud here, it would be something that takes both distance and time into account. Say that the scroll-detection runs every In JS code (line 9): const THRESHOLD = 10;
let curScrollPosition = 0, prevScrollPosition = 0;
setInterval(() => {
// Capture current scrollPosition
curScrollPosition = …;
// Bail out if scrolled only for a tiny amount
if(Math.abs(prevScrollPosition - curScrollPosition) <= THRESHOLD) return;
// @TODO: draw rest of the owl ..
// Get ready for next tick
prevScrollPosition = curScrollPosition;
}, 100); |
One downside of using a media query for this is that you're just targetting the viewport with it. You can't target a different scroll container element within your page. Though maybe this could somehow be achieved with container queries as well. Regarding the pseudo-class approach, I like @jonjohnjohnson's idea of functional pseudo-classes. With those, the threshold mentioned by @chrishtr could be provided as a parameter of that function. That could then look like this: header {
transition: transform 0.25s ease-in;
transform: translateY(-100%);
}
:scrolling(block-start, 20px) > header {
transition-delay: 250ms;
transform: translateY(0);
} To me, both ideas seem valid solutions for the use-case provided. Sebastian |
As there are now two very different proposals, I generalized the subject and added the related labels. Sebastian |
This seems like a good candidate for state queries, @mirisuzanne. .sticky-header {
position: sticky;
top: 0;
transform: translateY(0%);
transition: transform 0.5s ease-in-out;
transition-delay: 0.25s;
}
/* Move sticky header out of view when scrolling the page down */
@container body state(scrolling and scroll-direction: block-start) {
.sticky-header {
transform: translateY(-100%);
}
} (* insert potential confusion about scroll-direction here … does it mean the scroller is advancing to that position, or is the content moving to that position? *) |
I agree this could work with state queries. The main reason to go that direction is if we need to enforce a separation between the subject of the selector and the element being scrolled. I think that might be useful to enforce here - since we don't want to allow changing overflow based on scrolling? In terms of syntax bike-shedding: I think terms like forward and backward/reverse provide some more clear sense of 'movement direction' rather than naming an edge. And those can still be combined with logical axis. I'm also not sure that we need to query 'scrolling' separate from the scroll-direction. I'd imagine something like: @container optional-name state(scrolling) { /* boolean for any scrolling */ }
@container optional-name state(scrolling: block) { /* any block scrolling */ }
@container optional-name state(scrolling: block forward) { /* scrolling 'down' in the default case */ }
@container optional-name state(scrolling: block reverse) { /* scrolling 'up' in the default case */ }
/* etc for inline axis */ |
To my own surprise, knowing the active scroll direction (and speed) is made possible thanks to Scroll-Driven Animations: https://www.bram.us/2023/10/23/css-scroll-detection/ As detailed in the article it’s not entirely optimal, so in the end a proper solution would still be needed. The outlined hack relies on a parent-child relationship to make it work, rhyming with the earlier suggestion of tucking this feature into state queries. |
Would the concepts of Proposed in Scroll Triggered Animation Issue animation-play-state: toggle(entry 50%); Potential use in scroll direction animation-play-state: scroll(block forward);
/* if we want 2 animations; 1 to show it and 1 to hide it */
animation-play-state: scroll(block forward), scroll(block reverse); |
Scrolling can be triggered by an element above the current scroll position changing size. Would these selectors trigger then? If so, we could have problems with circularity. |
I see there were several proposals so far using scroll state container queries, media queries and pseudo classes, giving the latest changes to scroll state container queries, I assume it would be reasonable to use scroll state container queries similar to what @mirisuzanne
Edit: Renamed |
Agenda+ to discuss adding the scroll query from the preceding comment. We probably want to bikeshed the name a little so it doesn't repeat An open question is when it should match |
Syntax: scroll-direction: none | any | top | right | bottom | left | block-start | inline-start | block-end | inline-end. Open discussion: w3c/csswg-drafts#6400 Proposed resolution: w3c/csswg-drafts#6400 (comment) Chromestatus entry: https://chromestatus.com/feature/5083137520173056 Bug: 414556050 Change-Id: I849a40a7ec3340fac77a1d15b270d09c12e56289 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6482570 Reviewed-by: Rune Lillesveen <[email protected]> Commit-Queue: Munira Tursunova <[email protected]> Cr-Commit-Position: refs/heads/main@{#1453839}
Current set of If we add this I think we should just call it My understanding was that |
(I left (some form) of this comment also as a reply on the explainer PR, but repeating it here for visibility) There are a bunch of demos I built for https://www.bram.us/2023/10/23/css-scroll-detection/ that depend only on the active scroll direction (and also scroll speed). Once you stop scrolling, the effect no longer applies. For example:
The “Hidey Bar” then OTOH is the typical example that depends on the last non- So I think the API should support both options, as there are use-cases for both. Some ideas:
Personal preference for 1 as it seems more easy for authors 2 would become a keyword-mess. |
Actually, you don’t need to store the scroll-direction into a custom prop (+ then use style queries to respond to it) but can do this instead to respond to the last non- @property --translate {
…
}
@container scroll-state(active-scroll-direction: down) {
header {
--translate: -100%;
}
}
@container scroll-state(active-scroll-direction: up) {
header {
--translate: 0;
}
}
header {
translate: 0 var(--translate);
transition: --translate 0.25s 0s ease;
}
@container scroll-state(active-scroll-direction: none) {
header {
transition: --translate 0s calc(Infinity * 1s) ease;
}
} Still a bit complex though (especially when having multiple values that need to respond to it), so I still prefer to have this built-in. |
Bikeshedding...
Or...
|
@mirisuzanne |
Both of these demos, and many of the others which only have an effect while scrolling also use the velocity, or at least would benefit from using the scroll velocity. This suggests to me that a scroll driven animation would better suit these use cases, e.g. .tilt {
animation: tilt linear both;
animation-timeline: scroll-speed();
/* Getting very creative with units here. */
animation-range: -100px/s 100px/s;
} I suspect a somewhat common use case, given sufficient delays before returning to the none state, might be to recreate overlay scrollbars (i.e. temporarily show a scrollbar thumb when there has been recent scrolling). However this is likely a bit of a usability challenge.
The hidey bar case is an extremely common use case in the wild (typically implemented in JS), and the one for which this API was designed. We should focus on solving this use case well - for which we need to have the last scrolled direction persisted. It may be nice / handy if we had the option to handle the is actively scrolling case, however, I haven't seen too many use cases in the wild of this which aren't also velocity dependent and would need more than the scroll direction. |
+1 to @flackr. That's usually the case, so probably solving velocity will solve more cases. Furthermore, if we solve velocity/delayed we should probably do so for pointer-driven animations as well. Though there are use-cases where direction is used without depending on velocity. These are usually show/hide of fixedpos UI, usually on mobile. For example: show/hide scroll-to-top button or header when scrolling back up, kinda like the addressbar does it. |
Right, solving #7059 would mean that a VelocityTimeline could be combined with this delay to implement a an effect that persists past the end.
Yes, this makes sense to me.
Right, in the vast majority of these cases, developers don't want the fixed pos ui to go away when scrolling stops - otherwise it would prevent a usability nightmare. For cases where the bar scrolls in as the user scrolls we have talked about this in #5670 where I proposed that a tweak to position sticky could work. |
The CSS Working Group just discussed
The full IRC log of that discussion<fantasai> TabAtkins: We have a whole batch of scroll-related container queries<fantasai> TabAtkins: There are a lot of use cases in this space, particularly about knowing the last known scroll direction <fantasai> TabAtkins: Two questions are: what's most recent scroll direction. Other is what is current scrolling direction. <fantasai> TabAtkins: Hidey UI is like this on mobile <fantasai> TabAtkins: Some discussion about velocity and other things that might be better to handle as scroll animations <smfr> q+ <fantasai> TabAtkins: There's an explainer in the issue <fantasai> smfr: In general this seems reasonable. Went through explainer, didn't see any explanation of rubber-banding <fantasai> smfr: I don't think we want to expose that change in direction to web content <fantasai> smfr: Also curious, what happens for scrolls that result from content changes <fantasai> smfr: i.e. scrolls that scroll anchoring tries to prevent <fantasai> smfr: Could end up with circularity, or oscillating between two states -- we see this often with JS-driven changes <fantasai> TabAtkins: I think the answer should be that we base this on user-driven scrolling, so not script-driven or content-driven <fantasai> TabAtkins: that would avoid these circularity issues <flackr> +1 <Rossen8> ack smfr <fantasai> TabAtkins: And it's close to impossible to detect in JS, could address directly this way <fantasai> TabAtkins: Don't think there's a use case for script scrolling for these things <fantasai> smfr: Seems surprising, because wouldn't an animated scroll to 0,0 want to show/hide these bars? <fantasai> TabAtkins: possibly, but at that point you're scripting, so why not just script it <flackr> I think at least scroll anchoring should not count, programmatic scrolling may be interesting <fantasai> [missed] <fantasai> Rossen8: what about autoscrolling? <fantasai> TabAtkins: that's still scrolling <ntim> q+ <fantasai> TabAtkins: Wanted to ask if group wants to pursue this idea <fantasai> ntim: Not sure I'm convinced by answer that if we invoke script we can just invoke more script <fantasai> ntim: The way you write the query is @container scroll-state(), and if you invoke script and you can't change that state, then you have to duplicate your CSS to support a class <fantasai> ntim: And also have your scroll direction query <fantasai> ntim: It would be duplicated into 2 places, if you invoke script to manipulate the state <flackr> I think we can explore this with an example of how this would be done <fantasai> TabAtkins: Question of what to do about script-driven scrolls is something to think more about, so let's discuss in the issues <Rossen8> ack * <Rossen8> ack ntim <Rossen8> ack fantasai <TabAtkins> fantasai: you don't necessarily have to introduce a class, could do it with a property <TabAtkins> fantasai: and put that in the CQ <flackr> q+ <fantasai> flackr: Directionally, should we consider whether "last scrolling direction" is thing we should pursue first, because it comes up in all use cases, and active scrolling direction can be pursued afterwards? <fantasai> TabAtkins: Problems are easier and more common for recent scroll vs active scroll <fantasai> flackr: Agree <fantasai> flackr: Active scroll might need extra capabilities <fantasai> Rossen8: any objections to this direction? <TabAtkins> fantasai: We need to figure out what to do with active scroll, so we're designing a coherent syntax space fo rwhen it is tackled <fantasai> RESOLVED: Working on scrolling queries |
A common UI pattern on the web is to hide/show or partially collapse some content based on whether the user is currently scrolling up or down. I'm not particularly fond of this UI pattern, but it is widespread and is often necessary for long pages on mobile screens.
This currently requires JavaScript scroll listeners to change a class on the body or other scroll container in order to trigger the alternate layout of the collapsible headers/footers (and that's assuming it isn't done entirely in JS-framework state propagation!)
Browser-managed scrolling direction pseudoclasses on the scroll container would eliminate the need for many JS scroll listeners. Something like
body:scrolling-forward
vsbody:scrolling-backward
for primary (block) direction scrolling, and some other logical names for cross/inline scrolling. Maybe also some way to distinguish when scrolling has stopped for a while.Pseudoclasses could also enable browser-managed heuristics for better detecting when the scroll direction has changed (or stopped), based on the device scroll mechanism (touch gesture, keyboard, mouse wheel) or user accessibility customizations. For example, a user with shaky fingers might want to customize how much they need to scroll in a given direction before the layout shifts on them.
(I don't have the capacity to work on this; just throwing it out there in case someone else wants to pick it up!)
The text was updated successfully, but these errors were encountered: