:snapped
:snapped-x
:snapped-y
:snapped-inline
:snapped-block
This document is intended as a starting point for engaging the community and standards bodies in developing collaborative solutions fit for standardization. As the solutions to problems described in this document progress along the standards-track, we will retain this document as an archive and use this section to keep the community up-to-date with the most current standards venue and content location of future work and discussions.
- This document status: Active
- Expected venue: CSSWG
- Current version: this document
User Agents privately maintain scroll snap state, leaving web developers unable to update complimentary interfaces accordingly. Scroll snapping is often used for "picker UX", made popular by iOS.
A scroll snap container can have any nested child (just one) snapped to either the x or y axes (or inline/block respectively). In a 2-dimensional snapping layout, a UA may match 2 separate elements with :snapped
. Adding axes to the pseudo-class makes the selector more specific, as to match design or developer intent.
In the case where more than 1 element appear snapped or are snapped on an axis, the 1st of the DOM order is the :snapped
child. An exception is if a child is snapped on the cross axis, in which case it should be :snapped
even if it's not the 1st of the DOM order.
The selector matching timing should synchronize with the same frame the UA has decided on a new snap child. Sooner is better than later, but UA's can decide.
A gallery of scroll snap interactions can help illuminate the CSS UI need, here's the source. Specifically see the
:snapped
demo's for an attempt at deriving the snapped element.
Like :hover
, a developer may find they have caused a loop by changing a property on the snapped item that causes it to unsnap, then potentially snap again. This type of behavior is a concern mentioned by the CSSWG in selectors that depend on layout and is top of mind here in this feature.
This can be mitigated by limiting the loops to a single frame, just like hover. This avoids "tight loops" that could cause major browser problems, it instead is a loose loop which hover has proved for 20 years can be mitigated and avoided through diligence. It's understood there's a challenge here, and the enablement via CSS will pacify all the much worse JavaScript solutions folks attempt today. Hover is a cherished and respected property that has proven to not be as destructive than anticipated.
Worth considering too, only evaluating selectors with :snapped
if the containing scrollport is actually scrolling. If no scrolling is happening, the selectors won't continue firing. Consider it like the :hover
hit test, rather a scroll test. The user, or programmatic scroll from JS, are events that will trigger the selector evaluation, not a constant frame by frame evaluation.
Depending on the scroll snap styles, some (many) snap children may never become snapped. This is a recurring UX and API question, as a scroll gesture may be limited or at the end, and the desired snap target cannot be brought to the snap axis alignment area.
Take the following example, where figure elements want to align to start
but the container is scrolled to the end. Essentially, the last 2 figures will never be snapped:
Unless!
Styles are added so the user can scroll to the last item, aka enough padding in the container so items at the end can align on the axis:
Reduce Javascript responsibility and enable a declarative pattern for updating interface children if they are currently snapped.
- Carousels
- ListViews (great for making selections in long lists)
- Galleries (almost always an active item with additional UI to show)
5 new CSS pseudo-class selectors. 4 for higher specificity targetting, 1 for loose matching:
Name: | :snapped or :snapped-block :snapped-y :snapped-inline :snapped-x |
The loose example, estimated to be the most commonly used, will match all scroll snap targets, regardless of axis. Assuming folks aren't overlapping scroll snap children, this should be quite reliable.
:snapped {
outline: hotpink;
}
Features:
- Allows loose targeting shorthand for authors with distinct axis snap targets
- Allows specific axis selecting in 2D cases
.card:snapped {
--shadow-distance: 30px; /* raised size */
}
:snapped-inline {
outline: 3px solid hotpink;
}
section:snapped-y > header {
box-shadow: 0 .5em 1em .5em lch(5% 5% 200);
border-inline-start-color: hotpink;
}
We (Robert Flack, Tab Atkins and Adam Argyle) believe the value of adding this feature exceeds the implementation difficulties. The lengths at which people go to try and derive the snapped child are never 100% effective and require complex JavaScript algorithms.
We are OK letting :snapped
cycle like :hover
, as long as it's only once per lifecycle update. We believe developers will be responsible with this, as they have with :hover
.
There are no known privacy impact of this feature.
There are no known security impacts of this feature.
For now, please use this Discussion for comments and questions, or a Pull Request to offer changes 🙏