感知狀態的固定標頭

網頁功能: 容器捲動狀態查詢

置頂標頭是常見的 UI 模式,但通常需要在「置頂」時變更外觀,以維持可讀性或節省空間。傳統解決方案需要 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。