preface

  1. Why write a “sticky” analysis again?

【 front-end 】sticky sticky positioning. And then I matched up with someone, and I realized I had a mistake, and I didn’t, so I did it again.

  1. What are the improvements in this article?

The information on the MDN still feels a little blurry, so this time I will go straight to the W3C CSS toy Layout Module Level 3 and do the reading. And don’t arbitrarily add their own guesses, try to derive based on the specification.

  1. The tests in this article are based on Chrome 87 and do not explore compatibility with other environments and Sticky.

  2. Expect to have certain cognition of the noun sticky and layout system, which will not be repeated here

Sticky definition

The paper sticky

Relative positioning, which visually shifts a box relative to its laid-out location.

Sticky positioning, which visually shifts a box relative to its laid-out location in order to keep it visible when a scrollable ancestor would otherwise scroll it out of sight.

Sticky will “visually shift” the element so that it stays within the most recent scrolling ancestor element as much as possible.

Here is a simple overview of position:relative. Compare the descriptions between the two: I guess that the implementation of sticky in the bottom layer of browser is to monitor rolling events and dynamically set position:relative and other attributes for the sticky element. Other behaviors of the two are “very similar” (e.g., percentage width/height, etc.).

Strictly define sticky

In general, the “visual position” of sticky is mainly determined by “Sticky View Rectangle” and “containing block”.

sticky view rectangle

Sticky positioning is similar to relative positioning except the offsets are automatically calculated in reference to the nearest scrollport.

For a sticky positioned box, the inset properties represent insets from the respective edges of the nearest scrollport, Defining the sticky view rectangle used to constrain the box’s position. (For this purpose an auto value represents the pole zero inset.) If this results in a sticky view rectangle size in any axis less than the size of the border box of the sticky box in that axis, then the effective end-edge inset in the affected axis is reduced (possibly becoming negative) to bring the sticky view Rectangle’s size up to the size of the border box in that axis.

As mentioned above, position:sticky and position:relative are basically the same, except that position:sticky is located according to “nearest scrollport” (nearest rolling ancestor).

The following three points are mainly elaborated:

  1. For aposition:stickyElement, inset properties(i.etop,bottom,left,rightWill be based on the nearest Scrollport elementcontent areaGenerate a “sticky View Rectangle” for constraintsposition:stickyThe position of the element.
  2. If the value of inset properties isauto(default), then rectangle has a “zero” edge (meaning there is no constraint, or the corresponding edge is at infinity)
  3. If the generated Sticky View Rectangle is not large enough to containposition:stickyElements of theborder box, then the “end-edge” inset properties will be reduced until the Sticky View Rectangle can be fully containedposition:stickyElements.

A recent scroll ancestor has no effect on the size and position of the sticky View Rectangle. The distance between each edge of the sticky View Rectangle and each edge of the nearest scrolling ancestor content Area is constant

The value of “zero” in the second point is mainly understood according to the Node in the specification:

Note: A sticky positioned element with a non-auto top value and an auto bottom value will only ever be pushed down by sticky positioning; it will never be offset upwards.

If top is not auto, but bottom is auto, then the position:sticky element will only shift downward (relative to its initial position) and never upward. Same thing for left and right.

The third point is more problematic. The first is that “end-edge” doesn’t seem to be strictly defined in the specification. From the conventions of the specification document, I assume it means “the end of the layout direction”, which generally means right or bottom, but in some cases can be left/top, such as direction: RTL; unicode-bidi:bidi-override; (causes text to move from right to left).

There is no way to verify the meaning of “end-edge” in Chrome at this time, as The entire third point is not supported in Chrome and it is recommended not to use this feature for the time being. Details are as follows:

  • Issue 1170125: position:sticky bottom inset not work when “sticky view rectangle” size is less than sticky element
  • Issue 1140374: Position sticky with direction rtl not working

Sticky interaction

For each side of the box, if the corresponding inset property is not auto, and the corresponding border edge of the box would be outside the corresponding edge of the sticky view rectangle, then the box must be visually shifted (as for relative positioning) to be inward of that sticky view rectangle edge, insofar as it can while its position box remains contained within its containing block. The position box is its margin box, except that for any side for which the distance between its margin edge and the corresponding edge of its containing block is less than its corresponding margin, that distance is used in place of that margin.

The standard sentences are a bit too long to translate, so here are three logical breakdowns:

  1. ifposition:stickyOne side of the element inset property is notauto, and corresponding toborder edgeIf the rectangle is not inside the sticky View rectangle, add a “visual offset” to this element (and)position:relativeBehave identically, without affecting the layout of other elements), so that the element is still inside the Sticky View Rectangle.
  2. There is a prerequisite for “add visual offset” in point 1:position:stickyThe offset of the elementposition boxMust not exceed itcontaining block
  3. position boxGenerally refers to itmargin box, but there are “exceptions” (this time it isborder box). This “exception” means: on one side or the othermargin edgecontaining blockThe distance between the corresponding side is less than the corresponding sidemarginValue.

