定位当前处于“固定”状态的position:sticky元素

204

position: sticky现在在一些移动浏览器上可用,因此您可以使菜单栏随着页面滚动而滚动,但是当用户滚动超过它时,它会固定在视口的顶部。

但是,如果您想在当前“粘性”状态下稍微重新设计您的粘性菜单栏怎么办?例如,当它随页面滚动时,您可能希望该栏具有圆角,但是一旦它固定在视口的顶部,您希望去掉顶部圆角,并在其下添加一些阴影。

是否有任何伪选择器(例如::stuck)来针对当前处于“sticky”状态的具有position: sticky的元素?或者浏览器供应商是否有类似的东西在开发中?如果没有,我应该在哪里提出请求?

注意:对于这种情况,JavaScript解决方案并不好,因为在移动设备上,通常只有一个scroll事件,当用户释放手指时才会触发,因此JS无法知道滚动阈值被穿过的确切时刻。

6个回答

165

目前没有为“固定”元素提出任何选择器的建议。定义了position: sticky位置布局模块中也没有提到任何这样的选择器。

CSS的功能请求可以发布到www-style邮件列表。我认为一个:stuck伪类比::stuck伪元素更合理,因为你要在元素本身处于该状态时针对它们进行定位。事实上,:stuck伪类在一段时间前就已经讨论过;发现的主要问题是困扰几乎所有尝试根据呈现或计算样式进行匹配的建议选择器的循环依赖性。

对于:stuck伪类来说,最简单的循环情况会出现在以下CSS中:

:stuck { position: static; /* Or anything other than sticky/fixed */ }
:not(:stuck) { position: sticky; /* Or fixed */ }

还有许多更棘手的边缘情况需要解决。

虽然人们普遍认为,基于某些布局状态匹配选择器会很好,但不幸的是存在重大限制,使得这些实现起来并不容易。我不会对这个问题很快出现纯CSS解决方案抱有期待。


28
很遗憾,我也在寻找解决这个问题的方法。是否可以很容易地引入一条规则,即“固定”选择器上的“position”属性应该被忽略?(我的意思是针对浏览器供应商的规则,类似于关于“左”优先于“右”的规则) - powerbuoy
8
这不仅仅涉及到位置问题……想象一下一个:stucktop的值从0改为300px,然后向下滚动了150px……它应该固定还是不固定?再考虑一下一个带有position: stickybottom: 0的元素,:stuck也许会改变font-size,从而改变元素的大小(因此改变了应该固定的时机)。 - Roman
61
我认为许多已经存在的伪类(例如:hover改变宽度,:not(:hover)再改回来)都可以制造相同的循环问题。我希望有一个 :stuck 伪类,并且认为开发者应该对他的代码中不存在循环问题负责。 - Marek Lisý
30
嗯...我并不认为这是一个错误 - 这就像说 while 循环设计得不好,因为它允许无限循环 :) 不过,谢谢你的澄清 ;) - Marek Lisý
7
“:stuck” 可以忽略位置属性吗?大多数情况下,它只会用来添加一个该死的阴影... - oriadam
显示剩余16条评论

76
在某些情况下,一个简单的 IntersectionObserver 可以解决问题,如果情况允许将其粘贴到根容器外的一个或两个像素处,而不是正确地完全依靠它。这样,当它坐落在边缘之外时,观察者就会触发,然后我们就可以开始运行了。
const observer = new IntersectionObserver( 
  ([e]) => e.target.toggleAttribute('stuck', e.intersectionRatio < 1),
  {threshold: [1]}
);

observer.observe(document.querySelector('nav'));

使用top: -2px将元素粘在其容器外,然后通过stuck属性进行定位...

nav {
  background: magenta;
  height: 80px;
  position: sticky;
  top: -2px;
}
nav[stuck] {
  box-shadow: 0 0 16px black;
}

这里有一个例子:https://codepen.io/anon/pen/vqyQEK


5
我认为使用一个“被卡住”的类比使用自定义属性更好...你选择这种方法有具体的原因吗? - collimarco
4
一个类也可以正常工作,但这似乎比那更高级一些,因为它是一个派生属性。属性对我来说更合适,但无论哪种方式,都是品味问题。 - rackable
1
尝试在被固定的元素上添加一些顶部填充,例如在您的情况下使用 padding-top: 60px :) - Tim Willis
1
使用 rootMargin:'-61px 0px 90px 0px' 作为观察器的选项。其中 -61 使得 intersectionRect 的顶部边距更小。90像素将边界推出 instersectionRect,防止滚动过元素时出现卡住的属性。 - Tieskevo
1
这不仅仅是个人口味的问题——向元素添加无效的HTML5属性。我强烈建议您修改并承认@collimarco的评论。 - Roko C. Buljan
显示剩余6条评论

24

我希望找到一个纯CSS的解决方案,可以像使用::stuck伪选择器一样对'固定'元素进行样式设置(遗憾的是,即使到了2021年,这个选择器仍然不存在)。

我创建了一个纯CSS hack,它可以在不使用JS的情况下实现此效果,并满足我的需求。它通过使用两个元素的副本来实现,其中一个是sticky,另一个则不是(unstuck),后者将覆盖sticky元素,直到你滚动它。

