כותרות במיקום קבוע שמשתנות בהתאם למצב

כותרות דביקות הן דפוס נפוץ בממשק המשתמש, אבל לעיתים קרובות צריך לשנות את המראה שלהן כשהן הופכות ל "דביקות" כדי לשמור על הקריאוּת או לחסוך מקום. פתרונות מסורתיים דרשו מאזיני גלילה של 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 להתנהגות של רכיב דביק ולסוג קונטיינר

צריך להגדיר את 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). אי אפשר לעצב את רכיב הקונטיינר עצמו באמצעות שאילתת מצב הגלילה שלו.

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 בשביל רכיבים שמוצמדים לתחתית. העיגון של הגלילה מפצה רק על שינויים בפריסה מעל מיקום הגלילה הנוכחי, ולכן אין לו השפעה על רכיבים שמוצמדים לתחתית. כדאי להימנע משינוי מאפיינים של מודל תיבה במצב תקוע בכותרות שמוצמדות לתחתית.

אסטרטגיות חלופיות

שאילתות מצב גלילה הן שיפור הדרגתי (progressive enhancement). בדפדפנים שלא תומכים ב-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;
    }
}

טיפ: אם העיצוב של הרכיב ה "תקוע" דורש צבע רקע שונה כדי שיהיה קריא, כדאי להגדיר את הפריסה כך שהעיצוב שמוגדר כברירת מחדל יפעל בכל מקום (שיפור הדרגתי). אם חייבים להשתמש ב-fallback, אפשר להגביל את ההתנהגות של 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).