About containing block:

The containing block of a static, relative, or sticky box is as defined by its formatting context

Position: Containing blocks of sticky elements are determined by the formatting context in which they are located.

Although different display attributes have different formatting contexts, However, a containing block of an element is usually the content area of the “block-level parent element”, such as display:block, display:inline-block, display:flex, etc. Notice that display:inline does not generate containing blocks for child elements.

Scroll to the sticky element

For the purposes of any operation targetting the scroll position of a sticky positioned element (or one of its descendants), the sticky positioned element must be considered to be positioned at its initial (non-offsetted) position.

It’s easier here. If you scroll the browser to the Position :sticky element or its child via http://xxx.com/#elementId or element.scrollintoView (), Then the browser should scroll to the original position of the position:sticky element.

Understand the definition of sticky

Let’s start talking human here. Note that if I am going to be vague or inconsistent, please refer to the strict definition above.

Graphics understanding

  • Red: Sticky view Rectangle
  • Green: containing block
  • Orange:position:stickyThe initial position of the element
  • Blue:position:stickyThe final offset position of the element

  • After the roll happens,position:stickyOf the initial position of the elementborder-topThis edge goes beyond the Sticky View Rectangle, so the browser bottom layer adds the appropriate rectangle to itposition:relativeEffect, so that the elements remain inside the Sticky View Rectangle.

  • Keep scrolling until you reach a certain value, because there are preconditions.position:stickyElement does not exceedcontaining block“, soposition:stickyThe element is stuckcontaining blockThe bottom edge of the scroll up together. Or you could saycontaining blockLimit theposition:stickyThe maximum offset of the element, so it behaves like this.

  • Same as above,position:stickyOf the initial position of the elementborder-bottomThis edge is outside the Sticky View Rectangle, so shift it up so that it is inside the Sticky View Rectangle as much as possible.

Containing block limits the maximum offset value of the “sticky” element.

Algorithm to understand

  1. To obtainposition:stickyThe most recent scrolling ancestor of the element
function getStickyParent(node) {
  const { parentElement } = node;
  if(! parentElement)return null;
  const computedStyle = window.getComputedStyle(parentElement);

  // Thead, tbody, tfoot, tr have no impact
  const display = computedStyle.display;
  if (display.indexOf('table-row') = = =0) return getStickyParent(parentElement);
  if (display.indexOf('table-header') = = =0) return getStickyParent(parentElement);
  if (display.indexOf('table-footer') = = =0) return getStickyParent(parentElement);

  const textArr = computedStyle.overflow.split(' ');
  if (textArr.some(text= >text ! = ='visible')) {
    if (parentElement === document.body) {
      // Handle HTML, body, and viewport special relationships
      const htmlTextArr = window.getComputedStyle(document.documentElement).overflow.split(' ');
      return htmlTextArr.some(text= >text ! = ='visible')?document.body : null;
    }
    return parentElement;
  }
  return getStickyParent(parentElement);
}
Copy the code
  1. According to the result of point 1 andposition:stickyElements of thetop,bottom,left,rightTo calculate thesticky view rectangle
  2. To obtainposition:stickyElements of thecontaining block
  3. To calculatecontaining blockTo each side of theposition:stickyElements of theThe initial positionThe distance between the sidesblockTopDistance,blockBottomDistance,blockLeftDistance,blockRightDistance
  4. To calculatesticky view rectangleTo each side of theposition:stickyElements of theThe initial positionThe distance between the sidesstickyTopDistance.
  5. The distance to the “inset” direction is positive and the distance to the “outer” direction is negative ifstickyTopDistance. If there is a negative value, it means that pairs are requiredposition:stickyElement add offset
  6. If you need to add an offset, take the “top” edge as an example.realOffsetTop= Math.min(-stickyTopDistance, blockBottomDistance), and the underlying set the offset effect for the elementposition:relative; top:realOffsetTop
  7. Listen for the scroll event of the most recent scroll element, and perform Steps 5-7

