定位位置:当前处于“卡住”状态的粘性元素

Posted

技术标签:

【中文标题】定位位置:当前处于“卡住”状态的粘性元素【英文标题】:Targeting position:sticky elements that are currently in a 'stuck' state 【发布时间】:2014-10-08 03:36:45 【问题描述】:

position: sticky 现在可以在某些移动浏览器上使用,因此您可以让菜单栏随页面一起滚动,但当用户滚动经过它时,它会一直停留在视口的顶部。

但是,如果您想在粘性菜单栏当前“固定”时稍微重新设置它的样式怎么办?例如,您可能希望栏在与页面一起滚动时具有圆角,但是一旦它粘在视口的顶部,您就想摆脱顶部的圆角,并在下面添加一点阴影它。

是否有任何类型的伪选择器(例如::stuck)来定位当前具有position: sticky 的元素?或者浏览器供应商是否有类似的东西正在筹备中?如果没有,我在哪里申请?

注意。 javascript 解决方案对此并不好,因为在移动设备上,当用户松开手指时,您通常只会收到一个 scroll 事件,因此 JS 无法知道通过滚动阈值的确切时刻。

【问题讨论】:

【参考方案1】:

目前没有为当前“卡住”的元素推荐选择器。定义了position: sticky 的Postioned Layout module 也没有提到任何这样的选择器。

CSS 的功能请求可以发布到www-style mailing list。我相信:stuck 伪类比::stuck 伪元素更有意义,因为您希望在元素处于该状态时定位它们本身。实际上,some time ago 讨论了一个 :stuck 伪类;人们发现,主要的复杂情况是困扰着任何试图基于渲染或计算样式进行匹配的选择器:循环依赖。

:stuck 伪类的情况下,最简单的循环情况将使用以下CSS:

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

而且可能还有更多难以解决的极端情况。

虽然人们普遍认为拥有基于某些布局状态匹配的选择器会很好,但不幸的是,存在主要限制使得这些限制难以实现。我不会在短时间内为这个问题的纯 CSS 解决方案屏住呼吸。

【讨论】:

真可惜。我也在寻找解决这个问题的方法。简单地引入一条规则说:stuck 选择器上的position 属性应该被忽略,这不是很容易吗? (我的意思是浏览器供应商的规则,类似于关于 left 如何优先于 right 等的规则) 这不仅仅是位置...想象一下:stucktop 的值从0 更改为300px,然后向下滚动150px... 如果它坚持或不?或者考虑一个带有position: stickybottom: 0 的元素,其中:stuck 可能会改变font-size,因此元素的大小(因此会改变它应该粘贴的时刻)...... 请参阅github.com/w3c/csswg-drafts/issues/1660,其中的建议是让 JS 事件知道什么时候卡住/不卡住。那不应该有伪选择器引入的问题。 我相信许多已经存在的伪类也可以产生同样的循环问题(例如 :hover 改变宽度和 :not(:hover) 再次变回)。我很乐意 :stuck 伪类,并认为开发人员应该对他的代码中没有循环问题负责。 嗯...我真的不明白这是错误的-就像说while循环设计糟糕,因为它允许无限循环:)但是感谢您清除这个;)【参考方案2】:

在某些情况下,一个简单的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

【讨论】:

我认为stuck 类会比自定义属性更好...您的选择有什么具体原因吗? 一个类也可以正常工作,但这似乎比这更高一点,因为它是一个派生属性。一个属性对我来说似乎更合适,但无论哪种方式,它都是一个品味问题。 由于标题已经固定,我需要我的顶部为 60 像素,所以我无法让您的示例正常工作 尝试在被卡住的地方添加一些顶部填充,在你的情况下可能是padding-top: 60px :) 使用rootMargin:'-61px 0px 90px 0px' 作为观察者的选项。 -61 使 intersectionRect 的上边距变小。 90px 将边界推到 instersectionRect 的外部位置,以防止每当您滚动经过元素时出现卡住的属性。【参考方案3】:

我想要一个纯 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;

【讨论】:

这是一个不错的解决方案,因为它没有 JS。如果文本重复,还应将aria-hidden=true 设置为其中之一以避免可访问性胡扯。更好的解决方案可能是将重复的内容放入data-content="..." 并使用content: attr(data-content) 设置:after 元素的样式。不过我还没试过【参考方案4】:

Someone on the Google Developers blog 声称找到了一个基于 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));

我自己没有复制它,但也许它可以帮助遇到这个问题的人。

【讨论】:

【参考方案5】:

不太喜欢使用 js hack 来设置样式(即 getBoudingClientRect、滚动监听、调整大小监听),但这就是我目前解决问题的方式。此解决方案将在具有可最小化/可最大化内容 (

)、嵌套滚动或任何曲线球的页面出现问题。话虽如此,当问题很简单时,它也是一个简单的解决方案。

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。 if (requestedFrame) return; 由于动画帧批处理,它不是“性能杀手”。不过,Intersection Observer 仍然是一个改进。【参考方案6】:

当您在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'))

【讨论】:

以上是关于定位位置:当前处于“卡住”状态的粘性元素的主要内容,如果未能解决你的问题,请参考以下文章

当粘性元素位于容器内时保持粘性定位行为(因为 React 必须渲染单个父元素)

html 粘性定位 - 新位置

彻底理解粘性定位 - position: sticky

粘性布局 position:sticky

粘性菜单中的元素在滚动时被按下

position sticky 定位