Le intestazioni fisse sono un pattern UI comune, ma spesso devono cambiare aspetto quando diventano "fisse" per mantenere la leggibilità o risparmiare spazio. Le soluzioni tradizionali richiedevano listener di scorrimento JavaScript, che possono causare problemi di prestazioni. I CSS moderni ti consentono di gestire questa situazione in modo nativo utilizzando le query sullo stato di scorrimento.
Procedura di implementazione
1. Crea il contenitore fisso
Ti serve un container che funga da elemento sticky e da container per la query sullo stato di scorrimento.
<!-- 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. Applica il CSS per il comportamento fisso e il tipo di contenitore
Imposta container-type: scroll-state sull'elemento position: sticky, non sul suo predecessore scorrevole. Specifica sempre un container-name, poiché queste query potrebbero essere nidificate e i container senza nome entrerebbero in conflitto. Puoi anche combinarlo con query sulle dimensioni, ad esempio 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;
}
Importante: lo stato di scorrimento viene richiesto dai discendenti del contenitore dello stato di scorrimento. Gli stili nella query @container non verranno applicati a .sticky-container, ma ai relativi elementi secondari (come .sticky-header). Non puoi applicare uno stile all'elemento contenitore con la sua query scroll-state.
3. Targeting dello stato bloccato
Utilizza la query @container scroll-state(...) per applicare gli stili quando l'intestazione è bloccata.
/* 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);
}
}
Suggerimento: puoi anche utilizzare proprietà logiche come stuck: inset-block-start o stuck: inset-inline-start per supportare meglio l'internazionalizzazione (i18n) e i layout da destra a sinistra (RTL) eseguendo query sui bordi relativi al flusso anziché su quelli fisici.
Note sulle modifiche alle dimensioni
Se l'ancoraggio dello scorrimento non è disattivato, la modifica delle proprietà che influiscono sul layout (altezza, spaziatura interna, dimensione del carattere) quando l'elemento è bloccato può causare sfarfallio visivo. L'ancoraggio dello scorrimento sui contenuti in-flow sotto l'elemento fisso regola l'offset di scorrimento per compensare la modifica del layout, che spinge l'elemento fuori dalla sua posizione fissa e attiva un'oscillazione.
Disattiva l'ancoraggio dello scorrimento sull'elemento principale dell'elemento sticky per evitare questo problema:
.section {
overflow-anchor: none;
}
Applica overflow-anchor: none all'elemento principale diretto dell'elemento position: sticky, non a :root o body, che disattiverebbe l'ancoraggio di scorrimento per l'intera pagina. In questo modo, puoi modificare liberamente qualsiasi proprietà nello stato bloccato, incluse le proprietà del modello di scatola e le trasformazioni.
NON fare affidamento su overflow-anchor: none per gli elementi fissati nella parte inferiore. L'ancoraggio dello scorrimento compensa solo le variazioni del layout al di sopra della posizione di scorrimento corrente, quindi non ha alcun effetto sugli elementi bloccati in basso. Evita di modificare le proprietà del modello di riquadro nello stato bloccato per le intestazioni bloccate in basso.
Strategie di riserva
Le query sullo stato di scorrimento sono un potenziamento progressivo. Nei browser che non supportano container-type: scroll-state, l'intestazione rimarrà comunque fissa in alto (a causa di position: sticky), ma non si trasformerà visivamente. Per la maggior parte dei casi d'uso, questo è l'approccio consigliato.
Se la persistenza non è essenziale, ma position: sticky senza uno stile diverso sarebbe un'esperienza peggiore rispetto alla mancata persistenza, puoi anche aggregare position: sticky e la relativa dichiarazione in una query @supports:
.sticky-container {
@supports (container-type: scroll-state) {
position: sticky;
top: 0;
container-type: scroll-state;
container-name: section-header;
z-index: 10;
}
}
Suggerimento: se lo stile "bloccato" richiede un colore di sfondo diverso per la leggibilità, valuta la possibilità di impostare il layout in modo che lo stile predefinito funzioni ovunque (potenziamento progressivo). Se devi utilizzare un comportamento di riserva, puoi limitare il comportamento di position: sticky stesso all'interno di una query @supports (container-type: scroll-state).
Se la trasformazione visiva è assolutamente fondamentale per il design (ad esempio, lo stato bloccato introduce uno sfondo o una compattazione senza i quali i contenuti sono illeggibili), puoi implementare un fallback JavaScript efficace utilizzando IntersectionObserver. Devi duplicare gli stili CSS in una classe .is-stuck. Il seguente polyfill generico controlla getComputedStyle per risalire e trovare il container di scorrimento corretto (per impostazione predefinita, l'area visibile), in modo da corrispondere al comportamento di @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);
});
Nota: questo pattern IntersectionObserver generico può essere utilizzato anche come polyfill per la query scroll-state(scrollable).