Note: I did not look at the underlying browser code, the pseudo algorithm above is the result of my simple derivation

Question and analysis in practice

The rules defined in the specification are those above and don’t seem complicated. However, with features in other specifications, it is a real pain

Relationship with Position: Fixed?

This was a bit of a headache, because MDN said:

Viscous positioning can be thought of as a mixture of relative and fixed positioning. The element is positioned relative before crossing a specific threshold, and then fixed.

However, position:fixed itself has some features, such as its percentage width and height are generally relative to viewport, and it is not the same as the sticky behavior defined in w3c specification (the specification only defines sticky and relative as similar).

To be sure, the personal feeling is that “the closest scrolling ancestor of the position:sticky element is a viewport” is the specific case. At this time, apart from other position:fixed features, it really looks like a mixture of relative positioning and fixed positioning.

How do I determine the most recent scroll ancestor

Debug a common problem. For details, see the getStickyParent function in the sham algorithm. Generally speaking, if you look up the parent, the first overflow you encounter is either a visible element or a scrollport. If not, viewPort is the nearest scroll ancestor.

  • Why is it viewPort when you can’t find it?

This is also defined in the W3C specification as Overflow Viewport Propagation

If visible is applied to the viewport, it must be interpreted as auto. If clip is applied to the viewport, it must be interpreted as hidden.

The specification states that if overflow:visible is applied to a viewport, it should be interpreted as overflow: Auto. So the viewPort is always scrollable, that is, it acts as a “bottom pocket” for the nearest scrolling ancestor

Do you find that the demo is always positioned relative to viewport?

It’s probably and < HTML />. Overflow Viewport Propagation

UAs must apply the overflow-* values set on the root element to the viewport. However, when the root element is an [HTML] html element (including XML syntax for HTML) whose overflow value is visible (in both axes), and that element has a body element as a child, user agents must instead apply the overflow-* values of the first such child element to the viewport. The element from which the value is propagated must then have a used overflow value of visible.

The overflow values are not propagated to the viewport if no boxes are generated for the element whose overflow values would be used for the viewport (for example, if the root element has display: none).

Viewport overflow from:

  1. if<html/>theoverflownotvisibleThen this property will actually be applied to the viewPort
  2. if<html/>theoverflowisvisible, then<body/>theoverflowProperties are actually applied to the ViewPort

User agents must instead apply the overflow-* values of the first such child element to the viewport, It is “instead”, which means that even if getComputedStyle(document.body).overflow returns hidden, the body does not necessarily overflow hidden, You see that the Hidden is probably coming from the viewPort constraint, not the body. This is why the getStickyParent algorithm above added a special judgment.

HTML {overflow:auto}; HTML {overflow:auto}; body {overflow: … }, let viewPort “steal” the overflow property of < HTML />.

Why is Overflow: Hidden also a recent scrolling ancestor?

However, the content must still be scrollable programatically, for example using the mechanisms defined in [CSSOM-VIEW], and the box is therefore still a scroll container.

Again, scrollable is not defined by the presence or absence of a scrollable bar, but by the ability to “programmatically scroll” when content overflows.

An element with overflow:hidden. If it overflows, you can still call the scrollTo/scrollBy API to scroll its contents. Or you can dynamically set margins for child elements, or you can do “content scrolling” in a variety of ways.

Containing block containing “margin box” or “border box”?

Namely, the second and third dots of sticky interaction effect, here are examples.

  • Of the initial position of the sticky elementmargin boxThe bottom of thecontaining blockThe bottom is greater than themargin-bottom:containing blockNeed to includemargin boxThe bottom of

