Présentation de base sur la création d'un composant de paramètres composé de curseurs et de cases à cocher.
Dans cet article, je souhaite partager mes réflexions sur la création d'un composant de paramètres 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 une vidéo ou un aperçu de l'UI/UX de ce que nous créons, voici une visite guidée plus courte sur YouTube:
Présentation
J'ai divisé les aspects de ce composant en sections:
- Mises en page
- Couleur
- Saisie de plage personnalisée
- Saisie de case à cocher personnalisée
- Considérations sur l'accessibilité
- JavaScript
Mises en page
Il s'agit de la première démonstration de défi d'interface utilisateur entièrement basée sur la grille CSS. Voici chaque grille mise en surbrillance avec les outils pour les développeurs Chrome pour la grille:
Juste pour la coupure
La mise en page la plus courante:
foo {
display: grid;
gap: var(--something);
}
J'appelle cette mise en page "juste pour l'espace", car elle n'utilise que la grille pour ajouter des espaces entre les blocs.
Cinq mises en page utilisent cette stratégie. Voici toutes les mises en page:
L'élément fieldset
, qui contient chaque groupe d'entrée (.fieldset-item
), utilise gap: 1px
pour créer les bordures fines entre les éléments. Aucune solution de bordure délicate !
.grid { display: grid; gap: 1px; background: var(--bg-surface-1); & > .fieldset-item { background: var(--bg-surface-2); } }
.grid { display: grid; & > .fieldset-item { background: var(--bg-surface-2); &:not(:last-child) { border-bottom: 1px solid var(--bg-surface-1); } } }
Retour à la ligne naturel dans la grille
La mise en page la plus complexe s'est avérée être la mise en page macro, le système de mise en page logique entre <main>
et <form>
.
Centrer le contenu encapsulant
Flexbox et la grille offrent tous deux des fonctionnalités à align-items
ou align-content
. Lorsque vous travaillez avec des éléments de mise en page, les alignements de mise en page content
distribuent l'espace entre les enfants en tant que groupe.
main {
display: grid;
gap: var(--space-xl);
place-content: center;
}
L'élément principal utilise le saisies d'alignement place-content: center
afin que les enfants soient centrés verticalement et horizontalement dans les mises en page à une et deux colonnes.
Dans la vidéo ci-dessus, vous pouvez voir que le "contenu" reste centré, même si le retour à la ligne a eu lieu.
Répéter l'ajustement automatique minimal/maximal
<form>
utilise une mise en page de grille adaptative pour chaque section.
Cette mise en page passe d'une à deux colonnes en fonction de l'espace disponible.
form {
display: grid;
gap: var(--space-xl) var(--space-xxl);
grid-template-columns: repeat(auto-fit, minmax(min(10ch, 100%), 35ch));
align-items: flex-start;
max-width: 89vw;
}
Cette grille a une valeur différente pour row-gap
(--space-xl) que pour column-gap
(--space-xxl) pour apporter une touche personnalisée à la mise en page responsive. Lorsque les colonnes se superposent, nous souhaitons un espace important, mais pas aussi important que si nous utilisions un écran large.
La propriété grid-template-columns
utilise trois fonctions CSS: repeat()
, minmax()
et min()
. Una Kravets a écrit un excellent article de blog sur la mise en page à ce sujet, qu'elle appelle RAM.
Notre mise en page comporte trois ajouts particuliers par rapport à celle d'Una:
- Nous transmettons une fonction
min()
supplémentaire. - Nous spécifions
align-items: flex-start
. - Il existe un style
max-width: 89vw
.
La fonction min()
supplémentaire est bien décrite par Evan Minto sur son blog dans l'article Intrinsically Responsive CSS Grid with minmax() and min(). Je vous recommande de le lire. La correction d'alignement flex-start
permet de supprimer l'effet d'étirement par défaut, de sorte que les enfants de cette mise en page n'aient pas besoin d'avoir des hauteurs égales, mais des hauteurs naturelles et intrinsèques. La vidéo YouTube présente brièvement cette fonctionnalité.
max-width: 89vw
mérite une petite analyse dans cet article.
Je vais vous montrer la mise en page avec et sans le style appliqué:
Que se passe-t-il ? Lorsque max-width
est spécifié, il fournit un contexte, une taille explicite ou une taille définie pour l'algorithme de mise en page auto-fit
afin de savoir combien de répétitions il peut adapter à l'espace. Bien qu'il semble évident que l'espace soit "pleine largeur", conformément aux spécifications de la grille CSS, une taille ou une taille maximale définie doit être fournie. J'ai indiqué une taille maximale.
Pourquoi 89vw
? Parce que "ça a marché" pour ma mise en page.
Avec quelques autres membres de l'équipe Chrome, nous essayons de comprendre pourquoi une valeur plus raisonnable, comme 100vw
, n'est pas suffisante et si cela s'agit en fait d'un bug.
Espacement
La majorité de l'harmonie de cette mise en page provient d'une palette limitée d'espacements, sept pour être exact.
:root {
--space-xxs: .25rem;
--space-xs: .5rem;
--space-sm: 1rem;
--space-md: 1.5rem;
--space-lg: 2rem;
--space-xl: 3rem;
--space-xxl: 6rem;
}
L'utilisation de ces flux est très pratique avec la grille, CSS @nest et la syntaxe de niveau 5 de @media. Voici un exemple, l'ensemble de styles de mise en page <main>
complet.
main {
display: grid;
gap: var(--space-xl);
place-content: center;
padding: var(--space-sm);
@media (width >= 540px) {
& {
padding: var(--space-lg);
}
}
@media (width >= 800px) {
& {
padding: var(--space-xl);
}
}
}
Grille avec contenu centré, avec une marge modérée par défaut (comme sur mobile). Toutefois, à mesure que l'espace de la fenêtre d'affichage devient disponible, il se répartit en augmentant la marge intérieure. Le CSS de 2021 est plutôt prometteur !
Vous souvenez-vous de la mise en page précédente, "juste pour l'espace" ? Voici une version plus complète de leur apparence dans ce composant:
header {
display: grid;
gap: var(--space-xxs);
}
section {
display: grid;
gap: var(--space-md);
}
Couleur
L'utilisation contrôlée de la couleur a permis à cette conception de se démarquer comme étant expressive, mais minimaliste. Je procède comme suit:
:root {
--surface1: lch(10 0 0);
--surface2: lch(15 0 0);
--surface3: lch(20 0 0);
--surface4: lch(25 0 0);
--text1: lch(95 0 0);
--text2: lch(75 0 0);
}
Je nomme mes couleurs de surface et de texte avec des nombres plutôt que des noms comme surface-dark
et surface-darker
, car dans une requête multimédia, je les inverserai, et les couleurs claires et sombres n'auront pas de sens.
Je les inverse dans une requête de médias de préférence comme suit:
:root {
...
@media (prefers-color-scheme: light) {
& {
--surface1: lch(90 0 0);
--surface2: lch(100 0 0);
--surface3: lch(98 0 0);
--surface4: lch(85 0 0);
--text1: lch(20 0 0);
--text2: lch(40 0 0);
}
}
}
Il est important d'avoir un aperçu rapide de la stratégie et de l'image globale avant de nous plonger dans les détails de la syntaxe des couleurs. Mais comme je suis un peu en avance, laissez-moi revenir en arrière.
LCH ?
Sans trop entrer dans la théorie des couleurs, le LCH est une syntaxe orientée vers l'humain, qui tient compte de la façon dont nous percevons la couleur, et non de la façon dont nous la mesurons avec des mathématiques (comme 255). Cela présente un avantage distinctif, car les humains peuvent l'écrire plus facilement et les autres humains seront en phase avec ces ajustements.
Pour aujourd'hui, dans cette démonstration, concentrons-nous sur la syntaxe et les valeurs que je bascule pour créer des tons clairs et sombres. Examinons une surface et une couleur de texte:
:root {
--surface1: lch(10 0 0);
--text1: lch(95 0 0);
@media (prefers-color-scheme: light) {
& {
--surface1: lch(90 0 0);
--text1: lch(40 0 0);
}
}
}
--surface1: lch(10 0 0)
correspond à une luminosité 10%
, un chroma de 0 et une teinte de 0: un gris incolore très sombre. Ensuite, dans la requête multimédia pour le mode clair, la luminosité est définie sur 90%
avec --surface1: lch(90 0 0);
. Et c'est l'essentiel de la stratégie. Commencez par modifier simplement la luminosité entre les deux thèmes, en conservant les ratios de contraste requis par la conception ou qui peuvent maintenir l'accessibilité.
L'avantage de lch()
ici est que la légèreté est axée sur l'humain. Nous pouvons donc être satisfaits d'un changement de %
, car il sera perçu et cohérent.%
hsl()
, par exemple, n'est pas aussi fiable.
Pour en savoir plus sur les espaces de couleurs et lch()
, consultez les ressources supplémentaires. C'est en cours !
Pour le moment, le CSS ne peut pas du tout accéder à ces couleurs. Je répète: Nous n'avons pas accès au tiers des couleurs de la plupart des écrans modernes. Et ce ne sont pas n'importe quelles couleurs, mais les couleurs les plus vives que l'écran peut afficher. Nos sites Web sont délavés, car le matériel des écrans a évolué plus rapidement que les spécifications CSS et les implémentations de navigateurs.
Lea Verou
Commandes de formulaire adaptatives avec jeu de couleurs
De nombreux navigateurs proposent des commandes de thème sombre, actuellement Safari et Chromium, mais vous devez spécifier dans CSS ou HTML que votre conception les utilise.
L'exemple ci-dessus montre l'effet de la propriété dans le panneau "Styles" de DevTools. La démonstration utilise la balise HTML, qui est généralement un meilleur emplacement, à mon avis:
<meta name="color-scheme" content="dark light">
Pour en savoir plus, consultez cet articlecolor-scheme
de Thomas Steiner. Vous pouvez obtenir bien plus que des cases à cocher sombres.
CSS accent-color
Une activité récente a été menée autour de accent-color
dans les éléments de formulaire. Il s'agit d'un style CSS unique qui peut modifier la couleur de teinte utilisée dans l'élément de saisie du navigateur. Pour en savoir plus, consultez cette page sur GitHub. Je l'ai inclus dans mes styles pour ce composant. Comme les navigateurs le prennent en charge, mes cases à cocher seront plus adaptées au thème avec des touches de couleur rose et violet.
input[type="checkbox"] {
accent-color: var(--brand);
}
Créations color pop avec des dégradés fixes et un focus intérieur
Les couleurs ressortent le plus lorsqu'elles sont utilisées avec parcimonie. Pour y parvenir, j'aime utiliser des interactions d'UI colorées.
La vidéo ci-dessus présente de nombreuses couches de commentaires et d'interactions dans l'interface utilisateur, qui contribuent à donner de la personnalité à l'interaction en:
- Mettre en avant le contexte
- Fournir des commentaires sur l'UI sur le niveau de remplissage de la valeur dans la plage.
- Fournir des commentaires à l'UI indiquant qu'un champ accepte la saisie
Pour fournir des commentaires lorsqu'un élément est utilisé, le CSS utilise la pseudo-classe :focus-within
pour modifier l'apparence de divers éléments. Décomposons .fieldset-item
, c'est très intéressant:
.fieldset-item {
...
&:focus-within {
background: var(--surface2);
& svg {
fill: white;
}
& picture {
clip-path: circle(50%);
background: var(--brand-bg-gradient) fixed;
}
}
}
Lorsqu'un des enfants de cet élément a le focus:
- Une couleur de surface plus contrastée est attribuée à l'arrière-plan
.fieldset-item
. - Le
svg
imbriqué est rempli en blanc pour un contraste plus élevé. - Le
clip-path
<picture>
imbriqué se développe en cercle complet et l'arrière-plan est rempli du dégradé fixe lumineux.
Période personnalisée
Étant donné l'élément d'entrée HTML suivant, je vais vous montrer comment j'ai personnalisé son apparence:
<input type="range">
Cet élément se compose de trois parties que nous devons personnaliser:
Styles d'éléments de plage
input[type="range"] {
/* style setting variables */
--track-height: .5ex;
--track-fill: 0%;
--thumb-size: 3ex;
--thumb-offset: -1.25ex;
--thumb-highlight-size: 0px;
appearance: none; /* clear styles, make way for mine */
display: block;
inline-size: 100%; /* fill container */
margin: 1ex 0; /* ensure thumb isn't colliding with sibling content */
background: transparent; /* bg is in the track */
outline-offset: 5px; /* focus styles have space */
}
Les premières lignes de CSS sont les parties personnalisées des styles. J'espère que le libellé clair de ces lignes vous sera utile. Le reste des styles est principalement constitué de styles de réinitialisation, afin de fournir une base cohérente pour créer les parties délicates du composant.
Styles de piste
input[type="range"]::-webkit-slider-runnable-track {
appearance: none; /* clear styles, make way for mine */
block-size: var(--track-height);
border-radius: 5ex;
background:
/* hard stop gradient:
- half transparent (where colorful fill we be)
- half dark track fill
- 1st background image is on top
*/
linear-gradient(
to right,
transparent var(--track-fill),
var(--surface1) 0%
),
/* colorful fill effect, behind track surface fill */
var(--brand-bg-gradient) fixed;
}
L'astuce consiste à "révéler" la couleur de remplissage vive. Pour ce faire, utilisez le dégradé d'arrêt dur en haut. Le dégradé est transparent jusqu'au pourcentage de remplissage, puis utilise la couleur de la surface de la piste non remplie. Derrière cette surface non remplie se trouve une couleur pleine largeur, qui attend que la transparence la révèle.
Style de remplissage du tracé
Ma conception nécessite JavaScript pour conserver le style de remplissage. Il existe des stratégies CSS uniquement, mais elles exigent que l'élément de curseur ait la même hauteur que le canal, et je n'ai pas réussi à trouver une harmonie dans ces limites.
/* grab sliders on page */
const sliders = document.querySelectorAll('input[type="range"]')
/* take a slider element, return a percentage string for use in CSS */
const rangeToPercent = slider => {
const max = slider.getAttribute('max') || 10;
const percent = slider.value / max * 100;
return `${parseInt(percent)}%`;
};
/* on page load, set the fill amount */
sliders.forEach(slider => {
slider.style.setProperty('--track-fill', rangeToPercent(slider));
/* when a slider changes, update the fill prop */
slider.addEventListener('input', e => {
e.target.style.setProperty('--track-fill', rangeToPercent(e.target));
})
})
Je pense que cela constitue une belle amélioration visuelle. Le curseur fonctionne parfaitement sans JavaScript. La propriété --track-fill
n'est pas obligatoire. S'il n'est pas présent, il n'aura tout simplement pas de style de remplissage. Si JavaScript est disponible, renseignez la propriété personnalisée tout en observant les modifications apportées par l'utilisateur, en synchronisant la propriété personnalisée avec la valeur.
Voici un excellent post sur CSS-Tricks par Ana Tudor, qui présente une solution CSS uniquement pour le remplissage de la piste. J'ai également trouvé cet élément range
très inspirant.
Styles de curseurs
input[type="range"]::-webkit-slider-thumb {
appearance: none; /* clear styles, make way for mine */
cursor: ew-resize; /* cursor style to support drag direction */
border: 3px solid var(--surface3);
block-size: var(--thumb-size);
inline-size: var(--thumb-size);
margin-top: var(--thumb-offset);
border-radius: 50%;
background: var(--brand-bg-gradient) fixed;
}
La plupart de ces styles permettent de créer un cercle parfait.
Vous voyez à nouveau le dégradé d'arrière-plan fixe qui unifie les couleurs dynamiques des miniatures, des pistes et des éléments SVG associés.
J'ai séparé les styles pour l'interaction afin d'isoler la technique box-shadow
utilisée pour la surbrillance en survol:
@custom-media --motionOK (prefers-reduced-motion: no-preference);
::-webkit-slider-thumb {
…
/* shadow spread is initally 0 */
box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);
/* if motion is OK, transition the box-shadow change */
@media (--motionOK) {
& {
transition: box-shadow .1s ease;
}
}
/* on hover/active state of parent, increase size prop */
@nest input[type="range"]:is(:hover,:active) & {
--thumb-highlight-size: 10px;
}
}
L'objectif était de créer un élément visuel animé et facile à gérer pour les commentaires des utilisateurs. En utilisant une ombre portée, je peux éviter de déclencher la mise en page avec l'effet. Pour ce faire, je crée une ombre qui n'est pas floutée et qui correspond à la forme circulaire de l'élément de curseur. Je modifie ensuite la taille de l'écartement en cas de survol.
Si seulement l'effet de surbrillance était aussi simple sur les cases à cocher…
Sélecteurs multinavigateurs
J'ai constaté que j'avais besoin de ces sélecteurs -webkit-
et -moz-
pour assurer la cohérence entre les navigateurs:
input[type="range"] {
&::-webkit-slider-runnable-track {}
&::-moz-range-track {}
&::-webkit-slider-thumb {}
&::-moz-range-thumb {}
}
Case à cocher personnalisée
À partir de l'élément d'entrée HTML suivant, je vais vous montrer comment j'ai personnalisé son apparence:
<input type="checkbox">
Cet élément se compose de trois parties que nous devons personnaliser:
Élément "Case à cocher"
input[type="checkbox"] {
inline-size: var(--space-sm); /* increase width */
block-size: var(--space-sm); /* increase height */
outline-offset: 5px; /* focus style enhancement */
accent-color: var(--brand); /* tint the input */
position: relative; /* prepare for an absolute pseudo element */
transform-style: preserve-3d; /* create a 3d z-space stacking context */
margin: 0;
cursor: pointer;
}
Les styles transform-style
et position
préparent le pseudo-élément que nous présenterons plus tard pour styliser le repère. Sinon, il s'agit principalement de détails mineurs de style de ma part. J'aime que le curseur soit un pointeur, j'aime les décalages de contour, les cases à cocher par défaut sont trop petites, et si accent-color
est compatible, intégrez ces cases à cocher dans le jeu de couleurs de la marque.
Libellés des cases à cocher
Il est important de fournir des libellés pour les cases à cocher pour deux raisons. La première consiste à représenter l'utilisation de la valeur de la case à cocher pour répondre à la question "Activé ou désactivé pour quoi ?". Deuxièmement, pour l'expérience utilisateur, les utilisateurs Web se sont habitués à interagir avec les cases à cocher via les libellés associés.
<input type="checkbox" id="text-notifications" name="text-notifications" >
<label for="text-notifications"> <h3>Text Messages</h3> <small>Get notified about all text messages sent to your device</small> </label>
Dans votre étiquette, placez un attribut for
qui pointe vers une case à cocher par ID: <label for="text-notifications">
. Dans votre case à cocher, doublez le nom et l'ID pour vous assurer qu'il est détecté par différents outils et technologies, comme une souris ou un lecteur d'écran : <input type="checkbox" id="text-notifications" name="text-notifications">
.
:hover
, :active
et d'autres éléments sont inclus sans frais avec la connexion, ce qui augmente les possibilités d'interaction avec votre formulaire.
Mise en surbrillance de la case à cocher
Je souhaite que mes interfaces soient cohérentes. L'élément de curseur présente un joli surlignage de vignette que je voudrais utiliser avec la case à cocher. La miniature a pu utiliser box-shadow
et sa propriété spread
pour mettre à l'échelle une ombre. Toutefois, cet effet ne fonctionne pas ici, car nos cases à cocher sont, et doivent l'être, carrées.
J'ai pu obtenir le même effet visuel avec un pseudo-élément et une quantité regrettable de CSS délicat:
@custom-media --motionOK (prefers-reduced-motion: no-preference);
input[type="checkbox"]::before {
--thumb-scale: .01; /* initial scale of highlight */
--thumb-highlight-size: var(--space-xl);
content: "";
inline-size: var(--thumb-highlight-size);
block-size: var(--thumb-highlight-size);
clip-path: circle(50%); /* circle shape */
position: absolute; /* this is why position relative on parent */
top: 50%; /* pop and plop technique (https://web.dev/centering-in-css#5-pop-and-plop) */
left: 50%;
background: var(--thumb-highlight-color);
transform-origin: center center; /* goal is a centered scaling circle */
transform: /* order here matters!! */
translateX(-50%) /* counter balances left: 50% */
translateY(-50%) /* counter balances top: 50% */
translateZ(-1px) /* PUTS IT BEHIND THE CHECKBOX */
scale(var(--thumb-scale)) /* value we toggle for animation */
;
will-change: transform;
@media (--motionOK) { /* transition only if motion is OK */
& {
transition: transform .2s ease;
}
}
}
/* on hover, set scale custom property to "in" state */
input[type="checkbox"]:hover::before {
--thumb-scale: 1;
}
La création d'un pseudo-élément de cercle est une tâche simple, mais le placer derrière l'élément auquel il est associé a été plus difficile. Voici ce que cela donnait avant et après avoir corrigé le problème:
Il s'agit d'une micro-interaction, mais il est important pour moi de maintenir la cohérence visuelle. La technique de mise à l'échelle de l'animation est la même que celle que nous utilisons ailleurs. Nous définissons une propriété personnalisée sur une nouvelle valeur et laissons le CSS effectuer la transition en fonction des préférences de mouvement. La fonctionnalité clé est translateZ(-1px)
. L'élément parent a créé un espace 3D et cet élément enfant pseudo-élément s'y est appuyé en se plaçant légèrement en arrière dans l'espace Z.
Accessibilité
La vidéo YouTube offre une excellente démonstration des interactions de la souris, du clavier et du lecteur d'écran pour ce composant de paramètres. Je vais vous donner quelques détails.
Choix d'éléments HTML
<form>
<header>
<fieldset>
<picture>
<label>
<input>
Chacun d'eux contient des conseils sur l'outil de navigation de l'utilisateur. Certains éléments fournissent des indices d'interaction, d'autres connectent l'interactivité et d'autres aident à façonner l'arborescence d'accessibilité dans laquelle un lecteur d'écran navigue.
Attributs HTML
Nous pouvons masquer les éléments qui ne sont pas nécessaires aux lecteurs d'écran, dans ce cas l'icône à côté du curseur:
<picture aria-hidden="true">
La vidéo ci-dessus montre le flux du lecteur d'écran sur Mac OS. Notez que le focus de saisie passe directement d'un curseur à l'autre. En effet, nous avons masqué l'icône qui aurait pu être un arrêt sur le chemin vers le curseur suivant. Sans cet attribut, l'utilisateur doit s'arrêter, écouter et passer l'image qu'il ne peut pas voir.
Le SVG est un ensemble de calculs. Ajoutons un élément <title>
pour un titre libre au survol de la souris et un commentaire lisible par l'humain sur ce que les calculs créent:
<svg viewBox="0 0 24 24">
<title>A note icon</title>
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>
En dehors de cela, nous avons utilisé suffisamment de code HTML clairement marqué pour que le formulaire soit testé de manière optimale avec la souris, le clavier, les manettes de jeu vidéo et les lecteurs d'écran.
JavaScript
J'ai déjà expliqué comment la couleur de remplissage du canal était gérée à partir de JavaScript. Voyons maintenant le code JavaScript associé à <form>
:
const form = document.querySelector('form');
form.addEventListener('input', event => {
const formData = Object.fromEntries(new FormData(form));
console.table(formData);
})
Chaque fois que l'utilisateur interagit avec le formulaire et le modifie, la console le consigne en tant qu'objet dans un tableau pour faciliter son examen avant de l'envoyer à un serveur.
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 une démo, tweetez-moi des liens et je les ajouterai à la section Remix de la communauté ci-dessous.