Découvrez les principes de base de la création d'un composant switch réactif et accessible.
Dans ce post, je vais vous expliquer comment créer des composants de contacteur. Tester la fonctionnalité
<ph type="x-smartling-placeholder">Si vous préférez la vidéo, voici une version YouTube de cet article:
Présentation
Un commutateur fonctionne de la même manière qu'une case à cocher. mais qui représente explicitement les états d'activation et de désactivation booléens.
Cette démonstration utilise <input type="checkbox" role="switch">
pour la majorité de ses
qui présente l'avantage de ne pas avoir besoin de CSS ou de JavaScript
entièrement fonctionnelle et accessible. Chargement de CSS compatible avec l'écriture de droite à gauche
de langues, de verticalité, d'animation, etc. Passer au chargement en JavaScript
déplaçables et tangibles.
Propriétés personnalisées
Les variables suivantes représentent les différentes parties du commutateur et leurs
options. En tant que classe de premier niveau, .gui-switch
contient les propriétés personnalisées utilisées.
tout au long des éléments enfants des composants, et des points d'entrée pour une gestion
la personnalisation.
Suivre
La longueur (--track-size
), la marge intérieure et deux couleurs:
.gui-switch {
--track-size: calc(var(--thumb-size) * 2);
--track-padding: 2px;
--track-inactive: hsl(80 0% 80%);
--track-active: hsl(80 60% 45%);
--track-color-inactive: var(--track-inactive);
--track-color-active: var(--track-active);
@media (prefers-color-scheme: dark) {
--track-inactive: hsl(80 0% 35%);
--track-active: hsl(80 60% 60%);
}
}
Thumb
Taille, couleur d'arrière-plan et couleurs de surlignage pour les interactions:
.gui-switch {
--thumb-size: 2rem;
--thumb: hsl(0 0% 100%);
--thumb-highlight: hsl(0 0% 0% / 25%);
--thumb-color: var(--thumb);
--thumb-color-highlight: var(--thumb-highlight);
@media (prefers-color-scheme: dark) {
--thumb: hsl(0 0% 5%);
--thumb-highlight: hsl(0 0% 100% / 25%);
}
}
Mouvements réduits
Pour ajouter un alias clair et réduire les répétitions, un utilisateur de préférence de mouvement réduite la requête média peut être insérée dans une propriété personnalisée avec la fonction PostCSS du plug-in à partir de ce brouillon spécification dans les requêtes média 5:
@custom-media --motionOK (prefers-reduced-motion: no-preference);
Majoration
J'ai choisi d'encapsuler mon élément <input type="checkbox" role="switch">
avec une
<label>
, regroupant leur relation pour éviter l'association de cases à cocher et de libellés
d'ambiguïté, tout en donnant à l'utilisateur la possibilité d'interagir avec le libellé
activer/désactiver l'entrée.
<label for="switch" class="gui-switch">
Label text
<input type="checkbox" role="switch" id="switch">
</label>
<input type="checkbox">
est fourni avec un
API
et state. La
le navigateur gère
checked
une propriété et une entrée
événements
tels que oninput
et onchanged
.
Mises en page
Flexbox grid et custom propriétés sont essentielles en conservant les styles de ce composant. Ils centralisent les valeurs, donnent des noms des calculs ou des zones autrement ambigus, et permettre une petite propriété personnalisée pour personnaliser facilement les composants.
.gui-switch
La mise en page de premier niveau du commutateur est Flexbox. La classe .gui-switch
contient
les propriétés personnalisées privées et publiques que les enfants utilisent pour calculer leurs
mises en page.
.gui-switch {
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
}
L'extension et la modification de la mise en page Flexbox sont semblables aux modifications de n'importe quelle mise en page Flexbox.
Par exemple, pour placer des libellés au-dessus ou en dessous d'un commutateur, ou pour modifier le
flex-direction
:
<label for="light-switch" class="gui-switch" style="flex-direction: column">
Default
<input type="checkbox" role="switch" id="light-switch">
</label>
Suivre
L'entrée de la case à cocher est stylisée comme une piste de contacteur en supprimant le style normal
appearance: checkbox
et en fournissant sa propre taille à la place:
.gui-switch > input {
appearance: none;
inline-size: var(--track-size);
block-size: var(--thumb-size);
padding: var(--track-padding);
flex-shrink: 0;
display: grid;
align-items: center;
grid: [track] 1fr / [track] 1fr;
}
La piste crée également une zone de grille de une par une pour que le pouce la revendication.
Thumb
Le style appearance: none
supprime également la coche visuelle fournie par le
navigateur. Ce composant utilise un
pseudo-élément et :checked
pseudo-classe sur l'entrée de
remplacez cet indicateur visuel.
Le pouce est un pseudo-élément enfant associé à input[type="checkbox"]
.
s'empile en haut de la piste plutôt qu'en dessous en revendiquant la zone de la grille
track
:
.gui-switch > input::before {
content: "";
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
}
Styles
Les propriétés personnalisées offrent un composant switch polyvalent qui s'adapte à la couleur les langues qui se lisent de droite à gauche et les préférences de mouvement.
Styles d'interaction tactile
Sur mobile, les navigateurs ajoutent des fonctionnalités de mise en surbrillance d'appui et de sélection de texte aux libellés et
d'entrée. Ceux-ci ont eu un impact négatif
sur le style et le retour d'interaction visuelle qui
ce commutateur était nécessaire. Avec quelques lignes de CSS, je peux supprimer ces effets et en ajouter
mon propre style cursor: pointer
:
.gui-switch {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
Il n'est pas toujours conseillé de supprimer ces styles, car ils peuvent être utiles et d'interaction. Veillez à proposer des alternatives personnalisées si vous les supprimez.
Suivre
Les styles de cet élément dépendent principalement de sa forme et de sa couleur, auxquelles il accède
à partir de l'élément parent .gui-switch
via
cascade.
.gui-switch > input {
appearance: none;
border: none;
outline-offset: 5px;
box-sizing: content-box;
padding: var(--track-padding);
background: var(--track-color-inactive);
inline-size: var(--track-size);
block-size: var(--thumb-size);
border-radius: var(--track-size);
}
Un large éventail d'options de personnalisation du type de contacteur proviennent de quatre
propriétés personnalisées. border: none
a été ajouté, car appearance: none
ne le fait pas
supprimez les bordures de la case à cocher dans tous les navigateurs.
Thumb
L'élément "Miniature" se trouve déjà à droite de l'élément track
, mais il a besoin d'un style de cercle:
.gui-switch > input::before {
background: var(--thumb-color);
border-radius: 50%;
}
Interaction
Utiliser des propriétés personnalisées pour préparer les interactions qui entraîneront un pointage et les changements de position du curseur. La préférence de l'utilisateur est également vérifiées avant de faire passer de mise en surbrillance par mouvement ou par survol.
.gui-switch > input::before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}}
}
Position du pouce
Les propriétés personnalisées fournissent un mécanisme à source unique pour positionner le pouce dans
la piste. Nous disposons de la taille des pistes et des pouces que nous utiliserons pour
pour que le pouce soit bien décalé sur la piste et entre les différentes zones:
0%
et 100%
.
L'élément input
possède la variable de position --thumb-position
et le curseur.
pseudo-élément l'utilise comme position translateX
:
.gui-switch > input {
--thumb-position: 0%;
}
.gui-switch > input::before {
transform: translateX(var(--thumb-position));
}
Nous sommes maintenant libres de modifier --thumb-position
dans le CSS et les pseudo-classes.
fournies sur les éléments de case à cocher. Étant donné que nous avons précédemment défini transition: transform
var(--thumb-transition-duration) ease
de manière conditionnelle sur cet élément, ces modifications
peuvent s'animer lorsqu'elles sont modifiées:
/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
}
/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
}
Je pensais que cette orchestration découplée avait bien fonctionné. L'élément pouce est
ne concerne qu'un seul style, la position translateX
. L'entrée peut gérer toutes
la complexité et les calculs.
Vertical
La prise en charge a été effectuée à l'aide d'une classe de modificateur -vertical
, qui ajoute une rotation avec
CSS se transforme en élément input
.
En revanche, la rotation d'un élément en 3D ne modifie pas la hauteur globale du composant.
ce qui peut perturber la mise en page en bloc. Prenez en compte ces informations à l'aide des --track-size
et
Variables --track-padding
. Calculer l'espace minimal
nécessaire pour
un bouton vertical pour s'insérer comme prévu dans la mise en page:
.gui-switch.-vertical {
min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));
& > input {
transform: rotate(-90deg);
}
}
de droite à gauche (RTL)
Elad Schecter, ami CSS, avec qui j'ai prototypé et un menu latéral coulissant à l'aide de transformations CSS qui gèrent l'écriture de droite à gauche langues en inversant une seule . Nous avons procédé ainsi, car il n'y a pas de transformation de propriété logique en CSS, et il n'y en aura peut-être jamais. Elad a eu l'idée d'utiliser une valeur de propriété personnalisée pour inverser les pourcentages, afin de permettre la gestion d'un seul emplacement de nos propres pour les transformations logiques. J'ai utilisé cette même technique pour ce commutateur et j'ai que cela s'est très bien déroulé:
.gui-switch {
--isLTR: 1;
&:dir(rtl) {
--isLTR: -1;
}
}
Une propriété personnalisée appelée --isLTR
contient initialement la valeur 1
, ce qui signifie qu'elle est
true
, car la mise en page s'affiche de gauche à droite par défaut. Ensuite, à l'aide du code CSS,
pseudo-classe :dir()
,
La valeur est définie sur -1
lorsque le composant se trouve dans une mise en page de droite à gauche.
Utilisez --isLTR
dans une calc()
à l'intérieur d'une transformation:
.gui-switch.-vertical > input {
transform: rotate(-90deg);
transform: rotate(calc(90deg * var(--isLTR) * -1));
}
Maintenant, la rotation du commutateur vertical tient compte de la position du côté opposé requise par la mise en page de droite à gauche.
Les transformations translateX
du pseudo-élément Thumb doivent également être mises à jour vers
tenir compte de l'exigence de la partie opposée:
.gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
.gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
--thumb-position: calc(
((var(--track-size) / 2) - (var(--thumb-size) / 2))
* var(--isLTR)
);
}
Bien que cette approche ne réponde pas à tous les besoins liés à un concept comme le CSS logique il offre une certaine principes DRY pour de nombreux différents cas d'utilisation.
États
L'utilisation de l'input[type="checkbox"]
intégré ne serait pas complète sans
en gérant les différents états possibles: :checked
, :disabled
,
:indeterminate
et :hover
. :focus
a été délibérément laissé seul, avec un
l'ajustement effectué uniquement sur son décalage ; l'anneau de focus était parfait
dans Firefox et
Safari:
Coché
<label for="switch-checked" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>
Cet état représente l'état on
. Dans cet état, l'entrée "suivre"
l'arrière-plan est défini sur la couleur active et la position du pouce est définie sur
fin".
.gui-switch > input:checked {
background: var(--track-color-active);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
Désactivé
<label for="switch-disabled" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>
Un bouton :disabled
a une apparence différente, mais doit également rendre
L'immuabilité de l'interaction est exempte du navigateur, mais
Les états visuels nécessitent des styles en raison de l'utilisation de appearance: none
.
.gui-switch > input:disabled {
cursor: not-allowed;
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);
@media (prefers-color-scheme: dark) { & {
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
}}
}
}
Cet état est délicat, car il nécessite des thèmes sombre et clair, avec des fonctionnalités cochés. J'ai choisi des styles minimalistes pour faciliter l'utilisation de ces états la charge de maintenance des combinaisons de styles.
Indéterminé
:indeterminate
est un état souvent oublié, où une case à cocher n'est ni
cochée ou décochée. Cet état est amusant, accueillant et sobre. Un bon
rappelez-vous que les états booléens peuvent avoir un caractère trompeur entre les états.
Il est délicat de définir une case à cocher sur "indéterminé", seul JavaScript peut le définir:
<label for="switch-indeterminate" class="gui-switch">
Indeterminate
<input type="checkbox" role="switch" id="switch-indeterminate">
<script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>
Puisque l'État, selon moi, est simple et accueillant, il m'a semblé approprié de mettre position du curseur au milieu:
.gui-switch > input:indeterminate {
--thumb-position: calc(
calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
* var(--isLTR)
);
}
Survol
Les interactions avec le passage de la souris doivent offrir une assistance visuelle pour l'interface utilisateur connectée, mais aussi pour orienter l'interface utilisateur interactive. Ce bouton bascule met en évidence le pouce avec un anneau semi-transparent lorsque l'utilisateur pointe sur le libellé ou l'entrée. Ce survol puis oriente le curseur vers l'élément interactif "pique".
Le "point fort" l'effet est effectué avec box-shadow
. Lorsque l'utilisateur pointe sur une entrée non désactivée, augmentez la taille de --highlight-size
. Si l'utilisateur est d'accord avec le mouvement, la box-shadow
change d'apparence et s'affiche. S'il ne convient pas au mouvement, la mise en surbrillance apparaît instantanément:
.gui-switch > input::before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}}
}
.gui-switch > input:not(:disabled):hover::before {
--highlight-size: .5rem;
}
JavaScript
Pour moi, une interface de commutateur peut sembler étrange dans sa tentative d'émuler un une interface utilisateur, en particulier celle-ci avec un cercle à l'intérieur d'une piste. iOS a trouvé la bonne réponse avec leur bouton bascule, vous pouvez les faire glisser d'un côté à l'autre, et c'est très satisfaisant de qui ont le choix. À l'inverse, un élément d'interface utilisateur peut sembler inactif si un geste de glissement est et que rien ne se passe.
Pouce déplaçable
Le pseudo-élément "Thumb" reçoit sa position à partir de .gui-switch > input
définie sur var(--thumb-position)
, JavaScript peut fournir une valeur de style intégré sur
l'entrée pour mettre à jour de façon dynamique la position du pouce, de sorte qu'elle semble suivre
le geste du pointeur. Lorsque le pointeur est relâché, supprimez les styles intégrés et
Déterminez si le déplacement était plus proche de la position réelle ou de la désactivation à l'aide de la propriété personnalisée
--thumb-position
Il s’agit de l’épine dorsale
de la solution ; événements de pointeur
suivre la position du pointeur de manière conditionnelle afin de modifier les propriétés personnalisées du CSS.
Étant donné que le composant était déjà fonctionnel à 100% avant l'affichage de ce script il faut beaucoup de travail pour maintenir le comportement existant, en cliquant sur une étiquette pour activer/désactiver l'entrée. Notre JavaScript ne doit pas ajouter de fonctionnalités au niveau au détriment des fonctionnalités existantes.
touch-action
Le glissement est un geste, un geste personnalisé, ce qui en fait un excellent candidat pour
Avantages touch-action
. Dans le cas de ce commutateur, un geste horizontal doit
être gérée par notre script, ou un geste vertical capturé pour le bouton bascule vertical
variante d'origine. Avec touch-action
, nous pouvons indiquer au navigateur les gestes à gérer
afin qu'un script puisse gérer un geste sans concurrence.
Le code CSS suivant indique au navigateur que lorsqu'un geste du pointeur commence dans ce circuit de commutation, gérer les gestes verticaux, ne rien faire avec uns:
.gui-switch > input {
touch-action: pan-y;
}
Le résultat souhaité est un geste horizontal qui ne fait pas défiler . Un pointeur peut faire défiler verticalement l'élément à partir de l'entrée et faire défiler les mais les horizontaux sont traités de manière personnalisée.
Utilitaires du style de valeur Pixel
Lors de la configuration et du déplacement, diverses valeurs numériques calculées devront être récupérées.
des éléments. Les fonctions JavaScript suivantes renvoient des valeurs en pixels calculées
en fonction d'une propriété CSS. Il est utilisé dans le script de configuration comme ceci :
getStyle(checkbox, 'padding-left')
const getStyle = (element, prop) => {
return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}
const getPseudoStyle = (element, prop) => {
return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}
export {
getStyle,
getPseudoStyle,
}
Notez que window.getComputedStyle()
accepte un deuxième argument, un pseudo-élément cible. C'est intéressant, JavaScript peut lire autant de valeurs à partir d'éléments, même de pseudo-éléments.
dragging
C'est un moment central de la logique du drag, et il y a quelques points à noter à partir du gestionnaire d'événements de la fonction:
const dragging = event => {
if (!state.activethumb) return
let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
let directionality = getStyle(state.activethumb, '--isLTR')
let track = (directionality === -1)
? (state.activethumb.clientWidth * -1) + thumbsize + padding
: 0
let pos = Math.round(event.offsetX - thumbsize / 2)
if (pos < bounds.lower) pos = 0
if (pos > bounds.upper) pos = bounds.upper
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}
Le héros du script est state.activethumb
, le petit cercle dont est issu ce script
avec un pointeur. L'objet switches
est un Map()
où le
les clés sont des .gui-switch
, et les valeurs sont des limites et des tailles mises en cache qui conservent
l'efficacité du script. L'orientation de droite à gauche est gérée à l'aide de la même propriété personnalisée.
que le CSS est --isLTR
, et qu'il peut l'utiliser pour inverser la logique et continuer
compatible avec les langues RTL. event.offsetX
est également intéressant, car il contient un delta.
utile pour positionner le pouce.
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
Cette dernière ligne du CSS définit la propriété personnalisée utilisée par l'élément Thumb. Ce
l'attribution de valeur changerait au fil du temps, mais un pointeur précédent
l'événement a défini temporairement --thumb-transition-duration
sur 0s
, ce qui a entraîné la suppression de l'élément
aurait été une interaction faible.
dragEnd
Pour que l'utilisateur puisse faire glisser un événement de la fenêtre globale nécessaire enregistré:
window.addEventListener('pointerup', event => {
if (!state.activethumb) return
dragEnd(event)
})
Je pense qu'il est très important qu'un utilisateur ait la liberté de glisser librement et d'avoir être suffisamment intelligente pour en tenir compte. Il n'a pas fallu beaucoup de temps pour gérer cette situation avec ce changement, mais il a dû être mûrement réfléchi pendant le développement processus.
const dragEnd = event => {
if (!state.activethumb) return
state.activethumb.checked = determineChecked()
if (state.activethumb.indeterminate)
state.activethumb.indeterminate = false
state.activethumb.style.removeProperty('--thumb-transition-duration')
state.activethumb.style.removeProperty('--thumb-position')
state.activethumb.removeEventListener('pointermove', dragging)
state.activethumb = null
padRelease()
}
L'interaction avec l'élément est terminée, il est temps de définir l'entrée coché
et supprimer tous les événements gestuels. La case à cocher est remplacée par
state.activethumb.checked = determineChecked()
determineChecked()
Cette fonction, appelée par dragEnd
, détermine l'emplacement du courant du pouce.
dans les limites de sa piste et renvoie "true" si la valeur est égale ou supérieure à
à mi-chemin:
const determineChecked = () => {
let {bounds} = switches.get(state.activethumb.parentElement)
let curpos =
Math.abs(
parseInt(
state.activethumb.style.getPropertyValue('--thumb-position')))
if (!curpos) {
curpos = state.activethumb.checked
? bounds.lower
: bounds.upper
}
return curpos >= bounds.middle
}
Pensées supplémentaires
Le geste de déplacement entraînait une certaine quantité de code en raison de la structure HTML initiale.
en particulier l'encapsulation de l'entrée dans une étiquette. Le libellé, en tant que parent
, recevrait des interactions de clic après l'entrée. À la fin de
dragEnd
, vous avez peut-être remarqué que le son de padRelease()
était bizarre
.
const padRelease = () => {
state.recentlyDragged = true
setTimeout(_ => {
state.recentlyDragged = false
}, 300)
}
Cela permet de tenir compte du libellé qui entraîne le clic ultérieur, comme il le ferait décocher, ou vérifier, l'interaction qu'un utilisateur a effectuée.
Si je devais recommencer, j'envisage peut-être d'ajuster DOM avec JavaScript pendant la mise à niveau de l'expérience utilisateur, par exemple pour créer un élément qui gère lui-même les clics sur les libellés et ne lutte pas contre le comportement intégré.
C'est ce type de code JavaScript que j'aime le moins, ébullition d'événements conditionnels:
const preventBubbles = event => {
if (state.recentlyDragged)
event.preventDefault() && event.stopPropagation()
}
Conclusion
Ce petit bouton bascule a fini par être le plus difficile de tous les défis de l'IUG pour l'instant ! Maintenant que vous savez comment j'ai fait, comment feriez-vous ? 😃
Diversifiez nos approches et découvrons toutes les manières de créer des applications sur le Web. Créer une démonstration, me envoyer des tweets et je l'ajouterai à la section des remix de la communauté ci-dessous.
Remix de la communauté
- @KonstantinRouda avec un élément personnalisé: demo et code.
- @jhvanderschee avec un bouton: Codepen.
Ressources
Recherchez le code source .gui-switch
sur
GitHub.