Una panoramica di base su come creare un componente delle schede simile a quelli presenti nelle app per iOS e Android.
In questo post voglio condividere alcune idee sulla creazione di un componente Tabs per il web che sia responsive, supporti più input di 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. In precedenza, le schede desktop erano basate sull'elemento <frame>
, ma 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 con schede sono un'area di navigazione con pulsanti che attiva/disattiva la visibilità dei contenuti in un riquadro di visualizzazione. Molte diverse area di contenuti 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 abbastanza semplice da creare, grazie a alcune funzionalità fondamentali della piattaforma web:
scroll-snap-points
per interazioni eleganti con scorrimento e tastiera con posizioni di arresto dello scorrimento appropriate- Link diretti tramite hash dell'URL per supportare la condivisione e l'ancoraggio dello scorrimento in-page gestito dal browser
- Supporto per screen reader con markup degli elementi
<a>
eid="#hash"
prefers-reduced-motion
per attivare le transizioni con dissolvenza incrociata e lo scorrimento istantaneo all'interno della pagina- La funzionalità web
@scroll-timeline
in bozza per sottolineare e cambiare dinamicamente il colore della scheda selezionata
Il codice HTML
Fondamentalmente, l'esperienza utente è la seguente: fai clic su un link, l'URL rappresenta lo stato della pagina nidificata e poi vedi l'aggiornamento dell'area dei contenuti quando il browser scorre fino all'elemento corrispondente.
Sono presenti alcuni elementi di contenuti strutturali: link e :target
. Abbiamo bisogno di un elenco di link, per il quale è ideale un <nav>
, e di un elenco di elementi <article>
, per il quale è ideale un <section>
. Ogni hash del link corrisponderà a una sezione,
consentendo al browser di scorrere i contenuti tramite l'ancoraggio.
Ad esempio, facendo clic su un link, l'articolo :target
viene automaticamente messo a fuoco in Chrome 89, senza bisogno di JS. L'utente può quindi scorrere i contenuti dell'articolo con il suo dispositivo di input come sempre. Si tratta di contenuti aggiuntivi, 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
come segue:
<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>
Poi ho riempito gli articoli con quantità diverse di Lorem e i link con titoli di lunghezza e immagini diverse. Ora che abbiamo i contenuti, possiamo iniziare a creare il layout.
Layout scorrevoli
In questo componente sono presenti tre diversi tipi di aree di scorrimento:
- La barra di navigazione (rosa) è scorrevole orizzontalmente
- L'area dei contenuti (blu) è scorrevole orizzontalmente
- Ogni articolo (verde) è scorrevole verticalmente.
Esistono due tipi di elementi coinvolti nello scorrimento:
- Una finestra
Una casella con dimensioni definite che ha lo stile della proprietàoverflow
. - Una superficie di grandi dimensioni
In questo layout, si tratta dei contenitori dell'elenco: link di navigazione, articoli della sezione e contenuti degli articoli.
Layout <snap-tabs>
Ho scelto il layout di primo livello flessibile (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 con overflow nascosto. A breve, l'intestazione e la sezione utilizzeranno lo scorrimento oltre il limite, come singole zone.
<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; } }
Torna al diagramma colorato con tre scorrimenti:
<header>
è ora pronto per essere il contenitore scorrevole (rosa).<section>
è pronto per essere il contenitore scorrevole (blu).
I frame che ho evidenziato di seguito con VisBug ci aiutano a vedere le finestre create dai contenitori di scorrimento.
Layout <header>
schede
Il layout successivo è quasi uguale: utilizzo flex per creare l'ordinamento 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 scorrere orizzontalmente con il gruppo di link e questo layout dell'intestazione aiuta a impostare questa fase. Nessun elemento con posizionamento assoluto.
Poi, gli stili di scorrimento. A quanto pare possiamo condividere gli stili di scorrimento tra le due aree di scorrimento orizzontali (intestazione e sezione), quindi ho creato una classe di utilità, .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 richiede uno spazio aggiuntivo sull'asse x, un contenimento dello scorrimento per bloccare lo scorrimento eccessivo, barre di scorrimento nascoste per i dispositivi touch e infine lo snap dello scorrimento per bloccare le aree di presentazione dei contenuti. L'ordine delle schede della tastiera è accessibile e qualsiasi interazione guida il focus in modo naturale. I contenitori con scorrimento automatico offrono anche un'interazione di tipo carosello con la tastiera.
Layout <nav>
dell'intestazione delle schede
I link di navigazione devono essere disposti in una riga, senza interruzioni di riga, centrati verticalmente e ogni elemento del link deve essere agganciato al contenitore di scorrimento. Swift work per il CSS 2021.
<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 imposta automaticamente stili e dimensioni, quindi il layout della barra 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 larghezza in base al nuovo target. A seconda del numero di elementi, il browser mostrerà o meno una barra di scorrimento.
Layout <section>
schede
Questa sezione è un elemento flessibile e deve essere il principale consumatore di spazio. Deve anche creare colonne in cui inserire gli articoli. Ancora una volta, ottimo lavoro per il CSS 2021. block-size: 100%
allunga questo elemento per riempire il parent il più possibile, quindi per il proprio layout crea una serie di colonne che hanno la larghezza del parent.100%
Le percentuali sono perfette in questo caso
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 impostata su flex-shrink: 0
: è una difesa contro questa
spinta all'espansione), che imposta l'altezza della riga per un insieme di colonne a altezza intera. Lo stile auto-flow
indica alla griglia di disporre sempre gli elementi secondari in una riga orizzontale, senza a capo, esattamente ciò che vogliamo; per andare oltre la finestra principale.
A volte mi risultano difficili da comprendere. 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 di aiuto.
Layout <article>
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 sono in una posizione ordinata. Sono contemporaneamente elementi principali e secondari dello scorrimento. Il browser sta gestendo 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 far scattare gli articoli all'interno dello scorrevole principale. Mi piace molto come gli elementi link di navigazione e gli elementi dell'articolo si agganciano all'elemento inline-start dei rispettivi contenitori di scorrimento. Sembra una relazione armoniosa.
L'articolo è un elemento secondario della griglia e le sue dimensioni sono predeterminate in base all'area della visualizzazione della pagina in cui vogliamo fornire l'esperienza utente di scorrimento. Ciò significa che non ho bisogno di stili di altezza o larghezza, devo solo definire come si verifica il superamento. Ho impostato overflow-y su auto, e poi ho intrappolato anche le interazioni di scorrimento con la pratica proprietà overscroll-behavior.
Riepilogo delle 3 aree di scorrimento
Di seguito ho scelto nelle impostazioni di sistema di "mostrare sempre le barre di scorrimento". Penso che sia doppiamente importante che il layout funzioni con questa impostazione attivata, così come lo è per me esaminare il layout e l'orchestrazione dello scorrimento.
Penso che la visualizzazione della barra laterale in questo componente aiuti a mostrare chiaramente dove si trovano le aree di scorrimento, la direzione supportata e il modo in cui interagiscono tra loro. Tieni presente che ognuno di questi frame della finestra di scorrimento è anche un elemento principale flex o grid di un layout.
DevTools può aiutarci a visualizzare quanto segue:
I layout di scorrimento sono completi: con aggancio, link diretti e accessibili da tastiera. Una base solida per miglioramenti dell'esperienza utente, stile e soddisfazione.
Funzionalità in evidenza
I riquadri secondari a scorrimento bloccati mantengono la posizione bloccata durante il ridimensionamento. Ciò significa che JavaScript non dovrà mostrare nulla quando il dispositivo viene ruotato o le dimensioni del browser vengono modificate. Prova la funzionalità nella Modalità dispositivo di Chromium DevTools selezionando una modalità diversa da Adattabile e ridimensionando il riquadro del dispositivo. L'elemento rimane visibile e bloccato con i suoi contenuti. Questa funzionalità è stata disponibile da quando Chromium ha aggiornato la propria implementazione in modo da renderla conforme alle specifiche. Ecco un post del blog in merito.
Animazione
L'obiettivo dell'animazione è collegare chiaramente le interazioni con il feedback della UI. In questo modo, l'utente può trovare (si spera) senza problemi tutti i contenuti. Aggiungerò il movimento in modo mirato e condizione. 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ò un'evidenziazione della scheda alla posizione di scorrimento dell'articolo. L'aggancio non è solo un allineamento estetico, ma serve anche ad 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. Esistono alcuni posti fantastici da prendere in considerazione.
Comportamento di scorrimento
C'è un'opportunità per migliorare il comportamento dei movimenti sia di :target
sia di
element.scrollIntoView()
. Per impostazione predefinita, è istantaneo. Il browser imposta solo la posizione di scorrimento. E se volessimo passare a quella posizione di scorrimento,
invece di farla lampeggiare?
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
Poiché stiamo introducendo un movimento, e un movimento che l'utente non controlla (come lo scorrimento), applichiamo questo stile solo se l'utente non ha preferenze nel suo sistema operativo in merito al movimento ridotto. In questo modo, introduciamo il movimento scorrimento solo per le persone che lo accettano.
Indicatore delle schede
Lo scopo di questa animazione è aiutare ad associare l'indicatore allo stato
degli elementi. Ho deciso di applicare stili di transizione sfumata border-bottom
a colori per gli utenti che preferiscono ridurre il movimento e un'animazione di scorrimento con transizione sfumata e scorrimento collegato per gli utenti che non hanno problemi con il movimento.
In Chromium DevTools, posso attivare/disattivare la preferenza e mostrare i 2 diversi stili di transizione. Mi sono divertito molto 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 il pulsante .snap-indicator
quando l'utente preferisce la modalità Riduzione movimento, poiché non mi serve più. Poi lo sostituisco con gli stili border-block-end
e un
transition
. Inoltre, nell'interazione con le schede, tieni presente che l'elemento di navigazione attivo non solo ha un'evidenziazione del brand in sottolineato, ma anche il colore del testo è più scuro. L'elemento attivo ha un contrasto del colore del testo più elevato e un'illuminazione sottoscocca brillante.
Bastano alcune righe di CSS aggiuntive 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 transizione graduale con movimento ridotto e in questa sezione ti mostrerò come ho collegato l'indicatore a un'area di scorrimento. Ecco alcune funzionalità sperimentali divertenti che verranno implementate a breve. Spero che tu sia tanto 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 un movimento ridotto, non verrà eseguito nessuno degli effetti di movimento di collegamento allo scorrimento.
if (motionOK) {
// motion based animation code
}
Al momento della stesura di questo documento, il supporto dei 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
Sebbene sia CSS sia JavaScript possano creare schemi di scorrimento, ho attivato 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 gestore del link di scorrimento, ovvero il scrollSource
.
In genere, 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 passare ai fotogrammi chiave dell'animazione, ritengo sia importante sottolineare che il cursore dello scorrimento, tabindicator
, verrà animato in base a una sequenza temporale personalizzata, ovvero lo scorrimento della nostra sezione. Il collegamento è completo, ma manca l'ingrediente finale: i punti con stato tra cui eseguire l'animazione, noti anche come keyframe.
Fotogrammi chiave dinamici
Esiste un modo molto efficace per creare animazioni con CSS puramente dichiarativo
@scroll-timeline
, ma l'animazione che ho scelto era troppo dinamica. Non è possibile eseguire la transizione tra le larghezze auto
e non è possibile creare dinamicamente un numero di keyframe in base alla durata dei figli.
JavaScript sa come ottenere queste informazioni, quindi eseguiremo l'iterazione sui figli e acquisiremo i valori calcolati in fase di esecuzione:
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
. Vengono creati quattro fotogrammi chiave di trasformazione per l'animazione. Lo stesso vale per la larghezza, a ogni elemento viene chiesto qual è la sua larghezza dinamica, che viene poi utilizzata come valore della keyframe.
Ecco un esempio di output, in base ai miei caratteri e alle 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 riepilogare la strategia, l'indicatore della scheda ora verrà animato su quattro fotogrammi chiave, a seconda della posizione di snap dello scorrimento dello scorrevole della sezione. I punti di aggancio creano una chiara demarcazione tra i fotogrammi chiave e contribuiscono notevolmente all'effetto sincronizzato dell'animazione.
L'utente gestisce l'animazione con la propria 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 più chiaro non selezionato appare ancora più arretrato quando l'elemento evidenziato ha un maggiore contrasto. È comune applicare una transizione di colore al testo, ad esempio al passaggio del mouse e quando è selezionato, ma è di livello superiore applicare una transizione di colore allo scorrimento, sincronizzata 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 tra le schede ha bisogno di 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 è emettere un segno di spunta durante lo scorrimento, possiamo utilizzarlo in qualsiasi tipo di animazione. Come ho fatto prima, creo 4 fotogrammi chiave nel loop 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, altrimenti è un colore di testo standard. Il ciclo nidificato lo rende relativamente semplice, poiché il ciclo esterno è ogni elemento di navigazione e il ciclo interno è costituito dai keyframe personali di ogni elemento di navigazione. Controllo se l'elemento del ciclo esterno è uguale a quello del ciclo interno e lo uso per sapere quando è selezionato.
Mi sono divertito molto a scriverlo. Tantissimo.
Ulteriori miglioramenti a JavaScript
Vale la pena ricordare che la parte principale 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 per i dispositivi mobili, ma penso che lo scopo del link diretto sia raggiunto qui con le schede in quanto puoi condividere un URL direttamente con i contenuti di una scheda. Il browser si sposterà all'interno della pagina fino all'ID corrispondente nell'hash dell'URL. Ho scoperto che questo gestore onload
ha applicato l'effetto su più piattaforme.
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
Sincronizzazione della fine dello scorrimento
I nostri utenti non fanno sempre clic o non usano sempre una tastiera, a volte si limitano a scorrimento libero, come dovrebbero poter fare. Quando lo scorrimento della sezione si interrompe, la posizione in cui si ferma deve corrispondere a quella nella 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 scorri le sezioni, cancella il relativo timeout, se esistente, e avviane uno nuovo. Quando non viene più eseguito lo scorrimento delle sezioni, non cancellare il timeout e attivalo 100 ms dopo l'interruzione. Quando viene attivata, chiama la funzione che cerca di capire dove è stato interrotto l'utente.
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth;
const matchingNavItem = tabnavitems[i];
matchingNavItem && setActiveTab(matchingNavItem);
};
Supponendo che lo scorrimento sia agganciato, la divisione della posizione di scorrimento corrente per la larghezza dell'area di scorrimento deve produrre un numero intero e non un numero decimale. Poi provo a recuperare un navitem dalla nostra cache tramite questo indice calcolato e, se ne trova uno, invio la corrispondenza da impostare come attiva.
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active');
tabbtn.setAttribute('active', '');
tabbtn.scrollIntoView();
};
Per impostare la scheda attiva, inizia eliminando qualsiasi scheda attualmente attiva, quindi assegna all'elemento di navigazione in arrivo l'attributo dello stato attivo. La chiamata a scrollIntoView()
ha un'interazione divertente con il CSS che vale la pena di notare.
.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 snap dello scorrimento orizzontale abbiamo
nidificato una query sui media che applica scorrimentosmooth
se l'utente è tollerante al movimento. JavaScript può effettuare liberamente chiamate per far scorrere gli elementi in visualizzazione e il CSS può gestire l'esperienza utente in modo dichiarativo.
A volte sono una coppia davvero carina.
Conclusione
Ora che sai come ho fatto, come faresti? Si tratta di un'architettura di componenti molto interessante. 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 di seguito.
Remix della community
- @devnook, @rob_dodson e @DasSurma con Web Components: articolo.
- @jhvanderschee con i pulsanti: Codepen.