Una panoramica di base su come creare un componente di switch reattivo e accessibile.
In questo post voglio condividere le idee su un modo per creare componenti di switch. Prova la demo.
Se preferisci i video, ecco una versione di questo post su YouTube:
Panoramica
Un interruttore funziona in modo simile a una casella di controllo ma rappresenta esplicitamente gli stati di attivazione e disattivazione booleani.
Questa demo utilizza <input type="checkbox" role="switch">
per la maggior parte dei
di Google Cloud, che ha il vantaggio di non dover utilizzare CSS o JavaScript
completamente funzionali e accessibili. Il caricamento di CSS introduce il supporto della scrittura da destra a sinistra
lingue, verticalità, animazione e altro. Il caricamento di JavaScript esegue il passaggio
trascinabili e tangibili.
Proprietà personalizzate
Le seguenti variabili rappresentano le varie parti dello switch e le relative
le opzioni di CPU e memoria disponibili. Essendo la classe di primo livello, .gui-switch
contiene proprietà personalizzate utilizzate
a tutti gli elementi secondari dei componenti e punti di accesso per
personalizzazione.
Traccia
Lunghezza (--track-size
), spaziatura interna e due colori:
.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%);
}
}
Miniature
Le dimensioni, il colore dello sfondo e i colori di evidenziazione dell'interazione:
.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%);
}
}
Movimento ridotto
Per aggiungere un alias chiaro e ridurre le ripetizioni, un utente con preferenze di movimento ridotto la query supporti può essere inserita in una proprietà personalizzata con il plug-in basato su questa bozza nella sezione Query multimediali 5
@custom-media --motionOK (prefers-reduced-motion: no-preference);
Aumento
Ho scelto di aggregare il mio elemento <input type="checkbox" role="switch">
con un
<label>
, raggruppando la propria relazione per evitare l'associazione di caselle di controllo ed etichette
ambiguità, offrendo all'utente la possibilità di interagire con l'etichetta
attivare/disattivare l'input.
<label for="switch" class="gui-switch">
Label text
<input type="checkbox" role="switch" id="switch">
</label>
<input type="checkbox">
è predefinito
API
e state. La
del browser gestisce
checked
proprietà e input
eventi
ad esempio oninput
e onchanged
.
Layout
Flexbox grid e personalizzate proprietà sono fondamentali per mantenere gli stili di questo componente. Centralizzano i valori, assegnano nomi per calcoli o aree altrimenti ambigui e per abilitare una piccola proprietà personalizzata API per una facile personalizzazione dei componenti.
.gui-switch
Il layout di primo livello per il passaggio è flexbox. Il corso .gui-switch
contiene
le proprietà personalizzate pubbliche e private usate dai publisher secondari per calcolare
layout.
.gui-switch {
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
}
L'estensione e la modifica del layout flexbox sono analoghi a modificare il layout di un flexbox.
Ad esempio, per inserire etichette sopra o sotto un sensore o per modificare
flex-direction
:
<label for="light-switch" class="gui-switch" style="flex-direction: column">
Default
<input type="checkbox" role="switch" id="light-switch">
</label>
Traccia
L'input della casella di controllo viene definito come un cambio di traccia rimuovendo la sua normale
appearance: checkbox
e fornisce una dimensione specifica:
.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 traccia crea anche un'area di monitoraggio con griglia a singola cella una per una per un pollice reclamo.
Miniature
Lo stile appearance: none
rimuove anche il segno di spunta visivo fornito dallo
del browser. Questo componente utilizza un parametro
pseudo-elemento e :checked
pseudo-class sull'input in
sostituire questo indicatore visivo.
Il pollice è uno pseudo-elemento figlio collegato a input[type="checkbox"]
e
si posiziona sopra la traccia anziché sotto rivendicando l'area della griglia
track
:
.gui-switch > input::before {
content: "";
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
}
Stili
Le proprietà personalizzate consentono di utilizzare un componente di sensore versatile che si adatta al colore schemi, lingue da destra a sinistra e preferenze di movimento.
Stili di interazione tocco
Sui dispositivi mobili, i browser aggiungono le evidenziazioni del tocco e le funzionalità di selezione del testo a etichette e
di input. Ciò ha influito negativamente sullo stile e sul feedback visivo che
per usare questo interruttore. Con poche righe di CSS posso rimuovere questi effetti e aggiungere
il mio stile cursor: pointer
:
.gui-switch {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
Non è sempre consigliabile rimuovere questi stili, che possono avere valore visivo feedback sull'interazione. Se le rimuovi, assicurati di fornire alternative personalizzate.
Traccia
Gli stili di questo elemento riguardano principalmente la forma e il colore a cui accede
dal publisher principale .gui-switch
tramite
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'ampia varietà di opzioni di personalizzazione per il cambio di traccia è disponibile in quattro
proprietà personalizzate. border: none
è stato aggiunto perché appearance: none
non lo fa
rimuovi i bordi dalla casella di controllo in tutti i browser.
Miniature
L'elemento del pollice si trova già a destra track
, ma necessita di stili di cerchio:
.gui-switch > input::before {
background: var(--thumb-color);
border-radius: 50%;
}
Interazione
Utilizza le proprietà personalizzate per prepararti alle interazioni che mostreranno il passaggio del mouse le evidenziazioni e la posizione del pollice. Anche la preferenza dell'utente è selezionata prima di eseguire la transizione stili di evidenziazione movimento o al passaggio del mouse.
.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;
}}
}
Posizione pollice
Le proprietà personalizzate forniscono un unico meccanismo di origine per posizionare il pollice all'interno
la traccia. A nostra disposizione sono disponibili le dimensioni delle tracce e dei pollici che utilizzeremo
per mantenere lo spostamento corretto del pollice all'interno della traccia:
0%
e 100%
.
L'elemento input
è proprietario della variabile di posizione --thumb-position
e del pollice
lo pseudoelemento lo usa come posizione translateX
:
.gui-switch > input {
--thumb-position: 0%;
}
.gui-switch > input::before {
transform: translateX(var(--thumb-position));
}
Ora siamo liberi di modificare --thumb-position
da CSS e dalle pseudo-classi
forniti agli elementi delle caselle di controllo. Poiché in precedenza abbiamo impostato transition: transform
var(--thumb-transition-duration) ease
in modo condizionale su questo elemento, queste modifiche
può animarsi quando viene modificato:
/* 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)
);
}
Pensavo che questa orchestrazione disaccoppiata funzionasse bene. L'elemento pollice è
riguardare un solo stile, una posizione translateX
. L'input può gestire tutte
la complessità e i calcoli.
Verticale
Il supporto è stato eseguito con una classe di modificatore -vertical
che aggiunge una rotazione con
Il CSS viene trasformato nell'elemento input
.
Tuttavia, un elemento ruotato in 3D non modifica l'altezza complessiva del componente.
il che può compromettere il layout a blocchi. Tieni conto di questo problema utilizzando --track-size
e
--track-padding
variabili. Calcolare la quantità minima di spazio richiesta
un pulsante verticale che scorra nel layout come previsto:
.gui-switch.-vertical {
min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));
& > input {
transform: rotate(-90deg);
}
}
(RTL) da destra a sinistra
Io e un'amica CSS, Elad Schecter, abbiamo creato un prototipo un menu laterale a scorrimento, utilizzando le trasformazioni CSS che gestivano la scrittura da destra a sinistra lingue diverse capovolgendo . L'abbiamo fatto perché non ci sono trasformazioni logiche in CSS e potrebbero non esserci mai. Elad ha avuto l'idea di utilizzare un valore di proprietà personalizzato di invertire le percentuali, per consentire la gestione di un'unica sede logica per le trasformazioni logiche. Ho usato la stessa tecnica in questo passaggio e pensi che sia andata alla grande:
.gui-switch {
--isLTR: 1;
&:dir(rtl) {
--isLTR: -1;
}
}
Una proprietà personalizzata denominata --isLTR
inizialmente contiene il valore 1
, il che significa che
true
poiché il nostro layout va da sinistra a destra per impostazione predefinita. Quindi, utilizzando il linguaggio CSS
pseudoclasse :dir()
,
il valore è impostato su -1
quando il componente è in un layout da destra a sinistra.
Metti in azione --isLTR
utilizzandolo in un calc()
all'interno di una trasformazione:
.gui-switch.-vertical > input {
transform: rotate(-90deg);
transform: rotate(calc(90deg * var(--isLTR) * -1));
}
Ora la rotazione del sensore verticale tiene conto della posizione del lato opposto richiesti dal layout da destra a sinistra.
Anche le trasformazioni translateX
sullo pseudo-elemento pollice devono essere aggiornate in
tenere conto del requisito del lato opposto:
.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)
);
}
Anche se questo approccio non è efficace per risolvere tutte le esigenze relative a un concetto come il CSS logico trasformazioni, offre I principi DRY per molti e casi d'uso specifici.
Stati
L'utilizzo dell'asset integrato input[type="checkbox"]
non sarebbe completo senza
per gestire i vari stati in cui può trovarsi: :checked
, :disabled
:indeterminate
e :hover
. :focus
è stata lasciata intenzionalmente da sola, con un
l'aggiustamento apportato solo al suo offset; l'anello di messa a fuoco sembrava ottimo su Firefox
Safari:
Selezionato
<label for="switch-checked" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>
Questo stato rappresenta lo stato on
. In questo stato, l'input "track"
lo sfondo sia impostato sul colore attivo e la posizione del pollice è impostata su "
fine".
.gui-switch > input:checked {
background: var(--track-color-active);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
Disabilitato
<label for="switch-disabled" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>
Un pulsante :disabled
non solo ha un aspetto diverso, ma dovrebbe anche rendere il
immutabile.L'immutabilità dell'interazione è senza costi dal browser, ma
gli stati visivi richiedono stili a causa dell'uso di 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%);
}}
}
}
Questo stato è complicato perché richiede temi chiari e scuri sia disattivati che selezionati. Ho scelto stilisticamente stili minimalisti per rendere questi stati più nitidi la manutenzione delle combinazioni di stili.
Indeterminato
Uno stato spesso dimenticato è :indeterminate
, dove una casella di controllo non è né
selezionata o deselezionata. È uno stile divertente, invitante e senza pretese. Un buon
ricorda che gli stati booleani possono presentarsi
occulti tra uno stato e l'altro.
È difficile impostare una casella di controllo indeterminata, solo JavaScript può impostarla:
<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>
Poiché per me lo stato è semplice e invitante, è stato appropriato metterlo il pulsante di attivazione/disattivazione al centro:
.gui-switch > input:indeterminate {
--thumb-position: calc(
calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
* var(--isLTR)
);
}
Passaci il mouse sopra
Le interazioni con il passaggio del mouse dovrebbero fornire un supporto visivo per l'UI connessa e anche forniscono indicazioni verso una UI interattiva. Questa opzione evidenzia il pollice con un anello semitrasparente quando passi il mouse sopra l'etichetta o l'input. Questo passaggio l'animazione fornisce quindi la direzione verso l'elemento pollice interattivo.
L'elemento "in evidenza" è stato eseguito con box-shadow
. Al passaggio del mouse, aumenta le dimensioni di --highlight-size
di un input non disattivato. Se l'utente approva il movimento, eseguiamo la transizione del box-shadow
e lo vediamo crescere. Se non va bene per il movimento, l'evidenziazione viene visualizzata all'istante:
.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
Secondo me, l'interfaccia di uno switch può sembrare inquietante nel tentativo di emulare un modello fisico dell'interfaccia utente, in particolare questo tipo con un cerchio all'interno di una traccia. iOS ha capito bene con il loro interruttore, puoi trascinarle da un lato all'altro ed è molto soddisfacente l'opzione. Al contrario, un elemento UI può sembrare inattivo se viene eseguito un gesto di trascinamento e non succede nulla.
Pollice trascinabili
Lo pseudo-elemento pollice riceve la sua posizione dallo .gui-switch > input
con ambito var(--thumb-position)
, JavaScript può fornire un valore di stile incorporato
l'input per aggiornare dinamicamente la posizione del pollice facendolo sembrare seguire
il gesto del puntatore. Quando il puntatore viene rilasciato, rimuovi gli stili incorporati e
determinare se il trascinamento era più vicino o attivo utilizzando la proprietà personalizzata
--thumb-position
. Questa è la colonna portante della soluzione: eventi puntatore
monitorare in modo condizionale le posizioni del puntatore per modificare le proprietà personalizzate CSS.
Poiché il componente era già funzionante al 100% prima che questo script venga visualizzato occorre un bel po' di lavoro per mantenere il comportamento esistente, ad esempio fare clic su un'etichetta per attivare/disattivare l'input. Il nostro codice JavaScript non deve aggiungere funzionalità in a spese delle funzionalità esistenti.
touch-action
Il trascinamento è un gesto, personalizzato, il che lo rende ideale per
Vantaggi di touch-action
. Nel caso di questo cambio, un gesto orizzontale dovrebbe
essere gestito dallo script o un gesto verticale registrato per il passaggio
e la variante corrispondente. Con touch-action
possiamo indicare al browser quali gesti gestire
questo elemento, in modo che uno script possa gestire un gesto senza concorrenza.
Il seguente CSS indica al browser che, quando un gesto del puntatore viene avviato in questa traccia di cambio, gestire i gesti verticali, non fare nulla con la posizione quelli:
.gui-switch > input {
touch-action: pan-y;
}
Il risultato desiderato è un gesto orizzontale che non esegue anche la panoramica o lo scorrimento . Un puntatore può scorrere verticalmente iniziando dall'input e far scorrere mentre quelle orizzontali sono gestite in modo personalizzato.
Utilità dello stile dei valori pixel
Durante la configurazione e durante il trascinamento, sarà necessario acquisire vari valori di numeri calcolati
dagli elementi. Le seguenti funzioni JavaScript restituiscono valori di pixel calcolati
una proprietà CSS. Viene utilizzato nello script di configurazione come questo
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,
}
Nota come window.getComputedStyle()
accetta un secondo argomento, uno pseudoelemento target. È abbastanza chiaro che JavaScript possa leggere così tanti valori dagli elementi, anche dagli pseudo-elementi.
dragging
Questo è un momento chiave per la logica di trascinamento e ci sono alcuni aspetti da considerare dal gestore di eventi di funzione:
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`)
}
L'hero dello script è state.activethumb
, il piccolo cerchio rappresentato dallo script
insieme a un puntatore. L'oggetto switches
è un Map()
in cui
sono di .gui-switch
e i valori sono limiti e dimensioni memorizzati nella cache che mantengono
efficiente lo script. La scrittura da destra a sinistra viene gestita utilizzando la stessa proprietà personalizzata
che il CSS è --isLTR
e che può utilizzarlo per invertire la logica e continuare
che supporta RTL. Anche il parametro event.offsetX
è importante, in quanto contiene un delta
utile per posizionare il pollice.
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
Questa riga finale di CSS imposta la proprietà personalizzata utilizzata dall'elemento thumb. Questo
un'assegnazione di valore verrebbe altrimenti trasferita nel tempo, ma un puntatore precedente
l'evento ha impostato temporaneamente --thumb-transition-duration
su 0s
e ciò che verrà rimosso
ci sarebbe stata un'interazione lenta.
dragEnd
Per consentire all'utente di trascinare all'esterno dell'interruttore e rilasciare, viene visualizzata una evento finestra globale necessario registrato:
window.addEventListener('pointerup', event => {
if (!state.activethumb) return
dragEnd(event)
})
Penso che sia molto importante che un utente abbia la libertà di trascinare abbastanza intelligente da tenerne conto. Non ci è voluto molto per gestirlo con questo passaggio, ma è stata necessaria un'attenta valutazione durante lo sviluppo e il processo di sviluppo.
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'interazione con l'elemento è stata completata, è il momento di impostare l'input selezionato
e rimuovere tutti gli eventi relativi ai gesti. La casella di controllo viene modificata con
state.activethumb.checked = determineChecked()
.
determineChecked()
Questa funzione, chiamata da dragEnd
, determina la posizione attuale del pollice
entro i limiti della sua traccia e restituisce true se è uguale o superiore a
a metà strada:
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
}
Considerazioni aggiuntive
Il gesto di trascinamento ha subito un debito di codice a causa della struttura HTML iniziale
dei dati, soprattutto se aggrega l'input in un'etichetta. L'etichetta, in quanto padre
, riceverebbe le interazioni di clic dopo l'input. Alla fine
Evento dragEnd
, potresti aver notato padRelease()
come suono strano
personalizzata.
const padRelease = () => {
state.recentlyDragged = true
setTimeout(_ => {
state.recentlyDragged = false
}, 300)
}
per tenere conto dell'etichetta su cui verrà fatto clic in un secondo momento, come verrebbe deselezionato, o verificarla, l'interazione eseguita da un utente.
Se dovessi farlo di nuovo, potrei prendere in considerazione la modifica del DOM con JavaScript durante l'upgrade dell'esperienza utente, in modo da creare un elemento che gestisca i clic sulle etichette e non si oppone al comportamento integrato.
Questo tipo di JavaScript è quello che preferisco scrivere, non voglio gestire bubbling evento condizionale:
const preventBubbles = event => {
if (state.recentlyDragged)
event.preventDefault() && event.stopPropagation()
}
Conclusione
Questo componente di passaggio per adolescenti si è rivelato la parte più impegnativa di tutte le sfide legate alla GUI. finora! Ora che sai come ci ho fatto, come faresti‽ 🙂
Diversificaamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, twittami con i link e la aggiungerò alla sezione dei remix della community qui sotto.
Remix della community
- @KonstantinRouda con un elemento personalizzato: demo e code.
- @jhvanderschee con un pulsante: Codepen.
Risorse
Trova il codice sorgente .gui-switch
su
GitHub.