演示:https://codepen.io/TomAnthony/pen/qBqgErK

备用演示:https://codepen.io/TomAnthony/pen/mdOvJYw(这个版本更符合我的要求,我希望只有当它们被“固定”后,粘性条目才会出现-这也意味着没有重复内容。)

HTML:

<div class="sticky">
    <div class="unstuck">
        <div>
        Box header. Italic when 'stuck'.
        </div>
    </div>
    <div class="stuck">
        <div>
        Box header. Italic when 'stuck'.
        </div>
    </div>
</div>

CSS:

.sticky {
    height: 20px;
    display: inline;
    background-color: pink;
}

.stuck {
    position: -webkit-sticky;
    position: sticky;
    top: 0;
    height: 20px;
    font-style: italic;
}

.unstuck {
    height: 0;
    overflow-y: visible;
    position: relative;
    z-index: 1;
}

.unstuck > div {
    position: absolute;
    width: 100%;
    height: 20px;
    background-color: inherit;
}

6
这是一个好的解决方案,因为它不需要使用JS。如果文本是重复的,则应将 aria-hidden=true 设置为其中一个以避免无障碍性问题。更好的解决方案可能是将重复内容放入 data-content="..." 中,并使用 content: attr(data-content) 样式化 :after 元素。不过我还没有尝试过。 - phil294
1
一开始我对你的替代演示方式感到困惑,后来突然明白了。有点调皮,但我喜欢!虽然不是我需要的,但很不错 :) - undefined

8

Google Developers 博客上的某篇文章声称已经找到了一个基于 JavaScript 的高性能解决方案,其中使用了 IntersectionObserver

相关代码片段如下:

/**
 * Sets up an intersection observer to notify when elements with the class
 * `.sticky_sentinel--top` become visible/invisible at the top of the container.
 * @param {!Element} container
 */
function observeHeaders(container) {
  const observer = new IntersectionObserver((records, observer) => {
    for (const record of records) {
      const targetInfo = record.boundingClientRect;
      const stickyTarget = record.target.parentElement.querySelector('.sticky');
      const rootBoundsInfo = record.rootBounds;

      // Started sticking.
      if (targetInfo.bottom < rootBoundsInfo.top) {
        fireEvent(true, stickyTarget);
      }

      // Stopped sticking.
      if (targetInfo.bottom >= rootBoundsInfo.top &&
          targetInfo.bottom < rootBoundsInfo.bottom) {
       fireEvent(false, stickyTarget);
      }
    }
  }, {threshold: [0], root: container});

  // Add the top sentinels to each section and attach an observer.
  const sentinels = addSentinels(container, 'sticky_sentinel--top');
  sentinels.forEach(el => observer.observe(el));
}

我自己没有复制过它,但也许对于那些在这个问题上跌跌撞撞的人有所帮助。


3

我不是很喜欢使用js hack来设置样式(例如getBoundingClientRect、scroll监听、resize监听),但这是我目前解决问题的方法。这种解决方案在具有可最小化/最大化内容(<details>)或嵌套滚动或任何曲线球的页面上会出现问题。话虽如此,这是一个简单的解决方案,适用于问题也很简单的情况。

let lowestKnownOffset: number = -1;
window.addEventListener("resize", () => lowestKnownOffset = -1);

const $Title = document.getElementById("Title");
let requestedFrame: number;
window.addEventListener("scroll", (event) => {
    if (requestedFrame) { return; }
    requestedFrame = requestAnimationFrame(() => {
        // if it's sticky to top, the offset will bottom out at its natural page offset
        if (lowestKnownOffset === -1) { lowestKnownOffset = $Title.offsetTop; }
        lowestKnownOffset = Math.min(lowestKnownOffset, $Title.offsetTop);
        // this condition assumes that $Title is the only sticky element and it sticks at top: 0px
        // if there are multiple elements, this can be updated to choose whichever one it furthest down on the page as the sticky one
        if (window.scrollY >= lowestKnownOffset) {
            $Title.classList.add("--stuck");
        } else {
            $Title.classList.remove("--stuck");
        }
        requestedFrame = undefined;
    });
})

请注意,滚动事件监听器在主线程上执行,这使其成为性能杀手。请改用Intersection Observer API。 - Sceptical Jule
如果(requestedFrame){返回;} 由于动画帧批处理,它不是“性能杀手”。然而,Intersection Observer仍然是一种改进。 - Seph Reed

3

如果你有一个在 position:sticky 元素上方的元素,这是一种紧凑的方法。它设置属性 stuck,你可以在 CSS 中使用 header[stuck] 进行匹配:

HTML:

<img id="logo" ...>
<div>
  <header style="position: sticky">
    ...
  </header>
  ...
</div>

JS:

if (typeof IntersectionObserver !== 'function') {
  // sorry, IE https://caniuse.com/#feat=intersectionobserver
  return
}

new IntersectionObserver(
  function (entries, observer) {
    for (var _i = 0; _i < entries.length; _i++) {
      var stickyHeader = entries[_i].target.nextSibling
      stickyHeader.toggleAttribute('stuck', !entries[_i].isIntersecting)
    }
  },
  {}
).observe(document.getElementById('logo'))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接