Open AmeliaBR opened 3 years ago
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?
body:scrolling(bottom)
currently being scrolled toward bottom edgebody:scrolling(inline-start)
currently being scrolled toward inline-start edgebody:scrolling
currently being scrolled in any directionI've found myself disabling :hover
,:active
, and even animation stylings depending on the user currently scrolling. The last example could help in these cases.
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 :) One shortcoming I see with an on-or-off pseudoclass is that you aren't literally scrolling out the top bar with e.g. your finger, i.e. it doesn't move with your finger as you move it up and down. It merely switches to a state once your scrolling reaches a threshold, and the best you can do as far as I can tell is give it a transition, which does not correlate to your finger movements.
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:
block
: Scrolling in the block direction (aka to the top or bottom in a “default” ltr+toptobottom scenario)block-start
: Scrolling in the block-start
direction (aka to the top in a “default” ltr+toptobottom scenario)block-end
: Scrolling in the block-start
direction (aka to the bottom in a “default” ltr+toptobottom scenario)inline
: Scrolling in the inline
direction (aka to the left or right in a “default” ltr+toptobottom scenario)inline-start
: Scrolling in the inline-start
direction (aka to the left in a “default” ltr+toptobottom scenario)inline-end
: Scrolling in the inline-end
direction (aka to the right in a “default” ltr+toptobottom scenario)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, @media (scroll-direction: block-end)
would still be in effect.
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.
What if the developer wants to only bring in or out a toolbar after a delay
Authors can use transition-delay: 250ms;
for that.
… or certain number of pixels scrolled
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 100ms
(just grabbing a number here), it would need to check whether a certain distance THRESHOLD
was scrolled, and only then do the direction.
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.
A benefit of this approach is that a media query is independent of the element(s) you actually want to style. Another benefit is that authors can also use this query in JavaScript via window.matchMedia()
.
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 Add animation-trigger for triggering animations when an element is in a timeline's range
from https://github.com/w3c/csswg-drafts/issues/8942 potentially apply here as well? It seems to have some overlap. It would definitely not have as much power as a state or media query, but if the main use case is triggering animations it might be sufficient?
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);
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!)