感知状态的固定标题

Web 功能: 容器滚动状态查询

粘性标题是一种常见的界面模式,但它们通常需要在“粘滞”时更改其外观,以保持可读性或节省空间。传统解决方案需要 JavaScript 滚动监听器,这可能会导致性能问题。现代 CSS 允许您使用滚动状态查询以原生方式处理此问题。

实施步骤

1. 创建粘性容器

您需要一个容器,它将充当粘性元素和滚动状态查询的容器。

<!-- Wrap in a section to define the containment area for the sticky element -->
<div class="section">
  <div class="sticky-container">
    <div class="sticky-header">
      Section Header
    </div>
  </div>
  <div class="content">
    <!-- Content goes here -->
  </div>
</div>

2. 应用 CSS 以实现粘性行为和容器类型

在 position: sticky 元素上设置 container-type: scroll-state,而不是在其可滚动祖先元素上设置。请务必指定 container-name,因为这些查询可能会嵌套,并且未命名的容器会发生冲突。您还可以将其与大小查询结合使用,例如 container-type: scroll-state inline-size。

.sticky-container {
  position: sticky;
  top: 0;
  /* MANDATORY: Always specify a container name to avoid collisions if CQs are nested */
  container-type: scroll-state;
  container-name: section-header;
  z-index: 10;
}

.sticky-header {
  /* Base styles for the header */
  background: #f0f0f0;
  padding: 20px;
  transition: background-color 0.3s, box-shadow 0.3s;
}

重要提示:滚动状态由滚动状态容器的后代查询。@container 查询中的样式不会应用于 .sticky-container 本身,而是应用于其子元素(例如 .sticky-header)。您无法使用容器元素自身的滚动状态查询来设置容器元素本身的样式。

3. 针对卡住状态

使用 @container scroll-state(...) 查询可在标题固定时应用样式。

/* Apply styles when the named container is stuck at the top */
@container section-header scroll-state(stuck: top) {
  .sticky-header {
    background: #0056b3;
    color: white;
    box-shadow: 0 4px 6px rgb(0 0 0 / 0.15);
  }
}

提示:您还可以使用 stuck: inset-block-start 或 stuck: inset-inline-start 等逻辑属性,通过查询与流相关的边缘(而非物理边缘)来更好地支持国际化 (i18n) 和从右到左 (RTL) 布局。

关于维度变更的说明

如果未停用滚动锚定,在粘滞状态下更改会影响布局的属性(高度、内边距、字体大小)可能会导致视觉闪烁。粘性元素下方流入内容的滚动锚定会调整滚动偏移量,以补偿布局变化,从而将元素推回其粘性位置并触发振荡。

在粘性元素的父级上停用滚动锚定,以避免出现这种情况:

.section {
  overflow-anchor: none;
}

将 overflow-anchor: none 应用于 position: sticky 元素的直接父级,而不是 :root 或 body,否则会停用整个页面的滚动锚定。这样一来,您就可以随意更改处于粘滞状态的任何属性,包括盒模型属性和转换。

请勿:针对固定在底部的元素依赖 overflow-anchor: none。滚动锚定仅会补偿当前滚动位置上方的布局偏移,因此对底部固定元素没有影响。避免在底部粘性标题处于粘性状态时更改盒模型属性。

后备策略

滚动状态查询是一种渐进增强。在不支持 container-type: scroll-state 的浏览器中,标题仍会固定在顶部(由于 position: sticky),但不会在视觉上发生转换。对于大多数使用场景,建议采用此方法。

如果粘性并非必不可少,但没有不同样式的 position: sticky 体验会比完全没有粘性更差,您也可以将 position: sticky 和相关声明封装在 @supports 查询中:

.sticky-container {
  @supports (container-type: scroll-state) {
      position: sticky;
      top: 0;
      container-type: scroll-state;
      container-name: section-header;
      z-index: 10;
    }
}

提示:如果“粘滞”样式需要不同的背景颜色才能保证可读性,请考虑设置布局,使默认样式在任何位置都适用(渐进增强)。如果您必须使用后备,则可以在 @supports (container-type: scroll-state) 查询中限制 position: sticky 行为本身。

如果视觉转换对设计至关重要(例如,粘滞状态会引入背景或压缩,否则内容将无法阅读),您可以使用 IntersectionObserver 实现可靠的 JavaScript 回退。您必须在 .is-stuck 类下复制 CSS 样式。以下通用 polyfill 会检查 getComputedStyle,以向上遍历并找到正确的滚动容器(默认为视口),从而匹配 @container scroll-state(stuck: top) 的行为:

function getScrollParent(node) {
  if (node == null || node === document.body || node === document.documentElement) {
    return null; // default to viewport
  }
  if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
    const overflow = getComputedStyle(node).overflow;
    if (overflow !== 'visible' && overflow !== 'clip') {
      return node;
    }
  }
  return getScrollParent(node.parentNode);
}

document.querySelectorAll('.sticky-container').forEach(container => {
  const root = getScrollParent(container);
  
  const topOffset = parseFloat(getComputedStyle(container).top) || 0;
  
  const observer = new IntersectionObserver(
    ([e]) => {
      // Toggle the fallback class on the sticky header container
      e.target.classList.toggle('is-stuck', e.intersectionRatio < 1);
    },
    { 
      root: root,
      threshold: [1],
      rootMargin: `-${topOffset + 1}px 0px 0px 0px` 
    }
  );
  
  observer.observe(container);
});

注意:此通用 IntersectionObserver 模式也可用作 scroll-state(scrollable) 查询的 polyfill。