Présentation des principes de base de la création d'un composant de paramètres composé de curseurs et de cases à cocher.
Dans cet article, je souhaite partager notre réflexion sur la création d'un composant de paramètres pour le Web qui soit responsif, compatible avec plusieurs entrées d'appareil et compatible avec plusieurs navigateurs. Essayez la démonstration.
Si vous préférez la vidéo, ou si vous souhaitez avoir un aperçu de ce que nous développons actuellement, voici un bref tutoriel sur YouTube:
Présentation
J'ai décomposé les aspects de ce composant dans les sections suivantes:
- Mises en page
- Couleur
- Saisie de plage personnalisée
- Saisie d'une case à cocher personnalisée
- Considérations sur l'accessibilité
- JavaScript
Mises en page
Il s'agit de la première démonstration du défi de l'interface graphique à toutes les grilles CSS ! Voici chaque grille mise en évidence avec les Outils pour les développeurs Chrome:
Juste pour l'écart
La mise en page la plus courante:
foo {
display: grid;
gap: var(--something);
}
J'appelle cette mise en page « juste pour l'écart » car elle n'utilise que la grille pour ajouter des écarts entre les blocs.
Cinq mises en page utilisent cette stratégie. Les voici toutes:
L'élément fieldset
, qui contient chaque groupe d'entrées (.fieldset-item
), utilise gap: 1px
pour créer les bordures fines entre les éléments. Pas de problème de frontière !
.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); } } }
Enveloppement naturel de la grille
La mise en page la plus complexe a fini par être la mise en page macro, le système de mise en page logique entre <main>
et <form>
.
Centrer le contenu encapsulant
Les zones Flexbox et la grille offrent toutes deux des fonctionnalités pour align-items
ou align-content
. Lorsque vous traitez des éléments encapsulants, les alignements de mise en page content
répartissent 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 raccourci d'alignement place-content: center
afin que les enfants soient centrés verticalement et horizontalement dans les mises en page à une et deux colonnes.
Regardez dans la vidéo ci-dessus comment le "contenu" reste centré, même si l'encapsulation a eu lieu.
Répéter l'ajustement automatique minmax
<form>
utilise une mise en page en 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) et column-gap
(--space-xxl) pour appliquer cette touche personnalisée à la mise en page responsive. Lorsque les colonnes s'empilent, nous voulons un
écart important, mais pas aussi grand que sur un grand écran.
La propriété grid-template-columns
utilise trois fonctions CSS: repeat()
, minmax()
et min()
. Una Kravets publie un excellent article de blog sur la mise en page à ce sujet, en l'appelant RAM.
Il y a 3 ajouts spéciaux dans notre mise en page, si vous la comparez à 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 de l'alignement flex-start
consiste à supprimer l'effet d'étirement par défaut. Ainsi, les enfants de cette mise en page n'ont pas besoin d'avoir des hauteurs égales, mais peuvent avoir des hauteurs naturelles et intrinsèques. La vidéo YouTube présente brièvement cet ajout d'alignement.
Vous devriez découvrir max-width: 89vw
dans ce post.
Je vais vous montrer la mise en page avec et sans le style appliqué:
Qu'est-ce qui change ? Lorsque max-width
est spécifié, il fournit un contexte, un dimensionnement explicite ou un dimensionnement défini à l'algorithme de mise en page auto-fit
afin de savoir combien de répétitions il peut tenir dans l'espace. Bien qu'il semble évident que l'espace est "pleine largeur", conformément aux spécifications de la grille CSS, une taille précise ou une taille maximale doit être fournie. J'ai indiqué une taille maximale.
Alors, pourquoi utiliser 89vw
? Parce que "ça a fonctionné" pour ma mise en page.
Comme d'autres utilisateurs de Chrome, nous étudions pourquoi une valeur plus raisonnable, telle que 100vw
, ne suffit pas, et s'il s'agit en fait d'un bug.
Espacement
L'harmonie de cette mise en page provient en grande partie d'une palette limitée d'espaces, 7 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;
}
Ces flux sont parfaitement utilisables avec la grille, CSS @nest et la syntaxe de niveau 5 de @media. Voici un exemple de l'ensemble complet de styles de mise en page <main>
.
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 présentant du contenu centré et légèrement remplie par défaut (comme sur mobile) Toutefois, à mesure que l'espace disponible pour la fenêtre d'affichage augmente, celui-ci s'éparpille en augmentant la marge intérieure. Le CSS 2021 a l'air plutôt bien !
Vous vous souvenez de la mise en page précédente, « juste pour le manque » ? 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
Une utilisation contrôlée de la couleur a permis à cette conception de se démarquer comme expressive, mais minimale. Je fais comme ceci:
: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 chiffres au lieu de noms tels que surface-dark
et surface-darker
, car dans une requête média, je les inverse, et le clair et le sombre n'auront pas de sens.
Je les retourne dans une requête média de préférence, comme ceci:
: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 vue d'ensemble et de la stratégie avant de nous plonger dans les détails de la syntaxe des couleurs. Mais, comme je suis un peu en avance, permettez-moi de revenir en arrière.
LCH?
Sans aller plus loin dans la théorie des couleurs, la LCH est une syntaxe à vocation humaine qui s'adresse à la façon dont nous percevons les couleurs, et non à la façon dont nous mesurons les couleurs à l'aide de calculs mathématiques (comme 255). Cela lui donne un avantage distinct, car les humains peuvent l'écrire plus facilement et les autres humains sont en phase avec ces ajustements.
Pour aujourd'hui, dans cette démonstration, concentrons-nous sur la syntaxe et les valeurs que je retourne afin de rendre l'affichage clair et sombre. 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)
se traduit par une luminosité 10%
, 0 chroma et 0 teinte, soit un gris très foncé incolore. Ensuite, dans la requête média pour le mode clair, la luminosité est basculée sur 90%
avec --surface1: lch(90 0 0);
. Et c'est l'essentiel de la stratégie. Commencez par changer simplement la luminosité entre les deux thèmes, en conservant les rapports de contraste requis par la conception ou ce qui peut maintenir l'accessibilité.
L'avantage avec lch()
ici est que la légèreté est orientée vers l'humain. La modification de la %
est agréable, car elle sera différente de manière perçue et cohérente avec cette %
. hsl()
, par exemple, n'est pas aussi fiable.
Vous trouverez plus d'informations sur les espaces de couleur et sur lch()
si cela vous intéresse. Ça arrive !
Le CSS ne peut pas du tout accéder à ces couleurs. Laissez-moi répéter: Nous n'avons pas accès à un tiers des couleurs de la plupart des écrans modernes. Il ne s'agit pas seulement de n'importe quelle couleur, mais des couleurs les plus vives possibles à l'écran. Nos sites Web sont délavés, car le matériel de surveillance a évolué plus rapidement que les spécifications CSS et les implémentations de navigateur.
Lea Verou
Commandes de formulaire adaptatives avec un jeu de couleurs
De nombreux navigateurs proposent des commandes du thème sombre (actuellement Safari et Chromium), mais vous devez spécifier en CSS ou en HTML que votre conception les utilise.
L'exemple ci-dessus illustre l'effet de la propriété dans le panneau "Styles" des outils de développement. La démonstration utilise la balise HTML, qui, à mon avis, est généralement plus adaptée:
<meta name="color-scheme" content="dark light">
Pour en savoir plus, consultez cet article color-scheme
de Thomas Steiner. Il y a beaucoup plus à gagner
que des cases à cocher sombres !
CSS accent-color
Une activité récente s'est produite concernant accent-color
sur les éléments de formulaire. Il s'agit d'un style CSS unique capable de modifier la couleur de la teinte utilisée dans l'élément d'entrée du navigateur. Pour en savoir plus, cliquez ici sur GitHub. Je l'ai inclus dans mes
styles pour ce composant. Comme les navigateurs le prennent en charge, mes cases à cocher
s'afficheront sur le thème des couleurs rose et violette.
input[type="checkbox"] {
accent-color: var(--brand);
}
Color pop avec dégradés fixes et focus à l'intérieur
Les couleurs ressortent mieux lorsqu'elles sont utilisées avec parcimonie. Pour ce faire, j'aime utiliser des interactions colorées avec l'interface utilisateur.
La vidéo ci-dessus comporte de nombreuses couches de commentaires et d'interactions avec l'interface utilisateur, qui contribuent à donner de la personnalité à l'interaction en:
- Mise en avant du contexte
- Fournir un retour d'interface utilisateur sur le niveau d'occupation de la valeur dans la plage.
- Envoi de commentaires à l'interface utilisateur pour indiquer qu'un champ accepte une entrée
Pour fournir des commentaires lorsqu'un élément interagit avec un élément, CSS utilise la pseudo-classe :focus-within
afin de modifier l'apparence des différents éléments. Décomposons le .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;
}
}
}
Lorsque l'un des enfants de cet élément est au centre de l'attention:
- L'arrière-plan
.fieldset-item
se voit attribuer une couleur de surface plus contrastée. - L'élément
svg
imbriqué est blanc pour offrir un contraste plus élevé. - L'élément
<picture>
clip-path
imbriqué se développe pour former un cercle complet, et l'arrière-plan est rempli du dégradé fixe lumineux.
Période personnalisée
Je vais vous montrer comment j'ai personnalisé l'apparence de l'élément d'entrée HTML suivant:
<input type="range">
Cet élément comporte trois parties:
Styles d'élément 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 du code CSS sont les parties personnalisées des styles, et j'espère que des libellés clairs vous seront utiles. Les autres styles sont principalement des styles réinitialisés, afin de fournir une base cohérente pour la création des 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 à cela est de « révéler » la couleur de remplissage éclatante. Pour ce faire, appliquez le dégradé d'arrêt brutal en haut. Le dégradé est transparent jusqu'au pourcentage de remplissage. Ensuite, la couleur de la surface de suivi non remplie est utilisée. Derrière cette surface non remplie se trouve une couleur pleine largeur, attendant que la transparence la révèle.
Suivre le style de remplissage
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 "pouce" ait la même hauteur que le titre, et je n'ai pas trouvé d'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 c'est une belle amélioration visuelle. Le curseur fonctionne très bien sans JavaScript. La propriété --track-fill
n'est pas nécessaire, et il n'aura tout simplement pas de style de remplissage s'il n'est pas présent. 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 d'Ana Tudor qui illustre une solution CSS uniquement pour le remplissage des pistes. J'ai aussi trouvé cet élément range
très inspirant.
Styles de pouce
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 servent à créer un joli cercle.
Là encore, vous voyez le dégradé d'arrière-plan fixe qui unifie les couleurs dynamiques des pouces, des pistes et des éléments SVG associés.
J'ai séparé les styles de l'interaction afin d'isoler la technique box-shadow
utilisée pour la mise en surbrillance du 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 mettre en évidence des éléments visuels animés et faciles à gérer pour recueillir les commentaires des utilisateurs. En utilisant une ombre de case, je peux éviter de déclencher une mise en page avec cet 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 "pouce". Ensuite, je modifie la taille de l'étalonnage et la fais passer au survol.
Si seulement l'effet de mise en surbrillance était si facile avec les cases à cocher...
Sélecteurs pour plusieurs navigateurs
J'ai constaté que j'avais besoin des sélecteurs -webkit-
et -moz-
suivants 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
Je vais vous montrer comment j'ai personnalisé l'apparence de l'élément d'entrée HTML suivant:
<input type="checkbox">
Cet élément comporte trois parties:
Élément de 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 ultérieurement pour styliser la mise en surbrillance. Autrement dit, ce sont surtout des trucs
de style peu tranchés de ma part. J'aime que le curseur soit un pointeur, 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.
Étiquettes de cases à cocher
Il est important de fournir des libellés pour les cases à cocher pour deux raisons. La première consiste à représenter à quoi sert la valeur de la case à cocher, afin de répondre "activé ou désactivé pour quoi ?" Deuxièmement, pour l'expérience utilisateur, les internautes 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, insérez un attribut for
qui pointe vers une case à cocher par ID: <label for="text-notifications">
. Sur votre case à cocher, doublez le nom et l'ID pour vous assurer qu'il peut être trouvé avec 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 sont proposés sans frais avec la connexion, ce qui vous permet d'interagir davantage avec votre formulaire.
Mise en surbrillance d'une case à cocher
Je veux que mes interfaces restent cohérentes, et l'élément Slider présente une belle mise en surbrillance de la miniature que j'aimerais utiliser avec la case à cocher. La vignette pouvait utiliser box-shadow
, et sa propriété spread
permet de mettre à l'échelle une ombre à la hausse ou à la baisse. Cependant, cet effet ne fonctionne pas ici, car nos cases à cocher sont et devraient être carrées.
J'ai pu obtenir le même effet visuel avec un pseudo-élément et une quantité de CSS difficile:
@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 un travail simple, mais le placer derrière l'élément auquel il est rattaché était plus difficile. Voici avant et après l'avoir corrigé:
C'est certainement une micro-interaction, mais il est important pour moi de garder la cohérence visuelle. La technique de mise à l'échelle de l'animation est la même que celle que nous avons utilisée 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é principale est translateZ(-1px)
. Le parent a créé un espace 3D, et ce pseudo-élément enfant l'a appuyé dessus en se replaçant légèrement dans l'espace z.
Accessibilité
La vidéo YouTube illustre parfaitement les interactions avec la souris, le clavier et le lecteur d'écran pour ce composant de paramétrage. Je vais souligner certains des détails ici.
Choix des éléments HTML
<form>
<header>
<fieldset>
<picture>
<label>
<input>
Chacun de ces éléments contient des conseils et des astuces pour l'outil de navigation de l'utilisateur. Certains éléments fournissent des conseils d'interaction, d'autres l'interactivité de connexion et d'autres aident à façonner l'arborescence d'accessibilité dans laquelle un lecteur d'écran parcourt.
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 illustre le flux de lecteur d'écran sur Mac OS. Notez que la mise au point des entrées se déplace directement d'un curseur à l'autre. C'est parce que nous avons masqué l'icône qui a peut-être été un arrêt sur le chemin du curseur suivant. Sans cet attribut, l'utilisateur devrait s'arrêter, écouter et passer au-delà de l'image qu'il ne pourrait peut-être pas voir.
Le SVG est un tas de calculs. Ajoutons un élément <title>
pour un titre de survol sans frais et un commentaire lisible par l'humain sur ce que la formule mathématique crée:
<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 identifié pour que le formulaire soit parfaitement testé avec la souris, le clavier, les manettes de jeux vidéo et les lecteurs d'écran.
JavaScript
J'ai déjà vu comment la couleur de remplissage du titre était gérée à partir de JavaScript. Examinons maintenant le 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 l'enregistre en tant qu'objet dans une table afin de faciliter son examen avant de l'envoyer à un serveur.
Conclusion
Maintenant que tu sais comment j'ai fait, comment tu en ferais ?! Cela donne une architecture de composants amusante ! Qui créera la 1re version avec des emplacements dans son framework préféré ? 🙂
Diversissons nos approches et apprenons toutes les façons de créer sur le Web. Créez une démonstration, cliquez sur les liens Envoyez-moi un tweet et je l'ajouterai à la section Remix de la communauté ci-dessous.