Una panoramica di base su come creare un componente a schede simile a quelli presenti nelle app per iOS e Android.
In questo post voglio condividere il mio pensiero sulla creazione di un componente Schede per il web che sia reattivo, supporti più input da dispositivi e funzioni su tutti i browser. Prova la demo.
Se preferisci i video, ecco una versione di questo post su YouTube:
Panoramica
Le schede sono un componente comune dei sistemi di progettazione, ma possono assumere molte forme e
modalità. Prima c'erano le schede per computer basate sull'elemento <frame>
e ora abbiamo
componenti per dispositivi mobili fluidi che animano i contenuti in base alle proprietà fisiche.
Tutti cercano di fare la stessa cosa: risparmiare spazio.
Oggi, gli elementi essenziali di un'esperienza utente a schede sono un'area di navigazione con pulsanti che attiva/disattiva la visibilità dei contenuti in un frame di visualizzazione. Molte aree di contenuti diverse condividono lo stesso spazio, ma vengono presentate in modo condizionale in base al pulsante selezionato nella navigazione.

Tattiche web
Nel complesso, ho trovato questo componente piuttosto semplice da creare, grazie ad alcune funzionalità critiche della piattaforma web:
scroll-snap-points
per interazioni eleganti con scorrimento e tastiera con posizioni di arresto dello scorrimento appropriate- Link diretti tramite hash URL per supporto della condivisione e dell'ancoraggio dello scorrimento nella pagina gestito dal browser
- Supporto dello screen reader con markup degli elementi
<a>
eid="#hash"
prefers-reduced-motion
per attivare le transizioni crossfade e lo scorrimento istantaneo nella pagina- La funzionalità web
@scroll-timeline
in bozza per sottolineare e cambiare dinamicamente il colore della scheda selezionata
L'HTML
Fondamentalmente, l'esperienza utente qui è: fai clic su un link, l'URL rappresenta lo stato della pagina nidificata e poi l'area dei contenuti si aggiorna man mano che il browser scorre fino all'elemento corrispondente.
Sono presenti alcuni membri dei contenuti strutturali: link e :target
. Abbiamo bisogno di un elenco di link, per cui è perfetto un <nav>
, e di un elenco di elementi <article>
, per cui è perfetto un <section>
. Ogni hash del link corrisponderà a una sezione,
consentendo al browser di scorrere i contenuti tramite l'ancoraggio.
Ad esempio, se fai clic su un link, l'articolo :target
viene messo automaticamente in evidenza in
Chrome 89, senza bisogno di JS. L'utente può quindi scorrere i contenuti dell'articolo con
il dispositivo di input come sempre. Si tratta di contenuti senza costi, come indicato nel
markup.
Ho utilizzato il seguente markup per organizzare le schede:
<snap-tabs>
<header>
<nav>
<a></a>
<a></a>
<a></a>
<a></a>
</nav>
</header>
<section>
<article></article>
<article></article>
<article></article>
<article></article>
</section>
</snap-tabs>
Posso stabilire connessioni tra gli elementi <a>
e <article>
con le proprietà href
e id
in questo modo:
<snap-tabs>
<header>
<nav>
<a href="#responsive"></a>
<a href="#accessible"></a>
<a href="#overscroll"></a>
<a href="#more"></a>
</nav>
</header>
<section>
<article id="responsive"></article>
<article id="accessible"></article>
<article id="overscroll"></article>
<article id="more"></article>
</section>
</snap-tabs>
Successivamente ho riempito gli articoli con quantità miste di lorem e i link con una lunghezza mista e un set di titoli di immagini. Con i contenuti da utilizzare, possiamo iniziare il layout.
Layout scorrevoli
Questo componente include tre diversi tipi di aree di scorrimento:
- La navigazione (rosa) è scorrevole orizzontalmente
- L'area dei contenuti (blu) è scorrevole orizzontalmente
- Ogni elemento dell'articolo (verde) è scorrevole verticalmente.

