Una panoramica di base sulla creazione di un componente delle impostazioni di cursori e caselle di controllo.
In questo post voglio condividere alcune idee sulla creazione di un componente Impostazioni per il web che sia responsive, supporti più input di dispositivi e funzioni su più browser. Prova la demo.
Se preferisci i video o vuoi un'anteprima dell'interfaccia utente/dell'esperienza utente di ciò che stiamo creando, ecco un walkthrough più breve su YouTube:
Panoramica
Ho suddiviso gli aspetti di questo componente nelle seguenti sezioni:
- Layout
- Colore
- Inserimento intervallo personalizzato
- Input di caselle di controllo personalizzate
- Considerazioni sull'accessibilità
- JavaScript
Layout
Questa è la prima demo di GUI Challenge essere all CSS Grid. Ecco ogni griglia evidenziata con Chrome DevTools per la griglia:
Solo per lacune
Il layout più comune:
foo {
display: grid;
gap: var(--something);
}
Chiedo a questo layout "solo per spaziatura" perché utilizza la griglia solo per aggiungere spazi tra i blocchi.
Cinque layout utilizzano questa strategia. Eccoli tutti:
L'elemento fieldset
, che contiene ogni gruppo di input (.fieldset-item
), utilizza gap: 1px
per creare i bordi sottili tra gli elementi. Nessuna soluzione complicata per i confini.
.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); } } }
Aggiunta di righe aggiuntive alla griglia naturale
Il layout più complesso è risultato essere il layout macro, il sistema di layout logico tra <main>
e <form>
.
Centrare i contenuti del wrapping
Sia Flexbox che la griglia consentono di align-items
o
align-content
e, quando si ha a che fare con elementi di a capo, gli allineamenti del layout
content
distribuiranno lo spazio tra gli elementi secondari come gruppo.
main {
display: grid;
gap: var(--space-xl);
place-content: center;
}
L'elemento principale utilizza l'abbreviazione dell'allineamento di place-content: center
in modo che gli elementi secondari siano centrati verticalmente e orizzontalmente nei layout a una e due colonne.
Guarda il video qui sopra per scoprire come i "contenuti" rimangono al centro, anche se si verifica il wrapping.
Ripeti adattamento automatico minmax
<form>
utilizza un layout della griglia adattivo per ogni sezione.
Questo layout passa da una a due colonne in base allo spazio disponibile.
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;
}
Questa griglia ha un valore diverso per row-gap
(--space-xl) rispetto a column-gap
(--space-xxl) per apportare un tocco personalizzato al layout responsive. Quando le colonne sono impilate, vogliamo un intervallo ampio, ma non così ampio come se fossimo su uno schermo ampio.
La proprietà grid-template-columns
utilizza tre funzioni CSS: repeat()
, minmax()
e
min()
. Una Kravets ha scritto un ottimo post del blog sul layout in merito, chiamandolo
RAM.
Se lo confronti con quello di Una, il nostro layout presenta tre aggiunte speciali:
- Passiamo una funzione
min()
aggiuntiva. - Specifichiamo
align-items: flex-start
. - Esiste uno stile
max-width: 89vw
.
La funzione min()
aggiuntiva è descritta bene da Evan Minto nel suo blog nel post Intrinsically Responsive CSS Grid with minmax() and
min().
Ti consiglio di leggerlo. La correzione dell'allineamento flex-start
consente di rimuovere l'effetto di allungamento predefinito, in modo che gli elementi secondari di questo layout non debbano avere altezze uguali, poiché possono avere altezze naturali e intrinseche. Il
video di YouTube offre una rapida suddivisione di questo allineamento aggiunto.
Vale la pena analizzare un po' di più in questo post per max-width: 89vw
.
Vediamo il layout con e senza lo stile applicato:
Che cosa succede? Quando viene specificato, max-width
fornisce il contesto,
il dimensionamento esplicito o il definizione
delle dimensioni per l'algoritmo
di layout auto-fit
per sapere
quante ripetizioni può rientrare nello spazio. Sebbene sembri ovvio che lo spazio sia a "larghezza massima", secondo la specifica della griglia CSS, è necessario fornire una dimensione definita o una dimensione massima. Ho fornito una dimensione massima.
Allora, perché 89vw
? Perché "funziona" per il mio layout.
Io e un paio di altri membri del team di Chrome stiamo cercando di capire perché un valore più ragionevole, come 100vw
, non sia sufficiente e se si tratta di un bug.
Spaziatura
Gran parte dell'armonia di questo layout deriva da una tavolozza limitata di spazi, 7 per l'esattezza.
:root {
--space-xxs: .25rem;
--space-xs: .5rem;
--space-sm: 1rem;
--space-md: 1.5rem;
--space-lg: 2rem;
--space-xl: 3rem;
--space-xxl: 6rem;
}
L'utilizzo di questi flussi è molto piacevole con la griglia, CSS @nest e la sintassi di livello 5
di @media. Ecco un esempio, l'insieme di stili di layout completamente <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);
}
}
}
Una griglia con contenuti centrati e leggermente riempiti per impostazione predefinita (come sui dispositivi mobili). Tuttavia, man mano che lo spazio del viewport diventa disponibile, si espande aumentando il padding. Il CSS 2021 sembra molto buono.
Ricordi il layout precedente, "solo per il vuoto"? Ecco una versione più completa di come appaiono in questo componente:
header {
display: grid;
gap: var(--space-xxs);
}
section {
display: grid;
gap: var(--space-md);
}
Colore
L'uso controllato del colore ha contribuito a rendere questo design espressivo e allo stesso tempo minimale. Io lo faccio così:
: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);
}
Assegno i nomi ai colori di sfondo e del testo con numeri anziché con nomi come
surface-dark
e surface-darker
perché in una query sui contenuti multimediali li capovolgerò
e chiaro e scuro non avranno significato.
Li inverto in una query sui contenuti multimediali delle preferenze come questa:
: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);
}
}
}
È importante dare un'occhiata veloce al quadro generale e alla strategia prima di approfondire i dettagli della sintassi dei colori. Dato che ho anticipato un po', ricominciamo da capo.
LCH?
Il LCH è una sintassi orientata all'uomo che, senza addentrarci troppo nella teoria dei colori, si adatta a come percepiamo il colore, non a come misuriamo il colore con la matematica (come 255). Questo offre un vantaggio distinto in quanto gli esseri umani possono scriverlo più facilmente e altri esseri umani saranno in sintonia con questi aggiustamenti.
Per oggi, in questa demo, concentriamoci sulla sintassi e sui valori che sto modificando per cambiare il colore in chiaro e scuro. Diamo un'occhiata a una superficie e a un colore di testo:
: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)
si traduce in 10%
leggerezza, crominanza 0 e tonalità 0: un
grigio incolore molto scuro. Poi, nella query sui media per la modalità Luce, la luminosità viene impostata su 90%
con --surface1: lch(90 0 0);
. Questo è l'essenziale della strategia. Inizia cambiando solo la luminosità tra i due temi, mantenendo i rapporti di contrasto richiesti dal design o quelli che possono mantenere l'accessibilità.
Il vantaggio di lch()
è che la leggerezza è orientata alle persone e possiamo sentirci bene per un cambiamento di lch()
, che sarà percepito in modo coerente e costante come lch()
diverso.%
%
Ad esempio, hsl()
non è così affidabile.
Se ti interessa, scopri di più sugli spazi di colore e su lch()
. È in arrivo.
Al momento CSS non può accedere a questi colori. Lascia che ripeta: non abbiamo accesso a un terzo dei colori della maggior parte dei monitor moderni. Non si tratta di colori qualsiasi, ma dei colori più vivaci che lo schermo può mostrare. I nostri siti web sono sbiaditi perché l'hardware dei monitor si è evoluto più rapidamente delle specifiche CSS e delle implementazioni dei browser.
Lea Verou
Controlli adattivi dei moduli con schema di colori
Molti browser includono controlli per il tema scuro, attualmente Safari e Chromium, ma devi specificare in CSS o HTML che il tuo design li utilizza.
L'esempio riportato sopra mostra l'effetto della proprietà dal riquadro Stili di DevTools. La demo utilizza il tag HTML, che a mio avviso è in genere una posizione migliore:
<meta name="color-scheme" content="dark light">
Scopri di più in questo color-scheme
articolo di Thomas
Steiner. Puoi ottenere molto di più
degli input di casella di controllo scuri.
CSS accent-color
Ci sono state attività recenti su accent-color
sugli elementi del modulo, essendo un singolo stile CSS in grado di modificare il colore della tinta utilizzato nell'elemento di input del browser. Scopri di più qui su GitHub. L'ho incluso negli stili
di questo componente. Poiché i browser lo supportano, le mie caselle di controllo saranno più in tema con i colori rosa e viola.
input[type="checkbox"] {
accent-color: var(--brand);
}
Colori brillanti con gradienti fissi e messa a fuoco interna
I colori risaltano maggiormente se usati con parsimonia e uno dei modi che mi piace usare per ottenere questo risultato è tramite interazioni con l'interfaccia utente colorate.
Nel video qui sopra sono presenti molti livelli di feedback e interazione con l'interfaccia utente, che contribuiscono a dare personalità all'interazione:
- Mettere in evidenza il contesto.
- Fornire un feedback della UI relativo al "grado di completezza" del valore nell'intervallo.
- Fornire feedback sull'interfaccia utente che indica che un campo accetta input.
Per fornire un feedback quando si interagisce con un elemento, il CSS utilizza la pseudo classe
:focus-within
per modificare l'aspetto di vari elementi. Analizziamo la
.fieldset-item
, è molto interessante:
.fieldset-item {
...
&:focus-within {
background: var(--surface2);
& svg {
fill: white;
}
& picture {
clip-path: circle(50%);
background: var(--brand-bg-gradient) fixed;
}
}
}
Quando uno degli elementi secondari di questo elemento ha lo stato attivo:
- Allo sfondo
.fieldset-item
viene assegnato un colore di superficie con un contrasto più elevato. - L'elemento
svg
nidificato è bianco per un maggiore contrasto. - Il
<picture>
clip-path
nidificato si espande fino a formare un cerchio completo e lo sfondo viene riempito con il gradiente fisso luminoso.
Intervallo personalizzato
Dato il seguente elemento input HTML, ti mostrerò come ho personalizzato il suo aspetto:
<input type="range">
Questo elemento è composto da tre parti che dobbiamo personalizzare:
Stili degli elementi dell'intervallo
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 */
}
Le prime righe di CSS sono le parti personalizzate degli stili e spero che la loro etichettatura chiara ti sia utile. Gli altri stili vengono per lo più reimpostati, per fornire una base coerente per la creazione delle parti complesse del componente.
Stili di traccia
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;
}
Il trucco è "rivelare" il vivace colore di riempimento. Questo viene fatto con il gradiente di interruzione netta in alto. La sfumatura è trasparente fino alla percentuale di riempimento, dopodiché viene utilizzato il colore della superficie della traccia non riempita. Dietro questa superficie non compilata, c'è un colore di larghezza intera, in attesa che la trasparenza lo riveli.
Stile di riempimento del percorso
Il mio design richiede JavaScript per mantenere lo stile di riempimento. Esistono strategie solo per CSS, ma richiedono che l'elemento pollice abbia la stessa altezza della traccia e non sono riuscito a trovare un'armonia entro questi limiti.
/* 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));
})
})
Penso che sia un buon upgrade visivo. Il cursore funziona perfettamente senza JavaScript, la proprietà --track-fill
non è obbligatoria, semplicemente non avrà uno stile di riempimento se non è presente. Se JavaScript è disponibile, compila la proprietà personalizzata osservando anche le eventuali modifiche apportate dall'utente, sincronizzando la proprietà personalizzata con il valore.
Ecco un ottimo
post su
CSS-Tricks di Ana
Tudor, che mostra una soluzione solo CSS per preenchimento
della traccia. Anche questo elemento range
è stato molto stimolante.
Stili di miniatura
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 maggior parte di questi stili è pensata per creare un bel cerchio.
Anche in questo caso vediamo il gradiente di sfondo fisso che unifica i colori dinamici dei pollici, delle tracce e degli elementi SVG associati.
Ho separato gli stili dell'interazione per isolare la tecnica box-shadow
utilizzata per l'evidenziazione del passaggio del mouse:
@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'obiettivo era creare un'evidenziazione visiva animata e facile da gestire per i feedback degli utenti. Utilizzando un'ombra esterna, posso evitare di attivare il layout con l'effetto. Per farlo, creo un'ombra non sfocata che corrisponde alla forma circolare dell'elemento miniatura. Poi modifico e faccio la transizione delle dimensioni dell'espansione al passaggio del mouse.
Se solo l'effetto di evidenziazione fosse così facile sulle caselle di controllo…
Selettori cross-browser
Ho scoperto di aver bisogno di questi selettori -webkit-
e -moz-
per ottenere una coerenza tra browser:
input[type="range"] {
&::-webkit-slider-runnable-track {}
&::-moz-range-track {}
&::-webkit-slider-thumb {}
&::-moz-range-thumb {}
}
Casella di controllo personalizzata
Dato il seguente elemento input HTML, ti mostrerò come ho personalizzato il suo aspetto:
<input type="checkbox">
L'elemento deve essere personalizzato in tre parti:
Elemento casella di controllo
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;
}
Gli stili transform-style
e position
preparano lo pseudo-elemento che introdurremo più avanti per applicare uno stile al Video Ricordo. Per il resto, si tratta principalmente di piccole opinioni personali sul mio stile. Mi piace che il cursore sia un puntatore, mi piacciono gli offset dell'outline, le caselle di controllo predefinite sono troppo piccole e, se accent-color
è supportato, inserisci queste caselle di controllo nella combinazione di colori del brand.
Etichette casella di controllo
È importante fornire le etichette per le caselle di controllo per due motivi. La prima è rappresentare la finalità del valore della casella di controllo, per rispondere alla domanda "On o off per cosa?". In secondo luogo, per l'esperienza utente, gli utenti web si sono abituati a interagire con le caselle di controllo tramite le etichette associate.
<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>
Nell'etichetta, inserisci un attributo for
che rimandi a una casella di controllo in base all'ID: <label for="text-notifications">
. Nella casella di controllo, inserisci due volte il nome e l'ID per assicurarti che venga trovata con vari strumenti e tecnologie, come un mouse o uno screen reader:
<input type="checkbox" id="text-notifications" name="text-notifications">
.
:hover
, :active
e altri servizi sono senza costi con il collegamento, aumentando le modalità di interazione con il modulo.
Evidenziazione casella di controllo
Voglio che le interfacce siano coerenti e che l'elemento di scorrimento abbia una bella miniatura
in evidenza che vorrei usare con la casella di controllo. La miniatura è stata in grado di utilizzare box-shadow
e la sua proprietà spread
per aumentare e diminuire l'ombreggiatura. Tuttavia, questo effetto non funziona qui perché le nostre caselle di controllo sono e devono essere quadrate.
Ho potuto ottenere lo stesso effetto visivo con uno pseudo elemento e un quantità sfortunata di CSS complicato:
@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;
}
Creare uno pseudo-elemento circolare è semplice, ma posizionarlo dietro l'elemento a cui è associato è stato più difficile. Ecco prima e dopo che ho risolto il problema:
È sicuramente una micro interazione, ma per me è importante mantenere
la coerenza visiva. La tecnica di ridimensionamento delle animazioni è la stessa che utilizziamo in altri luoghi. Impostiamo una proprietà personalizzata su un nuovo valore e lasciamo che sia il CSS a eseguirne la transizione
in base alle preferenze di movimento. La funzionalità principale in questo caso è translateZ(-1px)
. L'elemento
padre ha creato uno spazio 3D e questo pseudo-elemento secondario vi ha avuto accesso collocandosi leggermente più indietro nello spazio Z.
Accessibilità
Il video di YouTube offre un'ottima dimostrazione delle interazioni con mouse, tastiera e screen reader per questo componente di impostazioni. Ti fornirò alcuni dettagli.
Scelte di elementi HTML
<form>
<header>
<fieldset>
<picture>
<label>
<input>
Ognuno di questi contiene suggerimenti per lo strumento di navigazione dell'utente. Alcuni elementi forniscono suggerimenti di interazione, altri collegano l'interattività e altri aiutano a modellare l'albero dell'accessibilità consultato da uno screen reader.
Attributi HTML
Possiamo nascondere gli elementi non necessari per gli screen reader, in questo caso l'icona accanto al cursore:
<picture aria-hidden="true">
Il video riportato sopra mostra il flusso dello screen reader su Mac OS. Nota come lo stato attivo si sposta direttamente da un cursore all'altro. Questo perché abbiamo nascosto l'icona che potrebbe essere stata una fermata sulla strada per il cursore successivo. Senza questo attributo, l'utente deve fermarsi, ascoltare e andare oltre l'immagine che potrebbe non essere in grado di vedere.
L'SVG è un insieme di operazioni matematiche, aggiungiamo un elemento <title>
per un titolo libero del passaggio del mouse e un commento leggibile da una persona su ciò che viene creato dalle operazioni matematiche:
<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>
A parte questo, abbiamo utilizzato HTML sufficientemente contrassegnato in modo che il modulo venga testato molto bene su mouse, tastiera, controller per videogiochi e screen reader.
JavaScript
Abbiamo già spiegato come veniva gestito il colore di riempimento della traccia da JavaScript, quindi ora diamo un'occhiata al codice JavaScript correlato <form>
:
const form = document.querySelector('form');
form.addEventListener('input', event => {
const formData = Object.fromEntries(new FormData(form));
console.table(formData);
})
Ogni volta che il modulo viene interagito e modificato, la console lo registra come oggetto in una tabella per facilitarne la revisione prima di inviarlo a un server.
Conclusione
Ora che sai come ho fatto, come faresti? Si tratta di un'architettura di componenti molto interessante. Chi creerà la prima versione con gli slot nel suo framework preferito? 🙂
Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, twittami i link e io la aggiungerò alla sezione Remix della community di seguito.