Présentation de base sur la création d'un composant de bouton fractionné accessible.
Dans cet article, je vais vous expliquer comment créer un bouton fractionné. Tester la fonctionnalité
Si vous préférez regarder une vidéo, voici une version YouTube de cet article :
Présentation
Les boutons fractionnés sont des boutons qui cachent un bouton principal et une liste de boutons supplémentaires. Ils sont utiles pour exposer une action courante tout en imbriquant des actions secondaires, moins fréquemment utilisées, jusqu'à ce qu'elles soient nécessaires. Un bouton fractionné peut être essentiel pour donner un aspect minimaliste à une conception chargée. Un bouton de fractionnement avancé peut même mémoriser la dernière action de l'utilisateur et la promouvoir en position principale.
Vous trouverez un bouton de répartition courant dans votre application de messagerie. L'action principale est l'envoi, mais vous pouvez peut-être envoyer le message plus tard ou enregistrer un brouillon :
La zone d'action partagée est agréable, car l'utilisateur n'a pas besoin de regarder autour de lui. Il sait que les actions essentielles pour les e-mails sont contenues dans le bouton de division.
Pièces
Décomposons les éléments essentiels d'un bouton fractionné avant d'examiner leur orchestration globale et l'expérience utilisateur finale. L'outil d'inspection de l'accessibilité de VisBug est utilisé ici pour afficher une vue macro du composant, en affichant des aspects du code HTML, du style et de l'accessibilité pour chaque partie principale.
Conteneur de bouton de fractionnement de niveau supérieur
Le composant de premier niveau est un flexbox intégré, avec une classe gui-split-button
, contenant l'action principale et .gui-popup-button
.
Bouton d'action principal
Le <button>
, initialement visible et pouvant être mis au point, s'intègre dans le conteneur avec deux formes angulaires correspondant pour que les interactions en mode focus, en survol et actives apparaissent dans .gui-split-button
.
Bouton d'activation des pop-ups
L'élément d'assistance "bouton pop-up" permet d'activer et d'évoquer la liste des boutons secondaires. Notez qu'il ne s'agit pas d'un <button>
et qu'il n'est pas sélectionnable. Toutefois, il s'agit de l'ancre de positionnement pour .gui-popup
et de l'hôte de :focus-within
utilisé pour présenter le pop-up.
Carte pop-up
Il s'agit d'une carte flottante enfant de son ancre .gui-popup-button
, positionnée de manière absolue et encapsulant sémantiquement la liste de boutons.
Les actions secondaires
Un <button>
pouvant être sélectionné avec une taille de police légèrement inférieure à celle du bouton d'action principal comporte une icône et un style complémentaire à celui du bouton principal.
Propriétés personnalisées
Les variables suivantes permettent de créer une harmonie de couleurs et un emplacement central pour modifier les valeurs utilisées dans l'ensemble du composant.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);
.gui-split-button {
--theme: hsl(220 75% 50%);
--theme-hover: hsl(220 75% 45%);
--theme-active: hsl(220 75% 40%);
--theme-text: hsl(220 75% 25%);
--theme-border: hsl(220 50% 75%);
--ontheme: hsl(220 90% 98%);
--popupbg: hsl(220 0% 100%);
--border: 1px solid var(--theme-border);
--radius: 6px;
--in-speed: 50ms;
--out-speed: 300ms;
@media (--dark) {
--theme: hsl(220 50% 60%);
--theme-hover: hsl(220 50% 65%);
--theme-active: hsl(220 75% 70%);
--theme-text: hsl(220 10% 85%);
--theme-border: hsl(220 20% 70%);
--ontheme: hsl(220 90% 5%);
--popupbg: hsl(220 10% 30%);
}
}
Mises en page et couleur
Annoter
L'élément commence par un <div>
avec un nom de classe personnalisé.
<div class="gui-split-button"></div>
Ajoutez le bouton principal et les éléments .gui-popup-button
.
<div class="gui-split-button">
<button>Send</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>
Notez les attributs aria aria-haspopup
et aria-expanded
. Ces signaux sont essentiels pour que les lecteurs d'écran soient informés des fonctionnalités et de l'état de l'expérience du bouton fractionné. L'attribut title
est utile pour tous.
Ajoutez une icône <svg>
et l'élément de conteneur .gui-popup
.
<div class="gui-split-button">
<button>Send</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
<svg aria-hidden="true" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
<ul class="gui-popup"></ul>
</span>
</div>
Pour un positionnement simple du pop-up, .gui-popup
est un enfant du bouton qui l'étend. Le seul inconvénient de cette stratégie est que le conteneur .gui-split-button
ne peut pas utiliser overflow: hidden
, car il empêcherait la fenêtre pop-up d'être visuellement présente.
Un élément <ul>
rempli de contenus <li><button>
s'annonce comme une "liste de boutons" auprès des lecteurs d'écran, ce qui est précisément l'interface présentée.
<div class="gui-split-button">
<button>Send</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
<svg aria-hidden="true" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
<ul class="gui-popup">
<li>
<button>Schedule for later</button>
</li>
<li>
<button>Delete</button>
</li>
<li>
<button>Save draft</button>
</li>
</ul>
</span>
</div>
Pour ajouter du style et m'amuser avec les couleurs, j'ai ajouté des icônes aux boutons secondaires sur https://heroicons.com. Les icônes sont facultatives pour les boutons principaux et secondaires.
<div class="gui-split-button">
<button>Send</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
<svg aria-hidden="true" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
<ul class="gui-popup">
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Schedule for later
</button></li>
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete
</button></li>
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
Save draft
</button></li>
</ul>
</span>
</div>
Styles
Une fois le code HTML et le contenu en place, les styles sont prêts à fournir la couleur et la mise en page.
Attribuer un style au conteneur du bouton fractionné
Un type d'affichage inline-flex
convient parfaitement à ce composant de mise en page, car il doit s'intégrer aux autres boutons, actions ou éléments de fractionnement.
.gui-split-button {
display: inline-flex;
border-radius: var(--radius);
background: var(--theme);
color: var(--ontheme);
fill: var(--ontheme);
touch-action: manipulation;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
Style <button>
Les boutons sont très efficaces pour masquer la quantité de code nécessaire. Vous devrez peut-être annuler ou remplacer les styles par défaut du navigateur, mais vous devrez également appliquer un certain héritage, ajouter des états d'interaction et vous adapter à diverses préférences et types d'entrée utilisateur. Les styles de boutons s'accumulent rapidement.
Ces boutons sont différents des boutons standards, car ils partagent un arrière-plan avec un élément parent. En général, un bouton possède son propre arrière-plan et sa propre couleur de texte. En revanche, elles le partagent et n'appliquent que leur propre arrière-plan à l'interaction.
.gui-split-button button {
cursor: pointer;
appearance: none;
background: none;
border: none;
display: inline-flex;
align-items: center;
gap: 1ch;
white-space: nowrap;
font-family: inherit;
font-size: inherit;
font-weight: 500;
padding-block: 1.25ch;
padding-inline: 2.5ch;
color: var(--ontheme);
outline-color: var(--theme);
outline-offset: -5px;
}
Ajoutez des états d'interaction avec quelques pseudo-classes CSS et utilisez des propriétés personnalisées correspondantes pour l'état :
.gui-split-button button {
…
&:is(:hover, :focus-visible) {
background: var(--theme-hover);
color: var(--ontheme);
& > svg {
stroke: currentColor;
fill: none;
}
}
&:active {
background: var(--theme-active);
}
}
Le bouton principal a besoin de quelques styles spéciaux pour compléter l'effet de conception:
.gui-split-button > button {
border-end-start-radius: var(--radius);
border-start-start-radius: var(--radius);
& > svg {
fill: none;
stroke: var(--ontheme);
}
}
Enfin, pour apporter une touche d'élégance, le bouton et l'icône du thème clair présentent une ombre:
.gui-split-button {
@media (--light) {
& > button,
& button:is(:focus-visible, :hover) {
text-shadow: 0 1px 0 var(--theme-active);
}
& > .gui-popup-button > svg,
& button:is(:focus-visible, :hover) > svg {
filter: drop-shadow(0 1px 0 var(--theme-active));
}
}
}
Un bouton de qualité a été conçu en tenant compte des micro-interactions et des détails.
Remarque concernant :focus-visible
Notez que les styles de boutons utilisent :focus-visible
au lieu de :focus
. :focus
est un élément essentiel pour créer une interface utilisateur accessible, mais il présente un inconvénient : il ne détermine pas de manière intelligente si l'utilisateur doit le voir ou non. Il s'applique à tout focus.
La vidéo ci-dessous tente de décomposer cette micro-interaction pour montrer en quoi :focus-visible
est une alternative intelligente.
Attribuer un style au bouton du pop-up
Une flexbox 4ch
pour centrer une icône et ancrer une liste de boutons pop-up. Comme le bouton principal, il est transparent jusqu'à ce que l'utilisateur passe la souris dessus ou interagisse avec lui, et il s'étire pour remplir l'espace.
.gui-popup-button {
inline-size: 4ch;
cursor: pointer;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border-inline-start: var(--border);
border-start-end-radius: var(--radius);
border-end-end-radius: var(--radius);
}
Superposez les états survol, focus et actif avec l'imbrication CSS et le sélecteur fonctionnel :is()
:
.gui-popup-button {
…
&:is(:hover,:focus-within) {
background: var(--theme-hover);
}
/* fixes iOS trying to be helpful */
&:focus {
outline: none;
}
&:active {
background: var(--theme-active);
}
}
Ces styles sont le principal crochet pour afficher et masquer le pop-up. Lorsque .gui-popup-button
contient focus
sur l'un de ses enfants, définissez opacity
, la position et pointer-events
sur l'icône et le pop-up.
.gui-popup-button {
…
&:focus-within {
& > svg {
transition-duration: var(--in-speed);
transform: rotateZ(.5turn);
}
& > .gui-popup {
transition-duration: var(--in-speed);
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
}
}
Une fois les styles d'entrée et de sortie terminés, la dernière étape consiste à effectuer une transition de manière conditionnelle en fonction des préférences de mouvement de l'utilisateur:
.gui-popup-button {
…
@media (--motionOK) {
& > svg {
transition: transform var(--out-speed) ease;
}
& > .gui-popup {
transform: translateY(5px);
transition:
opacity var(--out-speed) ease,
transform var(--out-speed) ease;
}
}
}
En observant attentivement le code, vous remarquerez que l'opacité est toujours en transition pour les utilisateurs qui préfèrent réduire le mouvement.
Styliser le pop-up
L'élément .gui-popup
est une liste de boutons de carte flottante qui utilise des propriétés personnalisées et des unités relatives pour être légèrement plus petite, correspondre de manière interactive au bouton principal et respecter la marque avec son utilisation de la couleur. Notez que les icônes ont moins de contraste, sont plus fines et que l'ombre est légèrement bleue. Comme pour les boutons, une UI et une expérience utilisateur solides sont le résultat de l'empilement de ces petits détails.
.gui-popup {
--shadow: 220 70% 15%;
--shadow-strength: 1%;
opacity: 0;
pointer-events: none;
position: absolute;
bottom: 80%;
left: -1.5ch;
list-style-type: none;
background: var(--popupbg);
color: var(--theme-text);
padding-inline: 0;
padding-block: .5ch;
border-radius: var(--radius);
overflow: hidden;
display: flex;
flex-direction: column;
font-size: .9em;
transition: opacity var(--out-speed) ease;
box-shadow:
0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
;
}
Les icônes et les boutons sont associés à des couleurs de marque pour s'intégrer parfaitement dans chaque carte au thème sombre et clair :
.gui-popup {
…
& svg {
fill: var(--popupbg);
stroke: var(--theme);
@media (prefers-color-scheme: dark) {
stroke: var(--theme-border);
}
}
& button {
color: var(--theme-text);
width: 100%;
}
}
Le pop-up du thème sombre comporte des ombres de texte et d'icône, ainsi qu'une ombre de zone légèrement plus intense:
.gui-popup {
…
@media (--dark) {
--shadow-strength: 5%;
--shadow: 220 3% 2%;
& button:not(:focus-visible, :hover) {
text-shadow: 0 1px 0 var(--ontheme);
}
& button:not(:focus-visible, :hover) > svg {
filter: drop-shadow(0 1px 0 var(--ontheme));
}
}
}
Styles d'icônes <svg>
génériques
Toutes les icônes sont dimensionnées par rapport au bouton font-size
dans lequel elles sont utilisées en utilisant l'unité ch
comme inline-size
. Chacune d'elles est également associée à des styles pour aider à dessiner des icônes douces et fluides.
.gui-split-button svg {
inline-size: 2ch;
box-sizing: content-box;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2px;
}
Mise en page de droite à gauche
Les propriétés logiques effectuent tout le travail complexe.
Voici la liste des propriétés logiques utilisées :
- display: inline-flex
crée un élément flex intégré.
- padding-block
et padding-inline
en paire, au lieu de l'abréviation padding
, profitez des avantages du rembourrage des côtés logiques.
- border-end-start-radius
et ses amis arrondissent les coins en fonction de l'orientation du document.
- inline-size
plutôt que width
garantit que la taille n'est pas liée aux dimensions physiques.
- border-inline-start
ajoute une bordure au début, qui peut être à droite ou à gauche, en fonction du sens du script.
JavaScript
La quasi-totalité du code JavaScript suivant vise à améliorer l'accessibilité. Deux de mes bibliothèques d'assistance sont utilisées pour faciliter les tâches. BlingBlingJS est utilisé pour des requêtes DOM succinctes et une configuration facile de l'écouteur d'événements, tandis que roving-ux facilite l'accessibilité des interactions avec le clavier et la manette de jeu pour le pop-up.
import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'
const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')
Une fois les bibliothèques ci-dessus importées, et les éléments sélectionnés et enregistrés dans des variables, la mise à niveau de l'expérience ne nécessite que quelques fonctions.
Indice itinérant
Lorsqu'un clavier ou un lecteur d'écran met en surbrillance le .gui-popup-button
, nous souhaitons transmettre la sélection au premier bouton (ou au bouton sélectionné en dernier) de la .gui-popup
. La bibliothèque nous aide à le faire avec les paramètres element
et target
.
popupButtons.forEach(element =>
rovingIndex({
element,
target: 'button',
}))
L'élément transmet désormais la sélection aux enfants <button>
cibles et permet de naviguer dans les options à l'aide des touches fléchées standards.
Activer/Désactiver aria-expanded
Bien qu'il soit visuellement évident qu'une fenêtre pop-up s'affiche et se ferme, un lecteur d'écran a besoin de plus que de repères visuels. JavaScript est utilisé ici pour compléter l'interaction :focus-within
basée sur le CSS en activant un attribut adapté aux lecteurs d'écran.
popupButtons.on('focusin', e => {
e.currentTarget.setAttribute('aria-expanded', true)
})
popupButtons.on('focusout', e => {
e.currentTarget.setAttribute('aria-expanded', false)
})
Activer la clé Escape
L'attention de l'utilisateur a été intentionnellement envoyée vers un piège, ce qui signifie que nous devons lui fournir un moyen de sortir. La méthode la plus courante consiste à autoriser l'utilisation de la clé Escape
.
Pour ce faire, surveillez les frappes sur le bouton du pop-up, car tous les événements de clavier sur les enfants remonteront jusqu'à ce parent.
popupButtons.on('keyup', e => {
if (e.code === 'Escape')
e.target.blur()
})
Si le bouton du pop-up détecte une pression sur la touche Escape
, il supprime le focus sur lui-même avec blur()
.
Clics sur le bouton de fractionnement
Enfin, si l'utilisateur clique, appuie ou interagit avec les boutons à l'aide du clavier, l'application doit effectuer l'action appropriée. La remontée d'événements est à nouveau utilisée ici, mais cette fois sur le conteneur .gui-split-button
, pour capturer les clics sur les boutons d'un pop-up enfant ou de l'action principale.
splitButtons.on('click', event => {
if (event.target.nodeName !== 'BUTTON') return
console.info(event.target.innerText)
})
Conclusion
Maintenant que vous savez comment j'ai fait, comment procéderiez-vous ? 🙂
Diversifions nos approches et découvrons toutes les façons de créer sur le Web. Créez une démo, envoyez-moi des tweets via des liens et je l'ajouterai à la section "Remix de la communauté" ci-dessous.