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 the
According to the MDN, fixed 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.
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.
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
}
}
The specs are difficult to understand so here is my attempt to explain them based on MDN. Some definitions first:
position: sticky
A sticky element having position: sticky; top: 100px;
is positioned as follows:
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:
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.