If you specify `bottom: 0` for position: sticky, why is it doing something different from the specs?
Asked Answered
E

3

18

This is a question when I read an article on the MDN position property. I thought that there was a clear difference between the behavior of sticky described there and the actual behavior.


According to the MDN, sticky position elements are treated as relative position elements until the specified threshold is exceeded, and when the threshold is exceeded, they are treated as fixed position elements until the boundary of the parent element is reached (Link).

Sticky positioning can be thought of as a hybrid of relative and fixed positioning. A stickily positioned element is treated as relatively positioned until it crosses a specified threshold, at which point it is treated as fixed until it reaches the boundary of its parent. For instance...

#one { position: sticky; top: 10px; } 

...would position the element with id one relatively until the viewport were scrolled such that the element would be less than 10 pixels from the top. Beyond that threshold, the element would be fixed to 10 pixels from the top.

So, I created the following code and confirmed the operation.

body {
  margin: 0;
}

.container {
  display: flex;
  flex-direction: column;
}

.container>* {
  width: 100%;
}

header {
  background: #ffa;
  height: 130vh;
}

main {
  background: #faf;
  height: 210vh;
}

footer {
  background: #faa;
  height: 8vh;
  position: sticky;
  bottom: 0;
}

.footer {
  background: #aff;
  height: 100vh;
}
<div class="container">
  <header>HEADER</header>
  <main>MAIN CONTENT</main>
  <footer>FOOTER</footer>
  <div class="footer"></div>
</div>

According to the MDN article, this code "is a relative placement element until the position of the element is less than 0px from the bottom of the viewport by scrolling the viewport, and becomes a fixed placement element when it is more than 0px from the bottom" I was thinking.

However, the result is the action of "Scroll to the fixed position element until the position of the element becomes smaller than 0px from the lower end of the viewport by scrolling the viewport, and become the relative arranged element when larger than 0px from the lower end".


Why does specifying the bottom:0 result in the opposite of the behavior shown in MDN?

When top: 0 is specified, the relative position is applied when the element does not reach bottom: 0 of the viewport, and when it reaches, fixed position is applied. When bottom: 0 is specified, the opposite is true. The relative position is applied when the element does not reach the bottom: 0 of the viewport, the fixed position is applied when it is reached

I read CSS3 but its mechanism was difficult to read

Enrollment answered 3/5, 2019 at 8:7 Comment(0)
B
21

According to the MDN, sticky position elements are treated as relative position elements until the specified threshold is exceeded

It's all a matter of language here because the above sentence doesn't mean the element will necesseraly start position:relative then become fixed. It says until the specified threshold is exceeded. So what if initially we have the specified threshold exceeded? This is actually the case of your example.

In other words, position:sticky has two states.

  1. It's treated as relative
  2. It's treated as fixed when the specified threshold is exceeded

Which one will be the first will depend on your HTML structure.

Here is a basic example to illustrate:

body {
  height:150vh;
  margin:0;
  display:flex;
  flex-direction:column;
  border:2px solid;
  margin:50px;
}

.b {
  margin-top:auto;
  position:sticky;
  bottom:0;
}

.a {
  position:sticky;
  top:0;
}
<div class="a"> 
  I will start relative then I will be fixed
</div>
<div class="b"> 
I will start fixed then I will be relative
</div>

You can also have a mix. We start fixed, become relative and then fixed again:

body {
  height:250vh;
  margin:0;
  display:flex;
  flex-direction:column;
  border:2px solid;
  margin:50px;
}
body:before,
body:after {
  content:"";
  flex:1;
}

.a {
  position:sticky;
  top:0;
  bottom:0;
}
<div class="a"> 
  I will start fixed then relative then fixed
</div>

As you can see in the above examples both states are independent. If the condition of the position:fixed is true then we have position:fixed, if not then it's relative.

We can consider that the browser will implement this pseudo code:

