Présentation de base sur la création d'un composant de tabulation semblable à ceux que vous trouverez dans les applications iOS et Android.
Dans cet article, je souhaite partager mes réflexions sur la création d'un composant Tabs pour le Web qui est responsif, compatible avec plusieurs entrées d'appareils et compatible avec tous les navigateurs. Essayez la démo.
Si vous préférez regarder une vidéo, voici une version YouTube de cet article:
Présentation
Les onglets sont un composant courant des systèmes de conception, mais ils peuvent prendre de nombreuses formes. Au départ, il y avait des onglets pour ordinateur basés sur l'élément <frame>
, et maintenant nous avons des composants mobiles fluides qui animent le contenu en fonction des propriétés physiques.
Ils ont tous le même objectif: gagner de l'espace.
Aujourd'hui, l'essentiel de l'expérience utilisateur des onglets est une zone de navigation par bouton qui active et désactive la visibilité du contenu dans un frame d'affichage. De nombreuses zones de contenu différentes partagent le même espace, mais sont présentées de manière conditionnelle en fonction du bouton sélectionné dans la navigation.
Stratégies Web
Dans l'ensemble, j'ai trouvé ce composant assez simple à créer, grâce à quelques fonctionnalités essentielles de la plate-forme Web:
scroll-snap-points
pour des interactions élégantes avec le balayage et le clavier avec des positions d'arrêt de défilement appropriées- Liens profonds via des hachages d'URL pour l'ancrage du défilement sur la page et la prise en charge du partage gérés par le navigateur
- Compatibilité avec les lecteurs d'écran grâce au balisage des éléments
<a>
etid="#hash"
prefers-reduced-motion
pour activer les transitions en fondu et le défilement instantané sur la page- Fonctionnalité Web
@scroll-timeline
en cours d'élaboration pour souligner et modifier dynamiquement la couleur de l'onglet sélectionné
Le code HTML
L'expérience utilisateur est fondamentalement la suivante: cliquez sur un lien, laissez l'URL représenter l'état de la page imbriquée, puis observez la mise à jour de la zone de contenu lorsque le navigateur fait défiler l'élément correspondant.
Il contient des membres de contenu structurels: des liens et des :target
. Nous avons besoin d'une liste de liens, pour laquelle <nav>
est idéal, et d'une liste d'éléments <article>
, pour laquelle <section>
est idéal. Chaque hachage de lien correspond à une section, ce qui permet au navigateur de faire défiler les éléments via l'ancrage.
Par exemple, cliquer sur un lien met automatiquement en surbrillance l'article :target
dans Chrome 89, sans JS requis. L'utilisateur peut ensuite faire défiler le contenu de l'article avec son appareil de saisie comme d'habitude. Il s'agit d'un contenu complémentaire, comme indiqué dans la balise.
J'ai utilisé le balisage suivant pour organiser les onglets:
<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>
Je peux établir des connexions entre les éléments <a>
et <article>
avec les propriétés href
et id
comme suit:
<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>
J'ai ensuite rempli les articles de différentes quantités de texte lorem ipsum, et les liens avec des titres de longueur et d'images variées. Maintenant que nous avons du contenu à utiliser, nous pouvons commencer à mettre en page.
Mises en page à défilement
Ce composant comporte trois types de zones de défilement:
- La barre de navigation (rose) est à défilement horizontal.
- La zone de contenu (bleue) est à défilement horizontal.
- Chaque article (vert) est à faire défiler verticalement.
Deux types d'éléments sont impliqués dans le défilement:
- Fenêtre
Cadre aux dimensions définies avec le style de propriétéoverflow
. - Une surface surdimensionnée
Dans cette mise en page, il s'agit des conteneurs de liste: liens de navigation, articles de section et contenu des articles.
Mise en page <snap-tabs>
J'ai choisi la mise en page de niveau supérieur "flex" (Flexbox). J'ai défini la direction sur column
, de sorte que l'en-tête et la section soient classés verticalement. Il s'agit de notre première fenêtre de défilement, qui masque tout avec un débordement masqué. L'en-tête et la section utiliseront bientôt le défilement excessif, en tant que zones individuelles.
<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; } }
En revenant au diagramme coloré à trois défilements:
<header>
est maintenant prêt à être le conteneur de défilement (rose).<section>
est préparé pour être le conteneur de défilement (bleu).
Les cadres que j'ai mis en surbrillance ci-dessous avec VisBug nous aident à voir les fenêtres créées par les conteneurs de défilement.
Mise en page <header>
des onglets
La mise en page suivante est presque identique: j'utilise flex pour créer un ordre vertical.
<snap-tabs> <header> <nav></nav> <span class="snap-indicator"></span> </header> <section></section> </snap-tabs>
header { display: flex; flex-direction: column; }
.snap-indicator
doit se déplacer horizontalement avec le groupe de liens, et cette mise en page d'en-tête permet de le faire. Aucun élément positionné de manière absolue ici !
Ensuite, les styles de défilement. Il s'avère que nous pouvons partager les styles de défilement entre nos deux zones de défilement horizontales (en-tête et section). J'ai donc créé une classe utilitaire, .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;
}
}
}
Chacune nécessite un débordement sur l'axe X, une structuration du défilement pour piéger le défilement excessif, des barres de défilement masquées pour les appareils tactiles et enfin un accrochage de défilement pour verrouiller les zones de présentation du contenu. L'ordre des onglets du clavier est accessible, et toutes les interactions guident naturellement la sélection. Les conteneurs de recadrage de défilement bénéficient également d'une belle interaction de style carrousel à partir de leur clavier.
Mise en page <nav>
de l'en-tête des onglets
Les liens de navigation doivent être disposés sur une ligne, sans saut de ligne, être centrés verticalement, et chaque élément de lien doit s'ancrer au conteneur de point d'ancrage de défilement. Travail rapide pour le 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; } }
Chaque lien se stylise et se dimensionne lui-même. La mise en page de la navigation n'a donc qu'à spécifier la direction et le flux. Les largeurs uniques des éléments de navigation rendent la transition entre les onglets amusante, car l'indicateur ajuste sa largeur à la nouvelle cible. En fonction du nombre d'éléments, le navigateur affiche une barre de défilement ou non.
Mise en page <section>
des onglets
Cette section est un élément flexible et doit être le principal consommateur d'espace. Il doit également créer des colonnes dans lesquelles placer les articles. Encore une fois, un travail rapide pour CSS 2021 ! block-size: 100%
étire cet élément pour le remplir autant que possible, puis pour sa propre mise en page, il crée une série de colonnes dont la largeur est 100%
celle du parent. Les pourcentages fonctionnent très bien ici, car nous avons écrit des contraintes strictes sur le parent.
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
C'est comme si nous disions "Étendez-vous verticalement autant que possible, de manière agressive" (rappelez-vous l'en-tête que nous avons défini sur flex-shrink: 0
: il s'agit d'une défense contre cette poussée d'expansion), ce qui définit la hauteur de ligne pour un ensemble de colonnes pleine hauteur. Le style auto-flow
indique à la grille de toujours disposer les enfants sur une ligne horizontale, sans retour à la ligne, exactement ce que nous voulons : déborder de la fenêtre parente.
Je trouve parfois difficile de comprendre ces règles. Cet élément de section s'adapte à une zone, mais crée également un ensemble de zones. J'espère que les visuels et les explications vous seront utiles.
Mise en page <article>
des onglets
L'utilisateur doit pouvoir faire défiler le contenu de l'article, et les barres de défilement ne doivent s'afficher que s'il y a un débordement. Ces éléments de l'article sont bien positionnés. Ils sont à la fois un parent de défilement et un enfant de défilement. Le navigateur gère ici des interactions tactiles, de souris et de clavier délicates.
<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; }
J'ai choisi de faire en sorte que les articles s'enclenchent dans leur élément parent. J'aime beaucoup la façon dont les éléments de lien de navigation et les éléments de l'article s'enclenchent au début en ligne de leurs conteneurs de défilement respectifs. Il semble que ce soit une relation harmonieuse.
L'article est un élément enfant de la grille, et sa taille est prédéterminée pour correspondre à la zone de la fenêtre d'affichage que nous souhaitons fournir pour l'expérience utilisateur de défilement. Cela signifie que je n'ai pas besoin de styles de hauteur ou de largeur ici. Je dois simplement définir la façon dont il déborde. J'ai défini overflow-y sur "auto", puis j'ai également piégé les interactions de défilement avec la propriété overscroll-behavior.
Récapitulatif des trois zones de défilement
Dans les paramètres système, j'ai choisi d'afficher toujours les barres de défilement. Je pense qu'il est doublement important que la mise en page fonctionne avec ce paramètre activé, car il m'est nécessaire d'examiner la mise en page et l'orchestration du défilement.
Je pense que la présence de la marge de la barre de défilement dans ce composant permet de montrer clairement où se trouvent les zones de défilement, la direction qu'elles acceptent et la façon dont elles interagissent les unes avec les autres. Notez que chacun de ces cadres de fenêtre de défilement est également un parent de flex ou de grille pour une mise en page.
Les outils de développement peuvent nous aider à visualiser cela:
Les mises en page de défilement sont complètes: elles permettent de faire un "snap", d'ajouter des liens profonds et d'être accessibles au clavier. Des bases solides pour améliorer l'expérience utilisateur, le style et le plaisir.
Fonctionnalité mise en avant
Les enfants ancrés au défilement conservent leur position verrouillée lors du redimensionnement. Cela signifie que JavaScript n'a pas besoin d'afficher quoi que ce soit lors de la rotation de l'appareil ou du redimensionnement du navigateur. Essayez-le dans le mode Appareil de Chromium DevTools en sélectionnant un mode autre que Responsive, puis en redimensionnant le cadre de l'appareil. Notez que l'élément reste visible et verrouillé avec son contenu. Cette fonctionnalité est disponible depuis que Chromium a mis à jour son implémentation pour qu'elle corresponde à la spécification. Pour en savoir plus, consultez cet article de blog.
Animation
L'objectif de l'animation ici est de relier clairement les interactions aux commentaires de l'interface utilisateur. Cela permet de guider ou d'aider l'utilisateur à découvrir (si possible) tous les contenus. J'ajouterai du mouvement de manière intentionnelle et conditionnelle. Les utilisateurs peuvent désormais spécifier leurs préférences de mouvement dans leur système d'exploitation, et j'aime beaucoup répondre à leurs préférences dans mes interfaces.
Je vais associer un soulignement d'onglet à la position de défilement de l'article. L'ancrage n'est pas seulement un alignement esthétique, il permet également d'ancrer le début et la fin d'une animation.
Cela permet de maintenir la <nav>
, qui agit comme une mini-carte, connectée au contenu.
Nous allons vérifier les préférences de mouvement de l'utilisateur à la fois en CSS et en JS. Il existe plusieurs endroits où faire preuve de considération.
Comportement de défilement
Il est possible d'améliorer le comportement de mouvement de :target
et de element.scrollIntoView()
. Par défaut, il est instantané. Le navigateur définit simplement la position de défilement. Que se passe-t-il si nous voulons passer à cette position de défilement au lieu de clignoter ?
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
Étant donné que nous introduisons ici un mouvement que l'utilisateur ne contrôle pas (comme le défilement), nous n'appliquons ce style que si l'utilisateur n'a aucune préférence dans son système d'exploitation concernant la réduction du mouvement. De cette façon, nous n'introduisons le mouvement de défilement que pour les personnes qui l'acceptent.
Indicateur d'onglets
L'objectif de cette animation est d'aider à associer l'indicateur à l'état du contenu. J'ai décidé de colorer les styles border-bottom
de fondu croisé pour les utilisateurs qui préfèrent réduire le mouvement, et une animation de défilement liée au glissement et au fondu des couleurs pour les utilisateurs qui acceptent le mouvement.
Dans les outils de développement Chromium, je peux activer/désactiver la préférence et montrer les deux styles de transition différents. J'ai pris beaucoup de plaisir à créer cette expérience.
@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;
}
}
Je masque le .snap-indicator
lorsque l'utilisateur préfère réduire le mouvement, car je n'en ai plus besoin. Je le remplace ensuite par des styles border-block-end
et un transition
. Notez également que, dans l'interaction des onglets, l'élément de navigation actif est non seulement mis en surbrillance par un soulignement de la marque, mais que sa couleur de texte est également plus foncée. L'élément actif présente un contraste de couleur de texte plus élevé et une accentuation lumineuse sous-jacente vive.
Quelques lignes de code CSS supplémentaires suffisent à donner l'impression que l'utilisateur est vu (dans le sens où nous respectons ses préférences de mouvement). J'adore.
@scroll-timeline
Dans la section ci-dessus, je vous ai montré comment gérer les styles de fondu croisé à mouvement réduit. Dans cette section, je vais vous montrer comment associer l'indicateur et une zone de défilement. Voici quelques fonctionnalités expérimentales amusantes à venir. J'espère que vous êtes aussi enthousiaste que moi.
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
Je vérifie d'abord les préférences de mouvement de l'utilisateur à partir de JavaScript. Si le résultat est false
, ce qui signifie que l'utilisateur préfère réduire le mouvement, aucun des effets de mouvement de liaison de défilement ne sera exécuté.
if (motionOK) {
// motion based animation code
}
Au moment de la rédaction de cet article, la compatibilité des navigateurs avec @scroll-timeline
est nulle. Il s'agit d'une spécification provisoire avec des implémentations expérimentales uniquement. Il dispose toutefois d'un polyfill, que j'utilise dans cette démonstration.
ScrollTimeline
Bien que CSS et JavaScript puissent tous deux créer des timelines de défilement, j'ai activé JavaScript pour pouvoir utiliser des mesures d'éléments en direct dans l'animation.
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection, // snap-tabs > section
orientation: 'inline', // scroll in the direction letters flow
fill: 'both', // bi-directional linking
});
Je souhaite qu'un élément suive la position de défilement d'un autre. En créant un ScrollTimeline
, je définis le pilote du lien de défilement, le scrollSource
.
Normalement, une animation sur le Web s'exécute sur un "tic" de période globale, mais avec un sectionScrollTimeline
personnalisé en mémoire, je peux tout changer.
tabindicator.animate({
transform: ...,
width: ...,
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Avant d'aborder les images clés de l'animation, je pense qu'il est important de souligner que le suiveur du défilement, tabindicator
, sera animé en fonction d'une timeline personnalisée, le défilement de notre section. La liaison est ainsi terminée, mais l'ingrédient final, à savoir les points d'état à animer entre eux, également appelés "keyframes", est manquant.
Images clés dynamiques
Il existe une méthode CSS déclarative très puissante pour animer avec @scroll-timeline
, mais l'animation que j'ai choisie était trop dynamique. Il n'est pas possible de faire la transition entre la largeur auto
, et il n'est pas possible de créer dynamiquement un nombre de clés-images en fonction de la longueur des enfants.
JavaScript sait cependant comment obtenir ces informations. Nous allons donc itérer sur les enfants et récupérer les valeurs calculées au moment de l'exécution:
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Pour chaque tabnavitem
, déstructurez la position offsetLeft
et renvoyez une chaîne qui l'utilise comme valeur translateX
. Cela crée quatre images clés de transformation pour l'animation. Il en va de même pour la largeur. On demande à chacun quelle est sa largeur dynamique, puis on l'utilise comme valeur de clé-image.
Voici un exemple de résultat, basé sur mes polices et mes préférences de navigateur:
Images clés 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)"]
Images clés de largeur:
[...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]
Pour résumer la stratégie, l'indicateur d'onglet s'animera désormais sur quatre clés-images en fonction de la position de l'ancrage de défilement de la barre de défilement de la section. Les points d'ancrage créent une délimitation claire entre nos images clés et renforcent vraiment l'impression de synchronisation de l'animation.
L'utilisateur contrôle l'animation par son interaction, et voit la largeur et la position de l'indicateur changer d'une section à l'autre, en suivant parfaitement le défilement.
Vous ne l'avez peut-être pas remarqué, mais je suis très fier de la transition de couleur lorsque l'élément de navigation mis en surbrillance est sélectionné.
Le gris clair non sélectionné apparaît encore plus en retrait lorsque l'élément mis en surbrillance présente un contraste plus élevé. Il est courant de faire passer le texte d'une couleur à une autre, par exemple lorsque l'utilisateur le survole ou le sélectionne, mais passer cette couleur lors du défilement, en synchronisation avec l'indicateur de soulignement, est une fonctionnalité de niveau supérieur.
Voici comment j'ai procédé:
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
});
Chaque lien de navigation dans les onglets a besoin de cette nouvelle animation de couleur, qui suit la même chronologie de défilement que l'indicateur de soulignement. J'utilise la même chronologie qu'avant: comme son rôle est d'émettre un "tic" lors du défilement, nous pouvons utiliser ce "tic" dans n'importe quel type d'animation. Comme précédemment, je crée quatre keyframes dans la boucle et renvoie des couleurs.
[...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)",
]
La clé-image de couleur var(--text-active-color)
met en évidence le lien, et la couleur du texte est par ailleurs standard. La boucle imbriquée ici est relativement simple, car la boucle externe correspond à chaque élément de navigation, et la boucle interne correspond aux clés-images personnelles de chaque élément de navigation. Je vérifie si l'élément de la boucle externe est identique à celui de la boucle interne, et je l'utilise pour savoir quand il est sélectionné.
J'ai beaucoup aimé écrire cet article. Vraiment.
Encore plus d'améliorations JavaScript
N'oubliez pas que l'essentiel de ce que je vous montre ici fonctionne sans JavaScript. Cela dit, voyons comment l'améliorer lorsque JS sera disponible.
Liens profonds
Les liens profonds sont plutôt un terme utilisé sur mobile, mais je pense que l'objectif du lien profond est atteint ici avec les onglets, car vous pouvez partager une URL directement vers le contenu d'un onglet. Le navigateur accède à l'ID correspondant dans le hachage de l'URL. J'ai constaté que ce gestionnaire onload
avait un effet sur les plates-formes.
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
Synchronisation de la fin du défilement
Nos utilisateurs ne cliquent pas toujours ni n'utilisent pas toujours un clavier. Parfois, ils font simplement défiler l'écran, comme ils devraient pouvoir le faire. Lorsque le défilement de la section s'arrête, l'élément sur lequel il se trouve doit être mis en correspondance dans la barre de navigation supérieure.
Voici comment j'attends la fin du défilement :
js
tabsection.addEventListener('scroll', () => {
clearTimeout(tabsection.scrollEndTimer);
tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100);
});
Chaque fois que les sections défilent, effacez le délai avant expiration de la section, le cas échéant, et démarrez-en un nouveau. Lorsque le défilement des sections s'arrête, ne supprimez pas le délai avant expiration et déclenchez-le 100 ms après le repos. Lorsqu'il se déclenche, appelez la fonction qui cherche à déterminer où l'utilisateur s'est arrêté.
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth;
const matchingNavItem = tabnavitems[i];
matchingNavItem && setActiveTab(matchingNavItem);
};
En supposant que le défilement est calé, diviser la position de défilement actuelle par la largeur de la zone de défilement doit donner un entier et non un nombre décimal. J'essaie ensuite de récupérer un élément de navigation à partir de notre cache via cet indice calculé. Si quelque chose est trouvé, j'envoie la correspondance à définir comme active.
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active');
tabbtn.setAttribute('active', '');
tabbtn.scrollIntoView();
};
Pour définir l'onglet actif, commencez par effacer tout onglet actuellement actif, puis attribuez à l'élément de navigation entrant l'attribut d'état actif. L'appel à scrollIntoView()
a une interaction amusante avec CSS qui mérite d'être notée.
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
Dans l'utilitaire CSS de glissement horizontal, nous avons imbriqué une requête multimédia qui applique le défilement smooth
si l'utilisateur est tolérant aux mouvements. JavaScript peut effectuer librement des appels pour faire défiler des éléments dans la vue, et le CSS peut gérer l'expérience utilisateur de manière déclarative.
Ils forment parfois un duo délicieux.
Conclusion
Maintenant que vous savez comment j'ai fait, comment pourriez-vous faire ? Cela permet d'obtenir une architecture de composants amusante. Qui va créer la première version avec des emplacements dans son framework préféré ? 🙂
Diversifions nos approches et découvrons toutes les façons de créer sur le Web. Créez un Glitch, tweetez-moi votre version, et je l'ajouterai à la section Remix de la communauté ci-dessous.
Remix de la communauté
- @devnook, @rob_dodson et @DasSurma avec les composants Web: article.
- @jhvanderschee avec les boutons: Codepen.