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émonstration.
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ébut, nous avons créé des onglets pour ordinateur basés sur l'élément <frame>
. Nous avons maintenant des composants de base pour mobile qui animent le contenu en fonction de propriétés physiques.
Ils ont tous le même objectif : économiser de l'espace.
Aujourd'hui, l'essentiel de l'expérience utilisateur des onglets est une zone de navigation à boutons qui active ou désactive la visibilité du contenu dans un cadre 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 géré par le navigateur et la prise en charge du partage
- Compatibilité du lecteur d'écran avec le balisage des éléments
<a>
etid="#hash"
prefers-reduced-motion
pour activer les transitions en fondu et le défilement instantané sur la page- La fonctionnalité Web
@scroll-timeline
in-draft permettant de souligner et de modifier la couleur de l'onglet sélectionné de manière dynamique
Le code HTML
L'expérience utilisateur se présente essentiellement comme suit: cliquez sur un lien, faites en sorte que l'URL représente l'état de la page imbriquée, puis observez la mise à jour de la zone de contenu à mesure que le navigateur fait défiler la page jusqu'à 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, si vous cliquez sur un lien, l'article :target
est automatiquement mis en surbrillance dans Chrome 89, sans code JS. 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 quantités variées de texte lorem ipsum, et les liens d'un ensemble de 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>
La mise en page de premier niveau que j'ai choisie était Flex (Flexbox). Je définis la direction sur column
afin que l'en-tête et la section soient ordonnés verticalement. Il s'agit de notre première fenêtre de défilement, et tout ce qui est masqué par le débordement est 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êt à ê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 de tabulation de notre clavier est accessible et toutes les interactions guident naturellement la navigation. 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 de l'en-tête des onglets <nav>
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 en fonction de 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 fortes 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 concepts. Cet élément de section s'adapte à une boîte, mais a également créé un ensemble de boîtes. J'espère que les visuels et les explications vous seront utiles.
Mise en page des onglets <article>
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 pour nous des interactions tactiles, avec la souris et avec le clavier.
<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 d'ancrer les articles dans le conteneur de défilement parent. J'aime beaucoup la façon dont les éléments du lien de navigation et les éléments de l'article s'alignent sur le début intégré de leurs conteneurs de défilement respectifs. Cela ressemble à 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ésumé des trois zones de défilement
Ci-dessous, j'ai choisi dans mes paramètres système l'option "Toujours afficher les barres de défilement". Je pense qu'il est deux fois important que la mise en page fonctionne avec ce paramètre activé, car c'est pour moi 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. Réfléchissez à la manière dont chacun de ces cadres de fenêtre de défilement est également un parent flexible ou grille d'une mise en page.
Les outils de développement peuvent nous aider à visualiser ceci:
Les dispositions de défilement sont terminées: ancrage, lien profond et accessibilité par clavier. Une base solide pour améliorer l'expérience utilisateur, le style et le plaisir.
Fonctionnalité mise en avant
Les enfants bloqués lors du défilement conservent leur position verrouillée pendant le 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 sa mise en œuvre pour qu'elle corresponde à la spécification. Voici un article de blog à ce sujet.
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 lier une tabulation soulignée à 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 et 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 pour la réduction des mouvements. De cette façon, nous n'introduisons le mouvement de défilement que pour les utilisateurs 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 pour les développeurs Chromium, je peux activer/désactiver la préférence et montrer les deux différents styles de transition. J'ai pris beaucoup de plaisir à créer cette fonctionnalité.
@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 .snap-indicator
lorsque l'utilisateur préfère que les mouvements soient réduits, 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 des couleurs de texte plus élevé et un contraste élevé de sous-éclairage.
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. Cette opération met fin à l'association, mais ne contient pas l'ingrédient final, à savoir les points avec état entre lesquels animer, ou images clés.
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 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 il est encore plus intéressant de faire passer cette couleur lors du défilement, en synchronisation avec l'indicateur de soulignement.
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 nécessite 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'auparavant : 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 je l'ai fait précédemment, je crée quatre images clés 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)",
]
L'image clé avec la couleur var(--text-active-color)
met en surbrillance le lien (sinon, il s'agit d'une couleur de texte standard). La boucle imbriquée permet de le faire relativement simplement, 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 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 l'effet sur toutes les plates-formes.
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
Faire défiler et terminer la synchronisation
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 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 les éléments jusqu'à l'affichage, et 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é ? 🙂
Diversifiez nos approches et découvrons toutes les manières de créer des applications 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.