|
| 1 | +<pre class=metadata> |
| 2 | +Title: CSS Scroll Anchoring Module Level 1 |
| 3 | +Shortname: css-scroll-anchoring |
| 4 | +Level: 1 |
| 5 | +Group: csswg |
| 6 | +Status: ED |
| 7 | +Work Status: Revising |
| 8 | +ED: https://drafts.csswg.org/css-scroll-anchoring |
| 9 | +Editor: Steve Kobes, Google |
| 10 | +Editor: Tab Atkins-Bittner, Google, http://xanthir.com/contact/, w3cid 42199 |
| 11 | +Abstract: Changes in DOM elements above the visible region of a <a>scrolling box</a> can result in the page moving |
| 12 | +Abstract: while the user is in the middle of consuming the content. |
| 13 | +Abstract: |
| 14 | +Abstract: This spec proposes a mechanism to mitigate this jarring user experience |
| 15 | +Abstract: by keeping track of the position of an anchor node and adjusting the scroll offset accordingly. |
| 16 | +Abstract: |
| 17 | +Abstract: This spec also proposes an API for web developers to opt-out of this behavior. |
| 18 | +</pre> |
| 19 | + |
| 20 | +<pre class=link-defaults> |
| 21 | +spec:css22; type:property; |
| 22 | + text:max-height |
| 23 | + text:min-height |
| 24 | + text:height |
| 25 | + text:max-width |
| 26 | + text:min-width |
| 27 | + text:width |
| 28 | +</pre> |
| 29 | + |
| 30 | +<h2 id=intro> |
| 31 | +Introduction</h2> |
| 32 | + |
| 33 | +Today, users of the web are often distracted by content moving around |
| 34 | +due to changes that occur outside the viewport. |
| 35 | +Examples include script inserting an iframe containing an ad, |
| 36 | +or non-sized images loading on a slow network. |
| 37 | + |
| 38 | +Historically the browser's default behavior has been |
| 39 | +to preserve the absolute scroll position when such changes occur. |
| 40 | +This means that to avoid shifting content, |
| 41 | +the webpage can attempt to reserve space on the page |
| 42 | +for anything that will load later. |
| 43 | +In practice, few websites do this consistently. |
| 44 | + |
| 45 | +Scroll anchoring aims to minimize surprising content shifts. |
| 46 | +It does this by adjusting the scroll position |
| 47 | +to compensate for the changes outside the viewport. |
| 48 | + |
| 49 | +The <a href="https://github.com/WICG/ScrollAnchoring/blob/master/explainer.md">explainer document</a> |
| 50 | +gives an informal overview of scroll anchoring. |
| 51 | + |
| 52 | +<h2 id='description'> |
| 53 | +Description</h2> |
| 54 | + |
| 55 | +Scroll anchoring works by selecting a DOM node (the <dfn export for=scroll-anchoring>anchor node</dfn>) |
| 56 | +whose movement is used to determine adjustments to the scroll position. |
| 57 | + |
| 58 | +<h3 id='anchor-node-selection'> |
| 59 | +Anchor Node Selection</h3> |
| 60 | + |
| 61 | +Each <a>scrolling box</a> aims to select an <a>anchor node</a> |
| 62 | +that is deep in the DOM |
| 63 | +and close to the block start edge of its <a>optimal viewing region</a>. |
| 64 | + |
| 65 | +Note: If the user agent does not support the 'scroll-padding' property, |
| 66 | +the optimal viewing region of the scrolling box is equivalent to its <a>content area</a>. |
| 67 | + |
| 68 | +The anchor node is either a non-anonymous <a>block box</a> or a <a>text node</a>. |
| 69 | +The anchor node is always a <a>descendant</a> of the <a>scrolling box</a>. |
| 70 | +In some cases, a scrolling box may not select any anchor node. |
| 71 | + |
| 72 | +<div algorithm> |
| 73 | + The <dfn id="anchoring-algorithm">anchor node selection algorithm</dfn> |
| 74 | + for a scrolling box |S| is as follows: |
| 75 | + |
| 76 | + 1. If |S| is associated with an element |
| 77 | + whose computed value of the 'overflow-anchor' property is ''overflow-anchor/none'', |
| 78 | + then do not select an anchor node for |S|. |
| 79 | + 2. Otherwise, for each DOM child |N| of the element or document associated with |S|, |
| 80 | + perform the <a>candidate examination algorithm</a> for |N| in |S|, |
| 81 | + and terminate if it selects an anchor node. |
| 82 | +</div> |
| 83 | + |
| 84 | +<div algorithm> |
| 85 | + The <dfn id="candidate-examination">candidate examination algorithm</dfn> |
| 86 | + for a candidate DOM node |N| in a scrolling box |S| is as follows: |
| 87 | + |
| 88 | + 1. If |N| is an <a>excluded subtree</a>, |
| 89 | + or if |N| is <a>fully clipped</a> in |S|, |
| 90 | + then do nothing (|N| and its descendants are skipped). |
| 91 | + 2. If |N| is <a>fully visible</a> in |S|, |
| 92 | + select |N| as the anchor node. |
| 93 | + 3. If N is <a>partially visible</a>: |
| 94 | + 1. For each DOM child |C| of |N|, |
| 95 | + perform the <a>candidate examination algorithm</a> for |C| in |S|, |
| 96 | + and terminate if it selects an anchor node. |
| 97 | + 2. For each <a>absolutely positioned</a> element |A| |
| 98 | + whose <a>containing block</a> is |N|, |
| 99 | + but whose DOM parent is not |N|, |
| 100 | + perform the <a>candidate examination algorithm</a> for |A| in |S|, |
| 101 | + and terminate if it selects an anchor node. |
| 102 | + 3. Select |N| as the anchor node. |
| 103 | + (If this step is reached, |
| 104 | + no suitable anchor node was found among |N|’s descendants.) |
| 105 | + |
| 106 | + Note: Deeper nodes are preferred to minimize the possibility of content changing |
| 107 | + inside the anchor node but outside the viewport, which would cause visible |
| 108 | + content to shift without triggering any scroll anchoring adjustment. |
| 109 | +</div> |
| 110 | + |
| 111 | +Conceptually, a new anchor node is computed for every scrolling box |
| 112 | +whenever the scroll position of any scrolling box changes. |
| 113 | +(As a performance optimization, |
| 114 | +the implementation may wait until the anchor node is needed before computing it.) |
| 115 | + |
| 116 | +<div algorithm> |
| 117 | + A DOM node |N| is an <dfn>excluded subtree</dfn> |
| 118 | + if it is an element and any of the following conditions holds: |
| 119 | + |
| 120 | + * |N|’s computed value of the 'display' property is ''display/none''. |
| 121 | + * |N|’s computed value of the 'position' property is ''position/fixed''. |
| 122 | + * |N|’s computed value of the 'position' property is ''position/absolute'' |
| 123 | + and |N|’s <a>containing block</a> is an ancestor of the scrolling box. |
| 124 | + * |N|’s computed value of the 'overflow-anchor' property is ''overflow-anchor/none''. |
| 125 | +</div> |
| 126 | + |
| 127 | +<div algorithm> |
| 128 | + A DOM node |N| is <dfn>fully visible</dfn> in a scrolling box |S| |
| 129 | + if |N|’s <a>scroll anchoring bounding rect</a> is entirely within the <a>optimal viewing region</a> of |S|. |
| 130 | +</div> |
| 131 | + |
| 132 | +<div algorithm> |
| 133 | + A DOM node |N| is <dfn>fully clipped</dfn> in a scrolling box |S| |
| 134 | + if |N|’s <a>scroll anchoring bounding rect</a> is entirely outside the <a>optimal viewing region</a> of |S|. |
| 135 | +</div> |
| 136 | + |
| 137 | +<div algorithm> |
| 138 | + A DOM node |N| is <dfn>partially visible</dfn> in a scrolling box |S| |
| 139 | + if |N| is neither <a>fully visible</a> in |S| nor <a>fully clipped</a> in |S|. |
| 140 | +</div> |
| 141 | + |
| 142 | +<div algorithm> |
| 143 | + The <dfn>scroll anchoring bounding rect</dfn> of a DOM node |N| |
| 144 | + is |N|’s <a>scrollable overflow rectangle</a> if |N| is a <a>block box</a>, |
| 145 | + or the bounding rect of its <a>line boxes</a> if |N| is a text node. |
| 146 | +</div> |
| 147 | + |
| 148 | +<h3 id="scroll-adjustment"> |
| 149 | +Scroll Adjustment</h3> |
| 150 | + |
| 151 | +If an anchor node was selected, |
| 152 | +then when the anchor node moves, |
| 153 | +the browser computes the previous offset <code>y0</code>, and the current offset <code>y1</code>, |
| 154 | +of the block start edge of the anchor node's <a>scroll anchoring bounding rect</a>, |
| 155 | +relative to the block start edge of the scrolling content in the <a>block flow direction</a> of the scroller. |
| 156 | + |
| 157 | +It then queues an adjustment to the scroll position of <code>y1 - y0</code>, |
| 158 | +in the block flow direction, |
| 159 | +to be performed at the end of the <a>suppression window</a>. |
| 160 | + |
| 161 | +The scroll adjustment is a type of [[cssom-view-1#scrolling-events#scrolling]] as defined by [[!CSSOM-VIEW]], |
| 162 | +and generates <a>scroll events</a> in the manner described there. |
| 163 | + |
| 164 | +<h4 id="suppression-windows"> |
| 165 | +Suppression Window</h4> |
| 166 | + |
| 167 | +Every movement of an anchor node occurs within a window of time |
| 168 | +called the <dfn>suppression window</dfn>, |
| 169 | +defined as follows: |
| 170 | + |
| 171 | +* The suppression window begins at the start of the current iteration of the |
| 172 | + <a href="https://html.spec.whatwg.org/multipage/webappapis.html#processing-model-8">HTML Processing Model</a> event loop, |
| 173 | + or at the end of the most recently completed suppression window, |
| 174 | + whichever is more recent. |
| 175 | +* The suppression window ends at the end of the current iteration of the |
| 176 | + <a href="https://html.spec.whatwg.org/multipage/webappapis.html#processing-model-8">HTML Processing Model</a> event loop, |
| 177 | + or immediately before the next operation whose result or side effects |
| 178 | + would differ as a result of a change in the scroll position |
| 179 | + (for example, an invocation of {{Element/getBoundingClientRect()}}), |
| 180 | + whichever comes sooner. |
| 181 | + |
| 182 | +Note: The suppression window boundaries should be incorporated into the HTML standard once the |
| 183 | +scroll anchoring API is stabilized. |
| 184 | + |
| 185 | +More than one anchor node movement may occur within the same suppression window. |
| 186 | + |
| 187 | +At the end of a suppression window, |
| 188 | +the user agent performs all scroll adjustments that were queued during the window |
| 189 | +and not suppressed by any <a>suppression trigger</a> during the window. |
| 190 | + |
| 191 | +<h4 id="suppression-triggers"> |
| 192 | +Suppression Triggers</h4> |
| 193 | + |
| 194 | +A <dfn>suppression trigger</dfn> is an operation |
| 195 | +that suppresses the scroll anchoring adjustment for an anchor node movement, |
| 196 | +if it occurs within the suppression window for that movement. |
| 197 | +These triggers are: |
| 198 | + |
| 199 | +* Any change to the computed value of any of the following properties, |
| 200 | + on any element in the path from the anchor node to the scrollable element (or document), |
| 201 | + inclusive of both: |
| 202 | + |
| 203 | + * 'top', 'left', 'right', or 'bottom' |
| 204 | + * 'margin' or its longhands |
| 205 | + * 'padding' or its longhands |
| 206 | + * 'width', 'height', 'min-width', 'max-width', 'min-height', or 'max-height' |
| 207 | + * 'position' |
| 208 | + * 'transform' |
| 209 | + |
| 210 | +* Any change to the computed value of the 'position' property |
| 211 | + on any element within the scrollable element (or document), |
| 212 | + such that the element becomes or stops being <a>absolutely positioned</a>. |
| 213 | + Note that this trigger applies regardless of whether the modified element is |
| 214 | + on the path from the anchor node to the scrollable element. |
| 215 | + |
| 216 | +Note: Suppression triggers exist for compatibility with existing web content that has negative |
| 217 | +interactions with scroll anchoring due to shifting content in scroll event handlers. |
| 218 | + |
| 219 | +<h2 id="exclusion-api"> |
| 220 | +Exclusion API</h2> |
| 221 | + |
| 222 | +Scroll anchoring aims to be the default mode of behavior when launched, |
| 223 | +so that users benefit from it even on legacy content. |
| 224 | +'overflow-anchor' can disable scroll anchoring in part or all of a webpage (opt out), |
| 225 | +or exclude portions of the DOM from the anchor node selection algorithm. |
| 226 | + |
| 227 | +<pre class=propdef> |
| 228 | +Name: overflow-anchor |
| 229 | +Value: auto | none |
| 230 | +Initial: auto |
| 231 | +Inherited: yes |
| 232 | +</pre> |
| 233 | + |
| 234 | +Values are defined as follows: |
| 235 | + |
| 236 | +<dl dfn-type=value dfn-for=overflow-anchor> |
| 237 | + : <dfn>auto</dfn> |
| 238 | + :: |
| 239 | + Declares that the DOM subtree rooted at the element |
| 240 | + is eligible to participate in the <a>anchor node selection algorithm</a> |
| 241 | + for any scrolling box created by the element or an ancestor. |
| 242 | + : <dfn>none</dfn> |
| 243 | + :: |
| 244 | + Declares that the DOM subtree rooted at E |
| 245 | + is <em>not</em> eligible to participate in the <a>anchor node selection algorithm</a> |
| 246 | + for any scrolling box created by the element or an ancestor. |
| 247 | +</dl> |
| 248 | + |
| 249 | +Note: The <code>overflow-anchor</code> property was also proposed (with different values) |
| 250 | +for <a href="http://tabatkins.github.io/specs/css-sticky-scrollbars/">CSS Sticky Scrollbars</a>, |
| 251 | +which has now been <a href="https://tabatkins.github.io/specs/css-sticky-scrollbars/#intro">superseded</a>. |
| 252 | + |
| 253 | +Issue: The spec originally said that 'overflow-anchor' isn't inherited, |
| 254 | +but, uh, I don't know how that works, |
| 255 | +given that the values are defined as affecting their descendants. |
| 256 | + |
| 257 | +<h2 id='priv-sec'> |
| 258 | +Privacy and Security Considerations</h2> |
| 259 | + |
| 260 | +This specification, |
| 261 | +as it only adjusts how we compute scroll positions, |
| 262 | +introduces no new privacy or security considerations. |
0 commit comments