Nagłówki przyklejone zależne od stanu

Przyklejone nagłówki to powszechny wzorzec interfejsu, ale często muszą zmieniać swój wygląd, gdy zostaną „przyklejone”, aby zachować czytelność lub zaoszczędzić miejsce. Tradycyjne rozwiązania wymagały detektorów przewijania JavaScriptu, co mogło powodować problemy z wydajnością. Nowoczesne arkusze CSS umożliwiają natywną obsługę tego problemu za pomocą zapytań o stan przewijania.

Etapy wdrażania

1. Tworzenie kontenera przyklejonego

Potrzebujesz kontenera, który będzie działać jako element przyklejony i kontener zapytania o stan przewijania.

<!-- 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. Zastosuj CSS dla zachowania przyklejonego i typu kontenera

Ustaw container-type: scroll-state w elemencie position: sticky, a nie w jego przewijanym elemencie nadrzędnym. Zawsze podawaj container-name, ponieważ te zapytania mogą być zagnieżdżone, a nienazwane kontenery będą powodować kolizje. Możesz też połączyć je z zapytaniami o rozmiar, np. 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;
}

Ważne: stan przewijania jest sprawdzany przez elementy podrzędne kontenera stanu przewijania. Style w zapytaniu @container nie będą stosowane do samego elementu .sticky-container, ale do jego elementów podrzędnych (takich jak .sticky-header). Nie możesz stylizować samego elementu kontenera za pomocą jego własnego zapytania o stan przewijania.

3. Stan zablokowania

Użyj zapytania @container scroll-state(...), aby zastosować style, gdy nagłówek jest zablokowany.

/* 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);
  }
}

Wskazówka: możesz też używać właściwości logicznych, takich jak stuck: inset-block-start lub stuck: inset-inline-start, aby lepiej obsługiwać internacjonalizację (i18n) i układy od prawej do lewej (RTL) przez wysyłanie zapytań o krawędzie względne, a nie fizyczne.

Uwagi dotyczące zmian wymiarów

Jeśli nie wyłączysz zakotwiczenia przewijania, zmiana właściwości wpływających na układ (wysokość, dopełnienie, rozmiar czcionki) może powodować migotanie obrazu. Zakotwiczenie przewijania w treściach w przepływie poniżej elementu przyklejonego dostosowuje przesunięcie przewijania, aby skompensować zmianę układu, co powoduje wypchnięcie elementu z pozycji przyklejonej i wywołuje oscylację.

Aby tego uniknąć, wyłącz zakotwiczenie przewijania w elemencie nadrzędnym elementu przyklejonego:

.section {
  overflow-anchor: none;
}

Zastosuj overflow-anchor: none do bezpośredniego elementu nadrzędnego elementu position: sticky, a nie do :root ani body, co spowodowałoby wyłączenie zakotwiczenia przewijania na całej stronie. Dzięki temu możesz swobodnie zmieniać dowolne właściwości w stanie zablokowanym, w tym właściwości modelu pudełkowego i przekształcenia.

NIE: polegaj na overflow-anchor: none w przypadku elementów przyklejonych do dołu. Zakotwiczenie przewijania kompensuje tylko przesunięcia układu powyżej bieżącej pozycji przewijania, więc nie ma wpływu na elementy przyklejone na dole. Unikaj zmieniania właściwości modelu pudełkowego w stanie przyklejenia w przypadku nagłówków przyklejonych na dole.

Strategie zastępcze

Zapytania o stan przewijania to progresywne ulepszanie. W przeglądarkach, które nie obsługują container-type: scroll-state, nagłówek nadal będzie przyklejony do góry (ze względu na position: sticky), ale nie będzie się zmieniał wizualnie. W większości przypadków jest to zalecane podejście.

Jeśli przyklejanie nie jest niezbędne, ale position: sticky bez innego stylu byłoby gorsze niż brak przyklejania, możesz też umieścić position: sticky i powiązaną deklarację w zapytaniu @supports:

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

Wskazówka: jeśli „przyklejony” styl wymaga innego koloru tła, aby był czytelny, rozważ skonfigurowanie układu tak, aby domyślny styl działał wszędzie (progresywne ulepszanie). Jeśli musisz użyć kreacji zastępczej, możesz ograniczyć działanie position: sticky w zapytaniu @supports (container-type: scroll-state).

Jeśli przekształcenie wizualne jest absolutnie kluczowe dla projektu (np. stan przyklejenia wprowadza tło lub kompresję, bez których treść jest nieczytelna), możesz wdrożyć niezawodny zastępczy kod JavaScript za pomocą IntersectionObserver. Musisz zduplikować style CSS w klasie .is-stuck. Poniższy ogólny polyfill sprawdza getComputedStyle, aby przejść w górę i znaleźć prawidłowy kontener przewijania (domyślnie widoczny obszar), co odpowiada zachowaniu @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);
});

Uwaga: ten ogólny wzorzec IntersectionObserver może być też używany jako polyfill dla zapytania scroll-state(scrollable).