on_scroll_event() {
   if(threshold exceeded)
      position <- fixed
   else
      position <- relative
}

For more accurate and complete understanding of the mechanism, you need to consider 3 elements. The sticky element (and the values of top/bottom/left/right), the containing block of the sticky element and the nearest ancestor with a scrolling box.

  1. The nearest ancestor with a scrolling box is simply the nearest ancestor with overflow different from visibile and by default it will be the viewport (as I explained here: What are `scrolling boxes`?). The scroll on this element will control the sticky behavior.
  2. The containing block for a sticky element is the same as for a relative element ref

Left/top/bottom/right are calculated relatively to the scrolling box and the containing block will define the limit of the sticky element.

Here is an example to illustrate:

body {
 margin:0;
}
.wrapper {
  width:300px;
  height:150px;
  border:2px solid red;
  overflow:auto;
}

.parent {
   height:200%;
   margin:100% 0;
   border:2px solid;
}

.sticky {
  position:sticky;
  display:inline-block;
  margin:auto;
  top:20px;
  background:red;
}
.non-sticky {
  display:inline-block;
  background:blue;
}
<div class="wrapper"><!-- our scrolling box -->
  <div class="parent"><!-- containing block -->
    <div class="sticky">I am sticky</div>
    <div class="non-sticky">I am the relative position</div>
  </div>
</div>

Initially our element is hidden which is logical because it cannot be outside its containing block (its limit). Once we start scrolling we will see our sticky and relative elements that will behave exactly the same. When we have a distance of 20px between the sticky element and the top edge of the scrolling box we reach the threshold and we start having position:fixed until we reach again the limit of the containing block at the bottom (i.e. we no more have space for the sticky behavior)

Now let's replace top with bottom

body {
 margin:0;
}
.wrapper {
  width:300px;
  height:150px;
  border:2px solid red;
  overflow:auto;
}

.parent {
   height:200%;
   margin:100% 0;
   border:2px solid;
}

.sticky {
  position:sticky;
  display:inline-block;
  margin:auto;
  bottom:20px;
  background:red;
}
.non-sticky {
  display:inline-block;
  background:blue;
}
<div class="wrapper"><!-- our scrolling box -->
  <div class="parent"><!-- containing block -->
    <div class="sticky">I am sticky</div>
    <div class="non-sticky">I am the relative position</div>
  </div>
</div>

Nothing will happen because when there is a distance of 20px between the element and the bottom edge of the scrolling box the sticky element is already touching the top edge of the containing block and it cannot go outside.

Let's add an element before:

body {
 margin:0;
}
.wrapper {
  width:300px;
  height:150px;
  border:2px solid red;
  overflow:auto;
}

.parent {
   height:200%;
   margin:100% 0;
   border:2px solid;
}

.sticky {
  position:sticky;
  display:inline-block;
  margin:auto;
  bottom:20px;
  background:red;
}
.non-sticky {
  display:inline-block;
  background:blue;
}

.elem {
  height:50px;
  width:100%;
  background:green;
}
<div class="wrapper"><!-- our scrolling box -->
  <div class="parent"><!-- containing block -->
  <div class="elem">elemen before</div>
    <div class="sticky">I am sticky</div>
    <div class="non-sticky">I am the relative position</div>
  </div>
</div>

Now we have created 50px of space to have a sticky behavior. Let's add back top with bottom:

body {
 margin:0;
}
.wrapper {
  width:300px;
  height:150px;
  border:2px solid red;
  overflow:auto;
}

.parent {
   height:200%;
   margin:100% 0;
   border:2px solid;
}

.sticky {
  position:sticky;
  display:inline-block;
  margin:auto;
  bottom:20px;
  top:20px;
  background:red;
}
.non-sticky {
  display:inline-block;
  background:blue;
}

