Posso usare CSS puro senza JavaScript per l’effetto shrink?
CSS puro non ha accesso diretto alla posizione di scroll, quindi la risposta breve è no, non è possibile con le specifiche attuali. Tuttavia, la nuova API di animazioni legate allo scroll (animation-timeline: scroll() e @keyframes) avvicina molto questa possibilità. Con la scroll-timeline puoi animare proprietà CSS in base alla percentuale di scroll, senza una sola riga di JavaScript. Il supporto browser nel 2026 è buono per Chrome e Firefox (con flag su Safari). Se vuoi sperimentare, l’articolo sulle animazioni legate allo scroll con view-timeline mostra i pattern base. Per il momento, in produzione usa sempre JavaScript come fallback.
Come gestisco la navbar shrink con un framework React o Vue?
Il pattern è lo stesso, adattato al lifecycle del framework. In React, usa useEffect per registrare il listener di scroll (o IntersectionObserver) e useRef per mantenere il flag ticking senza causare re-render. Lo stato isShrunk va in useState: il componente si aggiorna solo quando il booleano cambia. In Vue 3 con Composition API, usa onMounted/onUnmounted per registrare e rimuovere il listener, e ref(false) per lo stato. In entrambi i casi, ricordati di fare cleanup nel return di useEffect (React) o in onUnmounted (Vue) per evitare memory leak quando il componente viene smontato.
Conclusione
La navbar sticky con effetto shrink è uno di quei componenti che sembrano semplici ma nascondono dettagli importanti: la scelta tra sticky e fixed, il throttling dello scroll listener, l’accessibilità ARIA, il rispetto di prefers-reduced-motion. Gli snippet in questo articolo coprono i tre approcci principali — classe CSS on/off, interpolazione con custom properties, IntersectionObserver — e puoi scegliere quello più adatto al tuo contesto senza reinventare nulla. Il codice è volutamente vanilla per essere adottato in qualsiasi stack, da un progetto WordPress a un’app React. Copialo, adattalo, e ricordati di testarlo su dispositivi reali: il comportamento dello scroll su mobile touch è spesso diverso da quello su desktop, e vale sempre la pena verificarlo.
Suggerimenti e Risorse
🔧 Strumento: Chrome DevTools — tab Performance — registra la pagina durante lo scroll e controlla che non ci siano long task nel main thread causati dal listener. Un frame dovrebbe restare sotto i 16ms.
💡 CSS-only shortcut: Se usi Tailwind CSS v4, puoi combinare
sticky top-0con variantidata-[shrunk=true]:h-14e aggiornare il data attribute via JS, ottenendo lo stesso risultato con zero CSS custom.
🎯 Accessibilità: Aggiungi sempre
@media (prefers-reduced-motion: reduce) { .navbar { transition: none; } }per rispettare le preferenze degli utenti con sensibilità al movimento. Non è opzionale se vuoi essere WCAG 2.2 compliant.
Navbar Sticky con Effetto Shrink allo Scroll
Perché una Navbar Sticky Shrink nel 2026
La navigazione è il primo elemento con cui l’utente interagisce: se è ingombrante, distrae; se scompare, disorientra. L’effetto shrink — la navbar che si riduce di altezza non appena l’utente inizia a scorrere — risolve entrambi i problemi in modo elegante. Occupa spazio generoso quando la pagina è in cima, trasmettendo identità e brand, poi si contrae lasciando respiro al contenuto nel momento in cui lo scroll comincia.
Nel 2026 questo pattern è diventato standard nelle landing page di prodotto, nei portfolio e nei siti SaaS. Le ragioni tecniche sono altrettanto solide: CSS custom properties permettono di interpolare altezza, padding e font-size con una singola classe aggiunta via JavaScript, mentre position: sticky ha rimpiazzato soluzioni jQuery che calcolavano l’offset manualmente ad ogni frame. Il risultato è un componente che si può scrivere in meno di 80 righe tra HTML, CSS e JS, senza dipendenze esterne, con ottime prestazioni anche su mobile. In questo articolo trovi lo snippet completo, le varianti più comuni, e i dettagli su accessibilità e performance che spesso vengono trascurati.
Le Basi: position sticky vs position fixed
Prima di costruire l’effetto shrink, è fondamentale capire la differenza tra position: sticky e position: fixed. La confusione tra i due è alla radice di molti bug nei layout.
position: fixed toglie l’elemento dal flusso del documento e lo ancora alla viewport. La navbar non occupa spazio nel layout: il contenuto sottostante deve compensare con un padding-top pari all’altezza della barra. Se la navbar cambia altezza (effetto shrink), il padding va aggiornato di conseguenza — un calcolo in più da gestire. Se vuoi approfondire le differenze di posizionamento nel layout, l’articolo Sticky vs Fixed: differenze e usi nel layout CSS copre tutti i casi d’uso in dettaglio.
position: sticky si comporta come relative finché l’elemento non raggiunge la soglia impostata (di solito top: 0), poi si âncora alla viewport mantenendo il suo spazio nel flusso. Il vantaggio è che non serve padding compensatorio: il layout “respira” normalmente. Lo svantaggio storico era il supporto browser, oggi risolto: tutti i browser moderni lo supportano pienamente.
/* Struttura base navbar sticky */
.navbar {
position: sticky;
top: 0;
z-index: 1000;
width: 100%;
background-color: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0);
transition:
height 0.3s ease,
padding 0.3s ease,
box-shadow 0.3s ease;
}
/* Stato normale (pagina in cima) */
.navbar {
height: 80px;
padding-block: 20px;
}
/* Stato shrink (dopo lo scroll) */
.navbar.is-shrunk {
height: 56px;
padding-block: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}Nota l’uso di padding-block al posto dei classici padding-top e padding-bottom: è la sintassi logica CSS, più concisa e indipendente dalla direzione del testo. Se devi supportare browser molto datati, sostituiscila con le proprietà fisiche equivalenti.
Aggiungere la Classe Shrink con JavaScript Vanilla
La logica JavaScript si riduce a poche righe: aggiungere o rimuovere la classe is-shrunk in base alla posizione dello scroll. Il punto critico è come ascoltare lo scroll senza degradare le performance.
L’errore classico è usare window.addEventListener('scroll', handler) con una funzione pesante che aggiorna il DOM ad ogni evento. Gli eventi di scroll possono sparare decine di volte al secondo: senza debounce o throttle, anche un calcolo semplice può causare jank visibile. Il modo corretto nel 2026 è usare IntersectionObserver quando possibile, oppure un listener con passive: true per segnalare al browser che non chiamerai preventDefault(), consentendogli di ottimizzare lo scroll. Se vuoi approfondire le tecniche di ottimizzazione dello scroll, dai un’occhiata all’articolo sulle Animazioni legate allo scroll: view-timeline e animation-timeline.
// navbar-shrink.js — approccio con scroll listener ottimizzato
const navbar = document.querySelector('.navbar');
const SCROLL_THRESHOLD = 50; // px oltre cui scatta lo shrink
let lastScrollY = 0;
let ticking = false;
function updateNavbar() {
if (window.scrollY > SCROLL_THRESHOLD) {
navbar.classList.add('is-shrunk');
} else {
navbar.classList.remove('is-shrunk');
}
ticking = false;
}
window.addEventListener('scroll', () => {
lastScrollY = window.scrollY;
if (!ticking) {
// requestAnimationFrame: aggiorniamo il DOM solo prima del prossimo paint
window.requestAnimationFrame(updateNavbar);
ticking = true;
}
}, { passive: true });
// Stato iniziale: controlla se la pagina è già scrollata (es. dopo refresh)
updateNavbar();Il flag ticking assicura che requestAnimationFrame venga chiamato al massimo una volta per frame, indipendentemente da quanti eventi scroll arrivano. Il parametro { passive: true } nel listener è fondamentale: comunica al browser che non bloccheremo lo scroll, consentendo ottimizzazioni hardware. Per capire meglio perché il pattern requestAnimationFrame è essenziale nelle animazioni, l’articolo Animazioni JavaScript: Fondamenti per UI e UX Efficaci entra nel dettaglio del rendering pipeline.
Variante con CSS Custom Properties per Animazioni Fluide
Un approccio più moderno e flessibile sfrutta le CSS custom properties (variabili CSS) come canale di comunicazione tra JavaScript e CSS. Invece di aggiungere/rimuovere classi, il JS aggiorna direttamente una variabile che il CSS usa per interpolare tutti i valori. Questo pattern è particolarmente potente quando hai molte proprietà da animare (altezza, padding, font-size del logo, opacità degli elementi) perché centralizza tutta la logica di stile nel CSS.
/* Variante CSS custom properties */
:root {
--navbar-progress: 0; /* 0 = normale, 1 = shrunk (interpolato via JS) */
}
.navbar {
position: sticky;
top: 0;
z-index: 1000;
/* calc() interpola tra i due valori in base a --navbar-progress */
height: calc(80px - (80px - 56px) * var(--navbar-progress));
padding-block: calc(20px - (20px - 10px) * var(--navbar-progress));
background-color: hsl(
0 0%
calc(100% - 5% * var(--navbar-progress))
);
box-shadow: 0 2px 8px
rgba(0, 0, 0, calc(0.12 * var(--navbar-progress)));
transition: none; /* le custom props non si animano da sé */
}
/* Il logo si riduce proporzionalmente */
.navbar__logo {
height: calc(40px - (40px - 28px) * var(--navbar-progress));
transition: height 0.3s ease;
}// Variante con CSS custom property e interpolazione lineare
const root = document.documentElement;
const SCROLL_START = 0; // px: inizio interpolazione
const SCROLL_END = 80; // px: fine interpolazione (shrink completo)
let ticking = false;
function updateProgress() {
const scrollY = window.scrollY;
// Normalizza tra 0 e 1
const progress = Math.min(
Math.max((scrollY - SCROLL_START) / (SCROLL_END - SCROLL_START), 0),
1
);
root.style.setProperty('--navbar-progress', progress.toFixed(3));
ticking = false;
}
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(updateProgress);
ticking = true;
}
}, { passive: true });
updateProgress();Questa variante produce un effetto graduale: la navbar non salta di colpo da 80px a 56px, ma si contrae progressivamente mentre l’utente scrolla. Il risultato è percepibilmente più raffinato. Il compromesso è che style.setProperty viene chiamato ad ogni frame dello scroll, ma poiché scriviamo solo un attributo su :root, l’impatto è trascurabile. Per approfondire le possibilità delle variabili CSS in progetti complessi, l’articolo Sfruttare le variabili CSS per temi adattivi in tempo reale offre molti pattern riutilizzabili.
HTML Semantico e Accessibilità
Una navbar accessibile non è un’aggiunta opzionale: è parte integrante del componente. Lo scheletro HTML deve usare <header> e <nav> con i giusti attributi ARIA, e il comportamento shrink non deve compromettere la navigabilità da tastiera o screen reader.
<!-- HTML completo navbar sticky + shrink -->
<header class="navbar" role="banner">
<div class="navbar__container">
<a href="/" class="navbar__logo" aria-label="Torna alla home">
<img src="/logo.svg" alt="Logo azienda" width="120" height="40" />
</a>
<nav class="navbar__nav" aria-label="Navigazione principale">
<ul class="navbar__list" role="list">
<li><a href="/servizi">Servizi</a></li>
<li><a href="/portfolio">Portfolio</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contatti">Contatti</a></li>
</ul>
</nav>
<!-- Hamburger per mobile -->
<button
class="navbar__toggle"
aria-expanded="false"
aria-controls="mobile-menu"
aria-label="Apri menu di navigazione"
>
<span class="navbar__toggle-icon" aria-hidden="true"></span>
</button>
</div>
</header>Tre dettagli che fanno la differenza:
- role=”banner” sull’
<header>: ridondante se<header>è figlio diretto di<body>, ma utile in layout complessi con intestazioni annidate. - aria-label sul
<nav>: se la pagina ha più di una navigation landmark (es. footer), il label distingue quella principale dagli screen reader. - aria-expanded sul bottone hamburger: deve essere aggiornato via JavaScript ad ogni apertura/chiusura del menu mobile. Non dimenticarlo — è essenziale per chi usa uno screen reader.
Per chi costruisce il menu mobile abbinato alla navbar, l’articolo su come Creare un Hamburger Menu Responsive in Puro CSS copre l’implementazione CSS-only con il pattern :has().
Performance: IntersectionObserver come Alternativa allo Scroll Listener
Per gli scenari in cui basta un toggle binario (shrink on/off), IntersectionObserver è un’alternativa più performante al listener di scroll. L’idea è osservare un elemento sentinella posizionato all’inizio della pagina: quando esce dalla viewport, la navbar si contrae; quando rientra, torna normale.
// Approccio IntersectionObserver — ideale per toggle binario
const navbar = document.querySelector('.navbar');
const sentinel = document.createElement('div');
sentinel.style.cssText = 'position:absolute;top:1px;height:1px;width:1px;pointer-events:none;';
document.body.prepend(sentinel);
const observer = new IntersectionObserver(
([entry]) => {
// isIntersecting = true -> siamo in cima, navbar normale
// isIntersecting = false -> scrollato, navbar shrunk
navbar.classList.toggle('is-shrunk', !entry.isIntersecting);
},
{ threshold: 0 }
);
observer.observe(sentinel);Il vantaggio di IntersectionObserver è che il browser ottimizza internamente il rilevamento delle intersezioni, senza impattare il main thread ad ogni evento di scroll. Lo svantaggio è la minore granularità: non è adatto alla variante con interpolazione graduale (custom properties), ma per l’effetto on/off classico è la soluzione più snella.
🔧 Snippet pronto: Copia il codice della variante IntersectionObserver per progetti dove la semplicità conta più della granularità. Riduce il codice a meno di 15 righe e non richiede throttling manuale.
💡 Pro tip: Usa
will-change: height, paddingsulla navbar solo se vedi jank su dispositivi lenti. In caso contrario evitalo: crea un layer di compositing che consuma memoria GPU anche quando l’animazione non è attiva.
🎯 Best practice: Testa sempre l’effetto shrink con
prefers-reduced-motion. Gli utenti che hanno scelto di ridurre le animazioni nel sistema operativo si aspettano che il tuo sito le rispetti: aggiungi una media query che disabilita letransitionquando necessario.
FAQ e Domande Frequenti
La navbar sticky funziona con overflow: hidden sul parent?
No: position: sticky smette di funzionare se un antenato nella gerarchia ha overflow: hidden, overflow: auto o overflow: scroll. Questo è uno dei bug più comuni e frustranti. Se noti che la tua navbar non si incolla alla viewport, ispeziona gli elementi parent con Chrome DevTools cercando la proprietà overflow. La soluzione è rimuovere l’overflow dall’antenato o cambiare la struttura HTML in modo che la navbar sia figlia diretta di un elemento senza overflow impostato. In alternativa, passa a position: fixed con padding compensatorio sul <body>.
Come evitare il layout shift quando la navbar cambia altezza?
Con position: sticky non c’è layout shift: l’elemento mantiene il suo spazio nel flusso anche quando si contrae, perché lo spazio originale “rimane prenotato” nel documento. Il problema si presenta solo con position: fixed, dove il contenuto deve avere un padding-top iniziale pari all’altezza massima della navbar. In quel caso, applica il padding al <body> tramite CSS e non aggiornarlo dinamicamente: un cambio di padding causa reflow che può impattare negativamente il CLS (Cumulative Layout Shift), una delle metriche Core Web Vitals. Per approfondire le strategie di ottimizzazione dei Core Web Vitals, leggi la guida Web Performance: Core Web Vitals 2026.
Posso usare CSS puro senza JavaScript per l’effetto shrink?
CSS puro non ha accesso diretto alla posizione di scroll, quindi la risposta breve è no, non è possibile con le specifiche attuali. Tuttavia, la nuova API di animazioni legate allo scroll (animation-timeline: scroll() e @keyframes) avvicina molto questa possibilità. Con la scroll-timeline puoi animare proprietà CSS in base alla percentuale di scroll, senza una sola riga di JavaScript. Il supporto browser nel 2026 è buono per Chrome e Firefox (con flag su Safari). Se vuoi sperimentare, l’articolo sulle animazioni legate allo scroll con view-timeline mostra i pattern base. Per il momento, in produzione usa sempre JavaScript come fallback.
Come gestisco la navbar shrink con un framework React o Vue?
Il pattern è lo stesso, adattato al lifecycle del framework. In React, usa useEffect per registrare il listener di scroll (o IntersectionObserver) e useRef per mantenere il flag ticking senza causare re-render. Lo stato isShrunk va in useState: il componente si aggiorna solo quando il booleano cambia. In Vue 3 con Composition API, usa onMounted/onUnmounted per registrare e rimuovere il listener, e ref(false) per lo stato. In entrambi i casi, ricordati di fare cleanup nel return di useEffect (React) o in onUnmounted (Vue) per evitare memory leak quando il componente viene smontato.
Conclusione
La navbar sticky con effetto shrink è uno di quei componenti che sembrano semplici ma nascondono dettagli importanti: la scelta tra sticky e fixed, il throttling dello scroll listener, l’accessibilità ARIA, il rispetto di prefers-reduced-motion. Gli snippet in questo articolo coprono i tre approcci principali — classe CSS on/off, interpolazione con custom properties, IntersectionObserver — e puoi scegliere quello più adatto al tuo contesto senza reinventare nulla. Il codice è volutamente vanilla per essere adottato in qualsiasi stack, da un progetto WordPress a un’app React. Copialo, adattalo, e ricordati di testarlo su dispositivi reali: il comportamento dello scroll su mobile touch è spesso diverso da quello su desktop, e vale sempre la pena verificarlo.
Suggerimenti e Risorse
🔧 Strumento: Chrome DevTools — tab Performance — registra la pagina durante lo scroll e controlla che non ci siano long task nel main thread causati dal listener. Un frame dovrebbe restare sotto i 16ms.
💡 CSS-only shortcut: Se usi Tailwind CSS v4, puoi combinare
sticky top-0con variantidata-[shrunk=true]:h-14e aggiornare il data attribute via JS, ottenendo lo stesso risultato con zero CSS custom.
🎯 Accessibilità: Aggiungi sempre
@media (prefers-reduced-motion: reduce) { .navbar { transition: none; } }per rispettare le preferenze degli utenti con sensibilità al movimento. Non è opzionale se vuoi essere WCAG 2.2 compliant.

