ส่วนหัวแบบติดหนึบที่รับรู้สถานะ

ส่วนหัวแบบติดหนึบเป็นรูปแบบ UI ที่พบได้ทั่วไป แต่มักจะต้องเปลี่ยนรูปลักษณ์เมื่อ "ติดหนึบ" เพื่อให้อ่านง่ายหรือประหยัดพื้นที่ โซลูชันแบบเดิมต้องใช้ JavaScript Scroll Listener ซึ่งอาจทำให้เกิดปัญหาด้านประสิทธิภาพ 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 สำหรับลักษณะการทำงานแบบติดหนึบและประเภทคอนเทนเนอร์

ตั้งค่า container-type: scroll-state ในองค์ประกอบ position: sticky ไม่ใช่ในระดับบนสุดที่เลื่อนได้ ระบุ 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) คุณไม่สามารถจัดรูปแบบองค์ประกอบคอนเทนเนอร์ด้วยการค้นหา scroll-state ขององค์ประกอบเอง

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) แต่จะไม่เปลี่ยนรูปแบบในเชิงภาพ ซึ่งเป็นแนวทางที่แนะนำสำหรับ Use Case ส่วนใหญ่

หากความเหนียวแน่นไม่จำเป็น แต่ 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;
    }
}

เคล็ดลับ: หากการจัดรูปแบบ "ติดหนึบ" ต้องใช้สีพื้นหลังอื่นเพื่อให้อ่านง่าย ให้ลองตั้งค่าเลย์เอาต์เพื่อให้การจัดรูปแบบเริ่มต้นใช้งานได้ทุกที่ (การเพิ่มประสิทธิภาพแบบต่อเนื่อง) หากต้องใช้โฆษณาสำรอง คุณสามารถจำกัดลักษณะการทำงานของ position: sticky ไว้ภายในคำค้นหา @supports (container-type: scroll-state)

หากการเปลี่ยนรูปแบบภาพมีความสำคัญอย่างยิ่งต่อการออกแบบ (เช่น สถานะติดค้างทำให้เกิดพื้นหลังหรือการบีบอัดซึ่งทำให้เนื้อหาอ่านไม่ได้) คุณสามารถใช้ JavaScript สำรองที่มีประสิทธิภาพโดยใช้ IntersectionObserver คุณต้องทำซ้ำสไตล์ CSS ในคลาส .is-stuck 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 ทั่วไปนี้ยังใช้เป็น Polyfill สำหรับการค้นหา scroll-state(scrollable) ได้ด้วย