Esistono due tipi diversi di elementi coinvolti nello scorrimento:
- Una finestra
Una casella con dimensioni definite che ha lo stile della proprietàoverflow
. - Una superficie sovradimensionata
In questo layout, si tratta dei contenitori di elenchi: link di navigazione, articoli di sezioni e contenuti degli articoli.
Layout <snap-tabs>
Il layout di primo livello che ho scelto è flex (Flexbox). Ho impostato la direzione su
column
, quindi l'intestazione e la sezione sono ordinate verticalmente. Questa è la nostra prima
finestra di scorrimento e nasconde tutto ciò che ha un overflow nascosto. A breve, l'intestazione e
la sezione utilizzeranno l'overscroll come zone individuali.
<snap-tabs> <header></header> <section></section> </snap-tabs>
snap-tabs { display: flex; flex-direction: column; /* establish primary containing box */ overflow: hidden; position: relative; & > section { /* be pushy about consuming all space */ block-size: 100%; } & > header { /* defend againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
Tornando al diagramma colorato a tre scorrimenti:
<header>
è ora pronto per essere il contenitore di scorrimento (rosa).<section>
è pronto per essere il contenitore di scorrimento (blu).
I frame che ho evidenziato di seguito con VisBug ci aiutano a vedere le finestre che i contenitori di scorrimento hanno creato.

Layout <header>
delle schede
Il layout successivo è quasi identico: utilizzo flex per creare un ordine verticale.
<snap-tabs> <header> <nav></nav> <span class="snap-indicator"></span> </header> <section></section> </snap-tabs>
header { display: flex; flex-direction: column; }
Il .snap-indicator
deve spostarsi orizzontalmente con il gruppo di link e
questo layout dell'intestazione aiuta a preparare il terreno. Nessun elemento con posizionamento assoluto qui.

Poi, gli stili di scorrimento. Abbiamo scoperto che possiamo condividere gli stili di scorrimento
tra le due aree di scorrimento orizzontale (intestazione e sezione), quindi ho creato una classe
utilitaria, .scroll-snap-x
.
.scroll-snap-x {
/* browser decide if x is ok to scroll and show bars on, y hidden */
overflow: auto hidden;
/* prevent scroll chaining on x scroll */
overscroll-behavior-x: contain;
/* scrolling should snap children on x */
scroll-snap-type: x mandatory;
@media (hover: none) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
Ognuno deve avere overflow sull'asse x, contenimento dello scorrimento per bloccare l'overscroll, barre di scorrimento nascoste per i dispositivi touch e infine scorrimento rapido per bloccare le aree di presentazione dei contenuti. L'ordine delle schede della tastiera è accessibile e qualsiasi guida alle interazioni si concentra in modo naturale. I contenitori di scorrimento rapido ottengono anche una piacevole interazione in stile carosello dalla tastiera.
Layout dell'intestazione delle schede <nav>
I link di navigazione devono essere disposti in una riga, senza interruzioni di riga, centrati verticalmente e ogni elemento di link deve essere allineato al contenitore di scorrimento rapido. Swift work for 2021 CSS!
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
Ogni link si adatta a stili e dimensioni, quindi il layout di navigazione deve specificare solo la direzione e il flusso. Le larghezze uniche degli elementi di navigazione rendono divertente la transizione tra le schede, poiché l'indicatore regola la sua larghezza in base al nuovo target. A seconda del numero di elementi presenti, il browser visualizzerà o meno una barra di scorrimento.

Layout <section>
delle schede
Questa sezione è un elemento flessibile e deve occupare la maggior parte dello spazio. Deve
anche creare colonne in cui inserire gli articoli. Ancora una volta, un lavoro
rapido per CSS 2021. block-size: 100%
estende questo elemento per riempire
il contenitore il più possibile, quindi per il proprio layout crea una serie di
colonne che sono 100%
la larghezza del contenitore. Le percentuali funzionano benissimo qui
perché abbiamo scritto vincoli rigidi per l'elemento principale.
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
È come se dicessimo "espandi verticalmente il più possibile, in modo aggressivo"
(ricorda l'intestazione che abbiamo impostato su flex-shrink: 0
: è una difesa contro questa
espansione), che imposta l'altezza della riga per un insieme di colonne a tutta altezza. Lo stile
auto-flow
indica alla griglia di disporre sempre gli elementi secondari in una riga
orizzontale, senza wrapping, esattamente come vogliamo, in modo che superino i limiti della finestra principale.

A volte faccio fatica a capirli. Questo elemento della sezione si inserisce in una casella, ma ha anche creato un insieme di caselle. Spero che le immagini e le spiegazioni ti siano utili.
Layout <article>
delle schede
L'utente deve essere in grado di scorrere i contenuti dell'articolo e le barre di scorrimento devono essere visualizzate solo in caso di overflow. Questi elementi dell'articolo si trovano in una posizione ordinata. Sono contemporaneamente un elemento principale di scorrimento e un elemento secondario di scorrimento. Il browser gestisce alcune interazioni complesse con tocco, mouse e tastiera.
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
Ho scelto di fare in modo che gli articoli si adattassero allo scorrimento della pagina principale. Mi piace molto il modo in cui gli elementi del link di navigazione e gli elementi dell'articolo si agganciano all'inizio in linea dei rispettivi contenitori di scorrimento. Sembra una relazione armoniosa.

L'articolo è un elemento secondario della griglia e le sue dimensioni sono predeterminate per essere l'area del riquadro di visualizzazione in cui vogliamo fornire l'esperienza utente di scorrimento. Ciò significa che non ho bisogno di stili di altezza o larghezza qui, devo solo definire come si verifica l'overflow. Ho impostato overflow-y su auto e ho anche intercettato le interazioni di scorrimento con la pratica proprietà overscroll-behavior.
Riepilogo delle tre aree di scorrimento
Di seguito ho scelto nelle impostazioni di sistema di "mostrare sempre le barre di scorrimento". Penso che sia ancora più importante che il layout funzioni con questa impostazione attivata, in quanto mi consente di rivedere il layout e l'orchestrazione dello scorrimento.

Penso che vedere la barra di scorrimento in questo componente aiuti a mostrare chiaramente dove si trovano le aree di scorrimento, la direzione che supportano e come interagiscono tra loro. Considera come ciascuno di questi frame della finestra di scorrimento sia anche un elemento principale flessibile o a griglia per un layout.
DevTools può aiutarci a visualizzare questo aspetto:

I layout di scorrimento sono completi: snapping, link diretti e accessibilità da tastiera. Una base solida per i miglioramenti dell'esperienza utente, dello stile e del piacere.
Funzionalità in evidenza
Gli elementi secondari con scorrimento mantengono la posizione bloccata durante il ridimensionamento. Ciò significa che JavaScript non dovrà portare nulla in visualizzazione quando il dispositivo viene ruotato o la finestra del browser viene ridimensionata. Prova la modalità Dispositivo in Chromium DevTools selezionando una modalità diversa da Responsive e ridimensionando il frame del dispositivo. Nota che l'elemento rimane in visualizzazione e bloccato con i suoi contenuti. Questa funzionalità è disponibile da quando Chromium ha aggiornato la sua implementazione in modo che corrisponda alla specifica. Ecco un post del blog al riguardo.
Animazione
L'obiettivo del lavoro di animazione qui è collegare chiaramente le interazioni con il feedback dell'interfaccia utente. In questo modo, l'utente viene guidato o assistito nella scoperta (si spera) senza problemi di tutti i contenuti. Aggiungerò movimento con uno scopo e in modo condizionale. Ora gli utenti possono specificare le proprie preferenze di movimento nel sistema operativo e mi piace molto rispondere alle loro preferenze nelle mie interfacce.
Collegherò una sottolineatura della scheda alla posizione di scorrimento dell'articolo. L'allineamento
non è solo una questione estetica, ma anche un modo per ancorare l'inizio e la fine di un'animazione.
In questo modo, <nav>
, che funge da
mini-mappa, rimane collegato ai contenuti.
Controlleremo la preferenza di movimento dell'utente sia da CSS che da JS. Ci sono
alcuni ottimi posti in cui essere rispettosi.
Comportamento di scorrimento
Esiste l'opportunità di migliorare il comportamento di movimento sia di :target
sia di
element.scrollIntoView()
. Per impostazione predefinita, è immediato. Il browser imposta solo la
posizione di scorrimento. E se volessimo passare a quella posizione di scorrimento,
invece di lampeggiare lì?
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
Poiché qui introduciamo il movimento, e un movimento che l'utente non controlla (come lo scorrimento), applichiamo questo stile solo se l'utente non ha preferenze nel sistema operativo in merito al movimento ridotto. In questo modo, introduciamo il movimento di scorrimento solo per le persone che lo accettano.
Indicatore delle schede
Lo scopo di questa animazione è aiutare ad associare l'indicatore allo stato
dei contenuti. Ho deciso di applicare un crossfade di colore agli stili border-bottom
per gli utenti
che preferiscono un movimento ridotto e un'animazione di scorrimento collegata allo scorrimento e dissolvenza del colore
per gli utenti che non hanno problemi con il movimento.
In Chromium Devtools, posso attivare/disattivare la preferenza e mostrare i due diversi stili di transizione. Mi sono divertito tantissimo a realizzarlo.
@media (prefers-reduced-motion: reduce) {
snap-tabs > header a {
border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
transition: color .7s ease, border-color .5s ease;
&:is(:target,:active,[active]) {
color: var(--text-active-color);
border-block-end-color: hsl(var(--accent));
}
}
snap-tabs .snap-indicator {
visibility: hidden;
}
}
Nascondo .snap-indicator
quando l'utente preferisce un movimento ridotto, perché non mi serve più. Poi lo sostituisco con gli stili border-block-end
e un
transition
. Inoltre, nell'interazione con le schede, l'elemento di navigazione attivo non
solo ha un'evidenziazione sottolineata del brand, ma anche il colore del testo è più scuro. L'elemento
attivo ha un contrasto del colore del testo più elevato e un accento di illuminazione inferiore brillante.
Basta qualche riga di CSS in più per far sentire una persona vista (nel senso che rispettiamo attentamente le sue preferenze di movimento). Mi piace un sacco.
@scroll-timeline
Nella sezione precedente ti ho mostrato come gestisco gli stili di dissolvenza incrociata con movimento ridotto e in questa sezione ti mostrerò come ho collegato l'indicatore e un'area di scorrimento. A seguire, alcune funzionalità sperimentali divertenti. Spero che tu sia entusiasta quanto me.
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
Per prima cosa, controllo la preferenza di movimento dell'utente da JavaScript. Se il risultato è false
, ovvero l'utente preferisce il movimento ridotto, non verranno eseguiti
gli effetti di movimento del collegamento di scorrimento.
if (motionOK) {
// motion based animation code
}
Al momento della stesura di questo documento, il supporto del browser per
@scroll-timeline
non è disponibile. Si tratta di una
bozza di specifica con
solo implementazioni sperimentali. Tuttavia, ha un polyfill, che utilizzo in questa
demo.
ScrollTimeline
Anche se CSS e JavaScript possono creare timeline di scorrimento, ho scelto JavaScript per poter utilizzare le misurazioni degli elementi in tempo reale nell'animazione.
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection, // snap-tabs > section
orientation: 'inline', // scroll in the direction letters flow
fill: 'both', // bi-directional linking
});
Voglio che un elemento segua la posizione di scorrimento di un altro e, creando un
ScrollTimeline
, definisco il driver del collegamento di scorrimento, il scrollSource
.
Normalmente un'animazione sul web viene eseguita in base a un intervallo di tempo globale, ma con
un sectionScrollTimeline
personalizzato in memoria, posso cambiare tutto.
tabindicator.animate({
transform: ...,
width: ...,
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Prima di entrare nel dettaglio dei fotogrammi chiave dell'animazione, penso sia importante
sottolineare che il cursore di scorrimento, tabindicator
, verrà animato in base
a una sequenza temporale personalizzata, lo scorrimento della sezione. Il collegamento è completato, ma
manca l'ingrediente finale, ovvero i punti stateful tra cui animare, noti anche come
fotogrammi chiave.
Fotogrammi chiave dinamici
Esiste un modo CSS dichiarativo puro molto efficace per creare animazioni con
@scroll-timeline
, ma l'animazione che ho scelto di fare era troppo dinamica. Non è possibile
eseguire la transizione tra la larghezza di auto
e non è possibile creare dinamicamente
un numero di fotogrammi chiave in base alla lunghezza dei figli.
JavaScript sa come ottenere queste informazioni, quindi itereremo i figli e recupereremo i valori calcolati in fase di runtime:
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Per ogni tabnavitem
, destruttura la posizione offsetLeft
e restituisci una stringa
che la utilizza come valore translateX
. In questo modo vengono creati 4 fotogrammi chiave di trasformazione per l'animazione. Lo stesso vale per la larghezza: a ogni elemento viene chiesto qual è la sua larghezza dinamica
e poi viene utilizzata come valore del fotogramma chiave.
Ecco un esempio di output, basato sui miei caratteri e sulle preferenze del browser:
Fotogrammi chiave TranslateX:
[...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`)
// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]
Fotogrammi chiave della larghezza:
[...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]
Per riassumere la strategia, l'indicatore della scheda ora si anima su 4 fotogrammi chiave a seconda della posizione di scorrimento della sezione. I punti di snap creano una chiara distinzione tra i fotogrammi chiave e contribuiscono a dare un aspetto sincronizzato all'animazione.

L'utente guida l'animazione con la sua interazione, vedendo la larghezza e la posizione dell'indicatore cambiare da una sezione all'altra, seguendo perfettamente lo scorrimento.
Forse non l'hai notato, ma sono molto orgoglioso della transizione di colore quando l'elemento di navigazione evidenziato viene selezionato.
Il grigio chiaro non selezionato appare ancora più in secondo piano quando l'elemento evidenziato ha un contrasto maggiore. È comune eseguire la transizione del colore per il testo, ad esempio al passaggio del mouse e quando viene selezionato, ma è un livello successivo eseguire la transizione del colore durante lo scorrimento, in sincronia con l'indicatore di sottolineatura.
Ecco come ho fatto:
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
});
Ogni link di navigazione a schede deve avere questa nuova animazione di colore, che segue la stessa sequenza temporale di scorrimento dell'indicatore di sottolineatura. Utilizzo la stessa sequenza temporale di prima: poiché il suo ruolo è quello di emettere un segno di spunta durante lo scorrimento, possiamo utilizzarlo in qualsiasi tipo di animazione che vogliamo. Come ho fatto prima, creo 4 fotogrammi chiave nel ciclo e restituisco i colori.
[...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
// results in 4 array items, which represent 4 keyframe states
// [
"var(--text-active-color)",
"var(--text-color)",
"var(--text-color)",
"var(--text-color)",
]
Il fotogramma chiave con il colore var(--text-active-color)
evidenzia il link, mentre
il colore del testo è standard. Il ciclo nidificato rende il tutto relativamente
semplice, in quanto il ciclo esterno è ogni elemento di navigazione e il ciclo interno è ogni
fotogramma chiave personale dell'elemento di navigazione. Controllo se l'elemento del ciclo esterno è uguale a quello del ciclo interno e lo utilizzo per sapere quando è selezionato.
Mi sono divertito molto a scriverla. Tantissimo.
Ancora più miglioramenti di JavaScript
È importante ricordare che la base di ciò che ti mostro qui funziona senza JavaScript. Detto questo, vediamo come possiamo migliorarlo quando JS è disponibile.
Link diretti
I link diretti sono più un termine mobile, ma credo che l'intento del link diretto sia
soddisfatto qui con le schede, in quanto puoi condividere un URL direttamente ai contenuti di una scheda. Il
browser passerà alla navigazione in pagina all'ID corrispondente nell'hash dell'URL. Ho trovato
questo onload
gestore che ha creato l'effetto su tutte le piattaforme.
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
Sincronizzazione della fine dello scorrimento
I nostri utenti non sempre fanno clic o usano una tastiera, a volte scorrono liberamente, come dovrebbero poter fare. Quando lo scorrimento della sezione si interrompe, la sezione in cui si ferma deve corrispondere a quella della barra di navigazione in alto.
Ecco come attendo la fine dello scorrimento:
js
tabsection.addEventListener('scroll', () => {
clearTimeout(tabsection.scrollEndTimer);
tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100);
});
Ogni volta che le sezioni vengono scorrete, cancella il timeout della sezione, se presente, e avviane uno nuovo. Quando le sezioni smettono di scorrere, non cancellare il timeout e attiva l'evento 100 ms dopo l'arresto. Quando viene attivato, chiama la funzione che cerca di capire dove si è interrotto l'utente.
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth;
const matchingNavItem = tabnavitems[i];
matchingNavItem && setActiveTab(matchingNavItem);
};
Supponendo che lo scorrimento sia stato eseguito correttamente, la divisione della posizione di scorrimento corrente per la larghezza dell'area di scorrimento deve restituire un numero intero e non decimale. Poi provo a recuperare un elemento di navigazione dalla nostra cache tramite questo indice calcolato e, se trova qualcosa, invio la corrispondenza da impostare come attiva.
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active');
tabbtn.setAttribute('active', '');
tabbtn.scrollIntoView();
};
L'impostazione della scheda attiva inizia con la cancellazione di qualsiasi scheda attualmente attiva, quindi
viene assegnato all'elemento di navigazione in entrata l'attributo di stato attivo. La chiamata a scrollIntoView()
ha un'interazione divertente con CSS che merita di essere menzionata.
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
Nel CSS dell'utilità di scorrimento orizzontale, abbiamo
incorporato una query supporti che applica
lo scorrimento smooth
se l'utente è sensibile al movimento. JavaScript può effettuare liberamente chiamate per visualizzare gli elementi di scorrimento e CSS può gestire l'esperienza utente in modo dichiarativo.
A volte formano una coppia deliziosa.
Conclusione
Ora che sai come ho fatto, come faresti tu? In questo modo si ottiene un'architettura dei componenti divertente. Chi realizzerà la prima versione con gli slot nel suo framework preferito? 🙂
Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea un Glitch, inviami un tweet con la tua versione e la aggiungerò alla sezione Remix della community riportata di seguito.
Remix della community
- @devnook, @rob_dodson e @DasSurma con i componenti web: articolo.
- @jhvanderschee con i pulsanti: Codepen.