Skip to content

Commit a088cbf

Browse files
authored
[css-contain-4] Move Scroll State Container Queries explainer into repo #6402 (#9865)
* [css-contain-4] Move state queries explainer #6402 Resolved to move explainer from lilles.github.io in issue #6402 * state() -> scroll-state() --------- Co-authored-by: Rune Lillesveen <futhark@chromium.org>
1 parent f15dd4c commit a088cbf

File tree

1 file changed

+295
-0
lines changed

1 file changed

+295
-0
lines changed
+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# CSS Scroll State Container Queries Explainer
2+
3+
## Introduction
4+
5+
Level 3 of CSS Containment introduces Container Queries for querying size and
6+
style of containers to style their descendants depending on layout and computed
7+
styles respectively. There are requests to allow to query other states
8+
associated with a container, in particular various scroll based states.
9+
10+
## Background and Motivation
11+
12+
Certain scroll-based states are only available for styling through javascript
13+
events. These include whether a sticky positioned element is stuck in the sense
14+
that it has an position offset applied, whether a scroll-snap-aligned element
15+
is snapped to its scroll snap container or not. Exposing these states in CSS
16+
would make it simpler for authors to create effects on snapped elements, or
17+
have sticky positioned elements change their appearance when stuck.
18+
19+
This article talks about the need for support for styling scroll-snapped
20+
elements:
21+
[https://web.dev/state-of-css-2022/#scroll-snap-features-are-too-limited](https://web.dev/state-of-css-2022/#scroll-snap-features-are-too-limited).
22+
In particular, see: [https://web.dev/state-of-css-2022/#snap-target](https://web.dev/state-of-css-2022/#snap-target).
23+
This article also talks about the lack of being able to style snapped elements,
24+
using intersection observers as a javascript alternative:
25+
[https://blog.logrocket.com/style-scroll-snap-points-css/](https://blog.logrocket.com/style-scroll-snap-points-css/).
26+
27+
These ideas have been explored before. For instance, see
28+
[this github issue](https://github.com/w3c/csswg-drafts/issues/5979), and
29+
[this explainer](https://github.com/argyleink/ScrollSnapExplainers/blob/main/css-snap-target/readme.md)
30+
for `:snapped` pseudo classes. That explainer pre-dates the container queries
31+
specification and implementation that has happened recently.
32+
33+
One major argument against exposing this state to style resolution is that it
34+
introduces cycles in rendering updates since layout influences scroll positions
35+
which influence this state which again can influence layout through style
36+
depending on this state (see
37+
[this comment](https://github.com/w3c/csswg-drafts/issues/5979#issuecomment-899765136)
38+
and onwards).
39+
40+
There is an [open github issue](https://github.com/w3c/csswg-drafts/issues/6402)
41+
for state container queries where it was resolved to defer to the next level of
42+
the spec - presumably css-contain-4.
43+
44+
## Container Queries vs Pseudo Classes
45+
46+
Container queries are an alternative to using pseudo classes for these states.
47+
There are pros and cons of using new pseudo classes. Container queries would
48+
naturally limit the style depending on this state to be applied in the stuck or
49+
snapped element's subtree, while a pseudo class could arbitrarily be used to
50+
style sibling subtrees and even ancestors (using `:has()`) unless the use in
51+
selectors are restricted to certain combinators. One can argue that using
52+
pseudo classes would make it more likely to end up in situations where layout
53+
cycles are an issue.
54+
55+
Containers already have the container-type property which can be used to
56+
explicitly choose which elements can be queried for state, which could also
57+
help avoiding cycles issues. It would also allow for more performant
58+
implementations in that fewer elements need to be checked for state. When size
59+
containers were introduced, there was a conscious choice to introduce an
60+
explicit container-type instead of implicitly deduce a size container from the
61+
`contain` property. For a pseudo class approach, we would either have to
62+
introduce an explicit state property or deduce that an element is allowed to
63+
match based on another property. E.g. `:stuck` allowed for `position:sticky`.
64+
65+
In the discussion in:
66+
[https://github.com/w3c/csswg-drafts/issues/5979](https://github.com/w3c/csswg-drafts/issues/5979),
67+
container queries are brought up as an alternative to pseudo classes. That
68+
issue is also talking about containment, whether container queries should be
69+
used instead of pseudo classes, and the fact that the need for an extra loop
70+
after snapshotting could be hooked into the same HTML rendering update as for
71+
scroll driven animations.
72+
73+
## States to Query
74+
75+
This explainer tries to capture scroll-based state queries and the following
76+
sections cover each of them. The commonality is that they all rely on scroll
77+
positions and sizes.
78+
79+
### Stuck Sticky-positioned Elements
80+
81+
Introduce a container-type `sticky` to allow sticky positioned elements as
82+
query containers for querying whether the sticky positioned has an offset
83+
applied to fulfill the constraint for a given inset property. For instance:
84+
85+
```html
86+
<style>
87+
#sticky {
88+
container-name: my-menu;
89+
container-type: sticky;
90+
position: sticky;
91+
top: 0px;
92+
height: 100px;
93+
}
94+
95+
#sticky-child {
96+
background-color: orange;
97+
color: white;
98+
height: 100%;
99+
}
100+
101+
@container my-menu scroll-state(stuck: top) {
102+
#sticky-child { width: 50%; }
103+
}
104+
</style>
105+
<div id="sticky">
106+
<div id="sticky-child">
107+
Sticky
108+
</div>
109+
</div>
110+
```
111+
112+
There is a question whether the query should match when the applied offset is
113+
still 0, but it is exactly aligned with the edge of where an offset would start
114+
to take effect.
115+
116+
#### Use Cases and Author Requests
117+
118+
- Demo that changes class through javascript based on the scrollTop value:
119+
[Header (Logo pops in)](https://codepen.io/JGallardo/pen/ZEBbeP)
120+
- [Article using IntersectionObserver](https://davidwalsh.name/detect-sticky),
121+
but stating there should ideally be a :stuck pseudo
122+
- [Using IntersectionObserver to fire custom sticky-change events](https://developer.chrome.com/blog/sticky-headers/)
123+
- [Another demo using IntersectionObserver](https://codepen.io/bhupendra1011/pen/GRKxWMM)
124+
125+
#### Chrome Prototype Demo
126+
127+
<video width="400px" src="https://lilles.github.io/explainers/sticky.mov" controls muted></video>
128+
129+
### Scroll-snapped Elements
130+
131+
Introduce a container-type `snapped` to allow scroll-snap-aligned elements to
132+
be queried for whether they are currently
133+
[snapped](https://drafts.csswg.org/css-scroll-snap/#scroll-snap) in a given
134+
direction. For instance:
135+
136+
```css
137+
@container scroll-state(snapped: block) {
138+
#snap-child {
139+
outline: 5px solid yellow;
140+
}
141+
}
142+
```
143+
144+
#### Use Cases and Author Requests
145+
146+
In general being able to highlight the scroll snapped element, for instance in
147+
a carousel.
148+
149+
- [Author request on github](https://github.com/w3c/csswg-drafts/issues/7430)
150+
151+
### Overflowing
152+
153+
Query whether a container has scrollable overflow. Can be used to indicate
154+
there is content to scroll to in a given direction.
155+
156+
Needs further exploration.
157+
158+
### Anchor position fallback
159+
160+
In earlier exploration of anchor positioning, it was suggested that container
161+
queries could be used to style the contents of an anchor positioned element
162+
based on which [`@position-fallback`](https://drafts.csswg.org/css-anchor-position-1/#fallback-rule)
163+
is used.
164+
165+
This needs further exploration.
166+
167+
## Proposed Syntax
168+
169+
Add a new scroll-state() function to `@container` similar to `style()` where
170+
`state:value` pairs, and logical combinations of them, can be queried.
171+
`<state-feature>` will be similar to
172+
[`<style-feature>`](https://drafts.csswg.org/css-contain-3/#typedef-style-feature)
173+
in that it takes a state name and value separated by a colon.
174+
175+
Add a new [`container-type`](https://drafts.csswg.org/css-contain-3/#container-type)
176+
for each state which can be combined with size container types:
177+
178+
- `stuck`
179+
- `snapped`
180+
181+
Query values for `stuck`:
182+
183+
- `none`
184+
- `top`
185+
- `left`
186+
- `right`
187+
- `bottom`
188+
- `inset-block-start`
189+
- `inset-block-end`
190+
- `inset-inline-start`
191+
- `inset-inline-end`
192+
193+
Query values for `snapped`:
194+
195+
- `none`
196+
- `block`
197+
- `inline`
198+
199+
A query with only a state name (boolean context) would match if the state is
200+
different from `none`.
201+
202+
## Containment Requirements
203+
204+
The proposed approach to handling layout cycles is to have a two-pass rendering
205+
update if necessary like scroll-driven animations have. See the
206+
[Layout Cycles](#layout-cycles) section below for details. There is strictly no
207+
need to have containment applied to the new container-types to have a
208+
predictable rendering, but it would help authors to avoid flickering issues.
209+
The question is whether it makes sense to enforce containment if containment is
210+
not enough to fix all flickering issues.
211+
212+
By flickering issues we mean cases where the stuck styling would cause the
213+
sticky position to no longer be stuck and the following rendering update would
214+
lose that style and cause it to be stuck again. Similarly a scroll-snapped
215+
element could be made no longer snapped through style changes affecting layout
216+
and scroll position.
217+
218+
## Layout Cycles
219+
220+
The scroll based state queries have the same issue with potential layout cycles
221+
that scroll-driven animations have. Whether an element has a sticky position
222+
offset or is scroll-snapped is not known until after layout, and state queries
223+
affect styles inside the containers which may affect whether the sticky element
224+
has a sticky position offset or not.
225+
226+
It is possible that a lot of these cycles could be avoided if enough
227+
containment is applied, but that would not allow for inline-size containment
228+
(require full size containment), and also nesting sticky positioned elements
229+
would be a problematic case.
230+
231+
More importantly, there is the problem of applying scroll based state queries
232+
on the first frame, which regardless requires finishing layout before the query
233+
containers' state can be found which would require another pass regardless of
234+
containment requirements.
235+
236+
The scroll-driven animations spec specifies a set of stale timelines which
237+
makes it possible to apply the current timeline based on newly created
238+
timelines, or timelines that changed because of layout changes, to the same
239+
frame that the timeline was created or changed. It is however limited to a
240+
single extra update, specified as
241+
[modifications to the HTML event loop](https://drafts.csswg.org/scroll-animations/#html-processing-model-event-loop).
242+
243+
It should be possible to do scroll based state queries using the exact same
244+
timing for updating state query state and detect if there is a need for a
245+
second pass to update the rendering based on the updated state.
246+
247+
## Transitions
248+
249+
The two-pass rendering update introduces a side-effect of starting transitions
250+
which can be unfortunate, especially for the first rendered frame. This is
251+
already an observable effect for scroll-driven animations, which can be seen
252+
in the example below.
253+
254+
```html
255+
<!DOCTYPE html>
256+
<style>
257+
#timeline {
258+
scroll-timeline: --scroll y;
259+
width: 400px;
260+
height: 400px;
261+
overflow-y: scroll;
262+
}
263+
@keyframes w {
264+
0% {
265+
width: 300px;
266+
}
267+
100% {
268+
width: 400px;
269+
}
270+
}
271+
#container {
272+
container-type: inline-size;
273+
position: fixed;
274+
animation-timeline: --scroll;
275+
animation-name: w;
276+
width: 200px;
277+
height: 200px;
278+
}
279+
#target {
280+
height: 100%;
281+
background-color: green;
282+
transition: background-color 4s;
283+
}
284+
#target { background-color: green; }
285+
@container (width > 200px) {
286+
#target { background-color: lime; }
287+
}
288+
</style>
289+
<div id="timeline">
290+
<div id="container">
291+
<div id="target"></div>
292+
</div>
293+
<div style="height:2000px"></div>
294+
</div>
295+
```

0 commit comments

Comments
 (0)