.elem {
  height:50px;
  width:100%;
  background:green;
}
<div class="wrapper"><!-- our scrolling box -->
  <div class="parent"><!-- containing block -->
  <div class="elem">elemen before</div>
    <div class="sticky">I am sticky</div>
    <div class="non-sticky">I am the relative position</div>
  </div>
</div>

Now we have both behavior from top and bottom and the logic can be resumed as follow:

on_scroll_event() {
    if( top_sticky!=auto && distance_top_sticky_top_scrolling_box <20px && distance_bottom_sticky_bottom_containing_block >0) {
          position <- fixed
     } else if(bottom_sticky!=auto && distance_bottom_sticky_bottom_scrolling_box <20px && distance_top_sticky_top_containing_block >0) {
        position <- fixed
     } else (same for left) {
        position <- fixed
     } else (same for right) {
        position <- fixed
     } else {
        position <- relative
     }
}
Boulder answered 3/5, 2019 at 9:0 Comment(9)
The mixed code didn't work in Firefox. But It worked in Chrome. Also, how is the value to be compared with the threshold ( top , bottom value ) determined? It seems to me that the threshold used for comparison differs between top: 0 and bottom: 0.Enrollment
@dampymq I am adding more details (it's the same for both)Boulder
I did not notice. Thank you.Enrollment
@dampymq check the update (it was difficult to illustrate with figure so I tried to explain as much as possible) .. the mixed code should also work fine on firefoxBoulder
If you have a problem like this, feel free to open a github issue. They will take time to improve wordings and even minor improvements in the manual. I opened some translation mistakes and differences in the past, they take it serious.Metacenter
@DanielW. true but the spec is still on draft so we expect it to be unclear a bit as it's still not really complete and I am not a native english speaker (english is my 3rd language) so it won't be easy for me to provide improvements :/Boulder
@DanielW. I see you are refering to the MDN, but the MDN it's not really an issue as I consider the Spec (drafts.csswg.org/css-position-3/#sticky-pos) and in this case the Spec is not very clear and it's a bit confusing in some parts which is somehow logical since it's a draft oneBoulder
Ok, I didn't read the issue well enough, consider my comment a sidenote, just saying MDN on their side is very complaisant.Metacenter
@TemaniAfif According to the MDN, fixed position elements are treated as relative position elements until the specified threshold is exceeded Did you mean sticky? It makes your answer really confusing.Combination
V
11

The specs are difficult to understand so here is my attempt to explain them based on MDN. Some definitions first:

  • sticky element – an element with position: sticky
  • containing block – the parent of sticky element
  • flow root – lets just say that it means viewport

A sticky element having position: sticky; top: 100px; is positioned as follows:

  • It is positioned according to the normal flow
  • And its top edge will maintain a distance of at least 100px from the top edge of the flow root
  • And its bottom edge cannot go below the bottom edge of the containing block

The following example shows how these rules operate:

body { font: medium sans-serif; text-align: center; }
body::after { content: ""; position: fixed; top: 100px; left: 0; right: 0; border: 1px solid #F00; }
header, footer { height: 75vh; background-color: #EEE; }
.containing-block { border-bottom: 2px solid #FA0; background: #DEF; }
.containing-block::after { content: ""; display: block; height: 100vh; }
.before-sticky { border-bottom: 2px solid #080; padding-top: 50px; }
.after-sticky { border-top: 2px solid #080; padding-bottom: 50px; }
.sticky { position: sticky; top: 100px; padding-top: 20px; padding-bottom: 20px; background-color: #CCC; }
<header>header</header>
<div class="containing-block">
  <div class="before-sticky">content before sticky</div>
  <div class="sticky">top sticky</div>
  <div class="after-sticky">content after sticky</div>
</div>
<footer>footer</footer>

Likewise, a sticky element having position: sticky; bottom: 100px; is positioned as follows:

  • It is positioned according to the normal flow
  • And its bottom edge will maintain a distance of at least 100px from the bottom edge of the flow root
  • And its top edge cannot go above the top edge of the containing block

body { font: medium sans-serif; text-align: center; }
body::after { content: ""; position: fixed; bottom: 100px; left: 0; right: 0; border: 1px solid #F00; }
header, footer { height: 75vh; background-color: #EEE; }
.containing-block { border-top: 2px solid #FA0; background: #DEF; }
.containing-block::before { content: ""; display: block; height: 100vh; }
.before-sticky { border-bottom: 2px solid #080; padding-top: 50px; }
.after-sticky { border-top: 2px solid #080; padding-bottom: 50px; }
.sticky { position: sticky; bottom: 100px; padding-top: 20px; padding-bottom: 20px; background-color: #CCC; }
<header>header</header>
<div class="containing-block">
  <div class="before-sticky">content before sticky</div>
  <div class="sticky">bottom sticky</div>
  <div class="after-sticky">content after sticky</div>
</div>
<footer>footer</footer>

I hope this is simple enough explanation.

Verrazano answered 3/5, 2019 at 19:51 Comment(0)
C
0

MDN's definition of sticky position:

A stickily positioned element is an element whose computed position value is sticky. It's treated as relatively positioned until its containing block crosses a specified threshold (such as setting top to value other than auto) within its flow root (or the container it scrolls within), at which point it is treated as "stuck" until meeting the opposite edge of its containing block.

This definition is technically wrong. There happens to be an alternative definition on MDN as well, which you've linked in your question:

Sticky positioning can be thought of as a hybrid of relative and fixed positioning when it nearest scrolling ancestor is viewport. A stickily positioned element is treated as relatively positioned until it crosses a specified threshold, at which point it is treated as fixed until it reaches the boundary of its parent.

This statement is also technically incorrect, if by fixed in the last sentence MDN means fixed position(which should be case as they mentioned fixed positioning in the first sentence.). This one is very easy to prove wrong. A fixed positioned element doesn't occupy space. If sticky element is switched between relative and fixed positions, then it would fluctuate page's height which should be visible as a change in the scrollbar's height. Most likely, MDN is using fixed not in css positioning terminology.

Now, it can also be proved wrong that a sticky element is treated as relatively positioned. When you think the condition is met for the sticky element to behave as relative, go into inspect element and apply any css inset property. If it was truly a relatively positioned element then it should move without affecting the layout.

The authoritative html reference w3.org doesn't define or explain sticky position like MDN. w3.org's definition considers sticky similar to relative; not same as relative and mentions no relation with fixed position:

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.

The term sticky view rectangle(SVR) is key here. It is an imaginary rectangle in the scrollport(always visible). The sticky element must be physically positioned inside this imaginary rectangle. The sticky element can scroll as long as it stays withing the SVR. When the scroll container is scrolled the sticky element can move within that imaginary rectangle to stay visible.

For example top: 10px on a sticky element would cause the top-edge of the SVR to be at 10px from the top edge of it's scroll container. The sticky element, since has to be within SVR, will be pushed 10px down. On the other hand, bottom: 10px would cause the SVR's bottom-edge to be 10px away from scrollport's bottom. But, in this case the sticky element would not move to the bottom; rather will stay at it's normal position, because it is still within the SVR. This same explanation can be applied in regards with left and right inset properties.

In easy language top: 10px on a sticky element means the minimum physical distance between itself and it's scrollport's top edge is 10px, so if there is some content before the sticky element pushing it down, then it can be, but when we scroll, it will stick at 10px and won't scroll further. Similarly, bottom: 10px means the minimum distance from the bottom is 10px, that is it can sit at the top(distance > 10px).

In the last example of the linked answer, there is an additional wrapper to the sticky element. w3.org explains that scenario as well:

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.

This explains why would the sticky element go out of SVR, so as to stay within it's parent element(containing block). Although I don't fully grasp the last part, where it says:

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.

I've opened this up as a separate question

Combination answered 12/6, 2022 at 3:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.