demo: sticky-contain-margin

  • Of the initial position of the sticky elementmargin boxThe bottom of thecontaining blockThe bottom is less thanmargin-bottom:containing blockYou just need to include it completelyborder boxThe bottom of

demo: sticky-not-contain-margin

Sticky Top :0 when other content will exceed the scroll element?

This is in line with expectations. The sticky View Rectangle computs top:0 from the Content area of the ScrollPort.

The simple solution is to set “top” to negative, so that the “sticky View rectangle” will be placed on the top, and the “sticky” rectangle will be placed on the top.

The sticky element keeps rolling along?

Check the size and position of sticky View Rectangle and containing block carefully (🤣).

Sticky applies to elements related to the table

Because I am not familiar with the specification of the table, but I usually use a lot of it and step on a lot of pits. I carried it out as a section alone.

Table specification (Not Ready For Implementation) : CSS Table Module Level 3

Width and height of the table

In CSS 2.1, the effect of ‘min-height’ and ‘max-height’ on tables, inline tables, table cells, table rows, and row groups is undefined.

If the table-root’s width property has a computed value (resolving to Resolution-table-width) other than auto, the used width is the greater of resolved-table-width, and the used min-width of the table.

The height of a table is the sum of the row heights plus any cell spacing or borders. If the table has a height property with a value other than auto, it is treated as a minimum height for the table grid, and will eventually be distributed to the height of the rows if their collective minimum height ends up smaller than this number. If their collective size ends up being greater than the specified height, the specified height will have no effect.

  • Css2.1 does not define the behavior of min/ Max height/width on table elements.
  • Css3 table set width/height attribute, effect is equivalent to min-width, min-height

The table of the overflow

The inability to limit the width/height of

means that it will never overflow. Therefore, the only way to make a table scroll bar is to cover div tags and let the table scroll inside them.

This does not mean that overflow is an invalid attribute for

. If the overflow value of

is not visible,

can also act as a scrollport and affect the position:sticky of descendant elements

The table of the containing block

An anonymous table-wrapper box must be generated around each table-root. Its display type is inline-block for inline-table boxes and block for table boxes. The table wrapper box establishes a block formatting context. The table-root box (not the table-wrapper box) is used when doing baseline vertical alignment for an inline-table. The width of the table-wrapper box is the border-edge width of the table grid box inside it. Percentages which would depend on The width and height on the table-wrapper box’s size are relative to the table-wrapper box’s containing block instead, not the table-wrapper box itself.

The specification instructs browsers to generate an anonymous table-wrapper box for each

, and this table-wrapper box is either plain display:block or display:inline-block. As described earlier, the two formatting contexts give children containing blocks.

In short, you can assume that

will also generate containing blocks for descendant elements

Tr, thead, Tbody, tfoot

No specific explanation can be found in the specification. After searching, a reasonable statement can be found: Does a table cell have a containing block?

The specification doesn’t define or prohibit it, so it makes sense how browsers implement it.

Combine Issue 702927: Position: sticky does not work on or :

For tables, position: sticky defers to the position: 21. Relative spec. At this time, Blink only CSS 2.1 for toy elements, and the position: Relative CSS 2.1 spec says that it does not apply to <thead> and <tr> elements.

Until Blink supports CSS3 positioning, I don’t think we can properly support sticky on <thead> and <tr>, as it would require (from memory) changing the definition of container() for those elements (and for <th>).

For a CSS 2.1 compliant workaround, you should be able to apply sticky to the <th> elements and it should work properly.

As a css2.1 compatibility solution, the position:sticky attribute of and elements is still valid and working.

From the discussion of the above issue and the actual test, it can be seen that < TD > and in Chrome will contain the content area of

after setting position:sticky. Also the TR, thead, tbody, and tfoot elements do not cause any “intercepts” (even if they have overflow:hidden set).

This is also the basis for the getStickyParent algorithm to add special judgments to table related elements.


Issue 417223: Implement Relative Positioning for Table Rows. Chrome seems to be stuck on this issue. Issues 702927 mentions this issue as its pre-dependency (position: Relative does not apply to Table Rows). Therefore, personal speculation that sticky is based on relative implementation is also affected.

Th, td

If overflow is set to non-visible, it will become a scrollport. If overflow is set to non-visible, it will become a scrollport.

The last