Le nuove API di scroll per costruire caroselli accessibili, con controlli e indicatori gestiti dal browser.
I carousel dividono: c’è chi li associa subito ai grandi “hero slider” pieni di autoplay e contenuti ignorati, e chi invece li usa ogni giorno per pattern molto più utili (liste prodotto, gallerie, app store, playlist, card di articoli, recensioni). Il punto è semplice: su schermi piccoli e layout densi, comprimere più elementi in una fascia orizzontale scorrevole spesso è la scelta più pragmatica.
La cosa interessante è che un carousel non è magia: è uno scroll container con qualche regola in più. E negli ultimi rilasci di Chrome sono arrivate API CSS che alzano notevolmente il livello: controlli nativi, indicatori, stato “corrente” e perfino possibilità di rendere inert ciò che è off-screen.
Di seguito una panoramica ragionata (e pratica) di come comporre un carousel moderno in CSS.
1) La base: un carousel è uno scroller con scroll-snap
La struttura minima parte da un contenitore che scorre orizzontalmente e da item che “agganciano” una posizione.
.carousel {
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.carousel > * {
scroll-snap-align: center;
}
Risultato: puoi scorrere liberamente e, al termine del gesto, l’elemento più vicino si allinea al centro.
Nascondere la scrollbar senza uccidere lo scroll
Per un look più “carousel”, spesso conviene nascondere la barra mantenendo comunque lo scorrimento attivo:
.carousel {
scrollbar-width: none;
}
Questa scelta è molto diversa da overflow: hidden: lo scroll rimane disponibile (touch/trackpad/ruota), semplicemente non “consuma” spazio con la scrollbar.
Il problema successivo però è inevitabile: se non vedo la scrollbar, come capisco che posso scorrere? Qui entrano in gioco i controlli nativi.
2) Pulsanti di scorrimento nativi con ::scroll-button()
CSS introduce un pseudo-elemento funzionale per generare pulsanti “precedente/successivo” direttamente nello scroll container:
-
::scroll-button(left)/::scroll-button(right)per caroselli orizzontali -
::scroll-button(up)/::scroll-button(down)per quelli verticali - alternative logiche consigliate:
inline-start,inline-end,block-start,block-end
I pulsanti vengono creati solo quando assegni un content diverso da none.
Esempio (con etichette accessibili incluse via content):
.carousel {
/* ...base scroll... */
&::scroll-button(left) {
content: "←" / "Scorri a sinistra";
}
&::scroll-button(right) {
content: "→" / "Scorri a destra";
}
&::scroll-button(*) {
aspect-ratio: 1;
border-radius: 999px;
background: color-mix(in oklab, Canvas, CanvasText 10%);
border: 1px solid color-mix(in oklab, CanvasText, transparent 70%);
}
&::scroll-button(*):focus-visible {
outline: 2px solid Highlight;
outline-offset: 2px;
}
}
Comportamento “gratis”: disabled automatico
Un vantaggio enorme è che il browser gestisce lo stato: quando non puoi più scorrere in una direzione, il relativo pulsante risulta disabilitato. Puoi anche stilizzarlo con :disabled (ad esempio per nasconderlo o ridurne l’opacità).
Posizionamento
Per metterli “sopra” al carosello o ai lati, la strada più pulita oggi è usare l’anchor positioning: consente di ancorare i pulsanti al container senza hack di layout. (Vale la pena adottare proprietà logiche anche qui, così il carousel resta pronto per RTL e localizzazioni.)
Nota: essendo pseudo-elementi, i
::scroll-button()hanno alcune limitazioni di customizzazione rispetto a elementi DOM reali—ma in molti casi sono un compromesso eccellente.
3) Indicatori (i “pallini”) con scroll-marker-group e ::scroll-marker
Il pattern classico dei puntini cliccabili può essere costruito con:
1) una proprietà sul container che abilita il gruppo marker
2) uno pseudo-elemento marker sugli item
Abilitare il gruppo
.carousel {
scroll-marker-group: after;
}
after o before decidono se il gruppo marker viene inserito dopo o prima nel tab order (e, in pratica, dove viene collocato nel flusso dei controlli).
Definire i marker sugli item
.carousel > li::scroll-marker {
content: "" / "Vai all’elemento";
}
Come per i pulsanti: niente content, niente marker.
Evidenziare quello attivo con :target-current
Per dare feedback visivo serve distinguere il marker “corrente”:
.carousel > li::scroll-marker:target-current {
transform: scale(1.2);
background: CanvasText;
}
:target-current seleziona il marker attivo (quello associato all’item attualmente “in vista”/agganciato).
Marker più parlanti con i CSS counter
Dove supportato, puoi anche generare etichette più significative usando i counter CSS nel content, ad esempio “Vai all’elemento 5” invece del generico “Vai all’elemento”. È un dettaglio che migliora parecchio l’esperienza con screen reader quando gli item sono numerosi.
4) Quando vuoi controllo totale: scroll-target-group
I marker generati via pseudo-elementi sono pratici, ma proprio perché “pseudo” possono diventare stretti se vuoi markup ricco o styling molto personalizzato.
Per questo esiste scroll-target-group: invece di far generare i marker al browser, usi direttamente i link nel DOM come indicatori/target di scroll, mantenendo lo stesso comportamento.
.toc {
scroll-target-group: auto;
}
.toc a:target-current {
font-weight: 600;
text-decoration: underline;
}
Bonus: scroll-spy in due righe
Con una lista di anchor che puntano a sezioni della pagina (TOC) e :target-current, ottieni un effetto “scroll spy” senza logica JavaScript: lo stato attivo si aggiorna e puoi evidenziare la voce corrispondente.
Differenza importante rispetto a scroll-marker-group: il gruppo creato dal browser con scroll-marker-group porta con sé semantiche di navigazione attese (simili a una tablist), mentre con scroll-target-group parti da link reali e hai pieno controllo, ma anche più responsabilità sull’accessibilità e sulla UI.
5) Un approccio moderno: composizione con altre feature di scroll
Il salto di qualità arriva quando queste primitive si combinano con:
- scroll-driven animations (animazioni guidate dallo scorrimento)
- scroll state queries (stati legati alla posizione di scroll)
- gestione dell’off-screen (ad esempio rendere inert ciò che è fuori viewport, dove appropriato)
Non è “solo un carousel”: diventa un componente reattivo allo scroll, senza dover reinventare continuamente il controllo dello stato in JavaScript.
Sintesi: cosa cambia davvero per chi costruisce UI
- La base resta semplice:
overflow-x: auto+scroll-snap-*. - I controlli diventano nativi:
::scroll-button()riduce drasticamente la necessità di JS per prev/next e relativi stati. - Gli indicatori si possono ottenere rapidamente con
scroll-marker-group+::scroll-marker, e stilizzare con:target-current. - Se serve design totalmente custom (o markup ricco),
scroll-target-groupti porta su anchor reali mantenendo il comportamento di “marker”.
Implicazione pratica: vale la pena iniziare a progettare i carousel come scroll component composabili, lasciando al browser la gestione degli edge case (disabled, target corrente, navigazione) e usando JavaScript solo dove aggiunge reale valore—non per ricostruire fondamenta che ormai CSS sta rendendo standard.
Articolo originale: https://frontendfacile.it/blog/carousel-in-puro-css-scroll-snap-pulsanti-e-marker-nativi-senza-javascript
Top comments (0)