首页 > 解决方案 > Why does changing the height of a 'position:sticky' element alter the scroll position on Chrome but not Safari?

问题描述

I've noticed that Chrome and Safari handle position: sticky on elements slightly differently.

Specifically, when a sticky element is made taller, and the window is currently scrolled such that the sticky element is offset from its initial position, Chrome will alter the scrollTop position by the same amount - giving the appearance of the content staying still while the sticky element grows over the top.

Chrome position sticky behaviour demo

In Safari, the scrollTop position is unchanged in this scenario, giving the appearance of the content moving down to accommodate the sticky element's increased height.

Safari position sticky behaviour demo

I've created a code snippet below to demonstrate how the browser behaves in this situation. The screenshots above show how this demo behaves on each browser, but you can try it for yourself here.

function grow() {
  const header = document.getElementById("header");
  document.getElementById("header").classList.toggle("large-header");
  updateScrollText();
}

function updateScrollText() {
	const container = document.getElementById("container");
	const scrollParent = getScrollParent(container);
  document.getElementById("scrollbarpos1").innerHTML = scrollParent.scrollTop;
  document.getElementById("scrollheight1").innerHTML = scrollParent.scrollHeight;
  document.getElementById("containerheight1").innerHTML = container.offsetHeight;
  document.getElementById("scrollbarpos2").innerHTML = scrollParent.scrollTop;
  document.getElementById("scrollheight2").innerHTML = scrollParent.scrollHeight;
  document.getElementById("containerheight2").innerHTML = container.offsetHeight;
}

function getScrollParent(node) {
  if (node == null) {
    return null;
  }

  if (node.scrollHeight > node.clientHeight) {
    return node;
  } else {
    return getScrollParent(node.parentNode);
  }
}


window.onscroll = updateScrollText; 
window.onload = updateScrollText;
#header {
  background-color: #CACACA;
  position: sticky;
  top: 0;
  padding: 20px;
}

.large-header {
  height: 100px;
}

.content {
  background-color: #a2a6c4;
  height: 1500px;
}

.shift-down {
  margin-top: 50px;
}
<div id="container">
  <div id="header">
    <button type="button" onclick="grow()">Grow/Shrink</button>
  </div>
  <div class="content">
    <br>
    Scrollbar position: <span id="scrollbarpos1">0</span>
    <br>
    Scroll height: <span id="scrollheight1">0</span>
    <br>
    Container height: <span id="containerheight1">0</span>
    <br>
    <br>
    Voluptatibus omnis perspiciatis consequatur magni error exercitationem saepe qui. Ipsa sint non labore voluptates. Asperiores aut non ullam aut sit omnis ducimus in. Aut enim nihil unde ad expedita. Ratione necessitatibus quasi dolorem sunt aperiam nobis ducimus.
Sequi quasi maiores eos aut non. Ipsam delectus sit facilis aut. Dolor facilis eum dignissimos. Vero reiciendis odio quis blanditiis.
Error nesciunt rem facilis. Neque labore et qui sequi eos corrupti dolorem. Reprehenderit qui voluptatem et neque ducimus ipsum similique fugit. Ea sint alias qui laborum nesciunt. Nihil ex repellendus odit sint unde fuga.
A eum nulla ut cumque necessitatibus culpa exercitationem unde. Corrupti sit minima eveniet et aut possimus sapiente. Est accusantium aut ut numquam illo.
Praesentium fugit pariatur eum ad velit distinctio culpa id. Quia voluptatum dignissimos consequatur. Eaque nihil voluptas in voluptas voluptas eius voluptas.
    <br>
    <br>
    
    <div class="shift-down">
    Scrollbar position: <span id="scrollbarpos2">0</span>
    <br>
    Scroll height: <span id="scrollheight2">0</span>
    <br>
    Container height: <span id="containerheight2">0</span>
    </div>
  </div> 
</div>

I looked at the W3C spec on Positioned Layout but couldn't find anything specifically defining how this is supposed to work.

So my questions are:

标签: htmlcsscss-positionsticky

解决方案


Why is this behaviour different on these two browsers?

To understand this, you first need to understand how a sticky element takes up space on a page. As you scroll past the sticky element, the rendered part of the sticky element follows the top of your viewport, but the element still physically takes up space where it was originally placed:

As the viewport scrolls down, it takes the rendered element with it, but the element still occupies space where it was originally placed.

This puts the browser in a bit of a weird situation when you resize the header, because it's actually an element that you have already scrolled past. There are two approaches to solving this. You can do like Safari and Firefox and always keep the same distance to the top of the document, or you can do like Chromium and move the viewport so that it follows the element you are currently looking at:

As the header increases in height, the two groups of browsers have different approaches to handling it.

I believe Safari and Firefox do it their way, because it's the easiest solution, and that's just how every browser has always done it. I believe Chromium has changed it's approach recently because it has a distinct advantage when a user is reading articles with slow loading ads that change size after they've loaded:

As an ad loads and fills more, the content in the viewport shifts in Firefox and Safari, while it stays perfectly in place in Chromium.

As you can see above, when the ad loads in Safari and Firefox, it causes a major shift in the page content, which annoys and disorients the user. This can especially be bad if there are several ads that load shortly after one another. With the Chromium approach, the viewport is adjusted so that the user doesn't even notice that anything has happened.

I was not able to find any spec or discussion to back up my claims, but i'm pretty sure that this is the reasoning behind. I am not sure if Firefox or Safari has had a reason to not implement Chromiums approach, or if they just didn't feel it was that important.

Which one, if any, is "correct"?

Which solution is best is probably a very complicated and somewhat subjective discussion that i won't go into, although the benefits of Chromiums approach is undeniable.

Is there any way to make both browsers behave identically (either way)?

When you press the Grow/Shrink button, you can always detect if the scroll height changes, and then set it back right after it was changed. You can also do the opposite and change the scroll height yourself so all browsers behave like Chromium. Here is an old article from someone who did exactly that: https://simonerescio.it/en/2017/03/chrome-changes-its-center-of-gravity-reference-is-not-the-document-but-the-viewport


推荐阅读