Descripción general de los conceptos básicos para compilar un componente de interruptor responsivo y accesible
En esta publicación, quiero compartir ideas sobre una manera de crear componentes de interruptores. Prueba la demostración.
Si prefieres ver un video, aquí tienes una versión de YouTube de esta publicación:
Descripción general
Un switch funciona de manera similar a una casilla de verificación. pero representan explícitamente los estados de encendido y apagado booleanos.
En esta demostración, se usa <input type="checkbox" role="switch">
para la mayor parte de su
de Google Cloud, que tiene la ventaja de no necesitar que CSS o JavaScript
completamente funcionales y accesibles. La carga de CSS admite la escritura de derecha a izquierda.
lenguajes, verticalidad, animación y más. La carga de JavaScript realiza el cambio
arrastrables y tangibles.
Propiedades personalizadas
Las siguientes variables representan las distintas partes del switch y su
opciones de estado. Como clase de nivel superior, .gui-switch
contiene las propiedades personalizadas que se usan.
en todos los componentes secundarios y los puntos de entrada para
personalización.
Pista
La longitud (--track-size
), el padding y los dos colores:
.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%);
}
}
Miniatura
El tamaño, el color de fondo y los colores de resaltado de la interacción:
.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%);
}
}
Movimiento reducido
Para agregar un alias claro y reducir la repetición, un usuario con preferencia de movimiento la consulta de medios se puede colocar en una propiedad personalizada con el método PostCSS complemento basado en este borrador la especificación en las consultas de medios 5.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
Marca
Elegí unir mi elemento <input type="checkbox" role="switch">
con una
<label>
, agrupando su relación para evitar la asociación de casillas de verificación y etiquetas
ambigüedad, y al mismo tiempo que se brinda al usuario la posibilidad de interactuar con la etiqueta para
activar o desactivar la entrada.
<label for="switch" class="gui-switch">
Label text
<input type="checkbox" role="switch" id="switch">
</label>
<input type="checkbox">
viene precompilado con un
API
y state. El
navegador administra la
checked
propiedad e entrada
eventos
como oninput
y onchanged
.
Diseños
Flexbox grid y custom propiedades son fundamentales para mantener los estilos de este componente. Centralizan los valores, les dan nombres para cálculos o áreas que de otra manera serían ambiguas, y permitir una pequeña propiedad personalizada para personalizar componentes fácilmente.
.gui-switch
El diseño de nivel superior del interruptor es flexbox. La clase .gui-switch
contiene
las propiedades personalizadas privadas y públicas que los elementos secundarios usan para calcular sus
y diseños.
.gui-switch {
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
}
Extender y modificar el diseño de flexbox es como cambiar cualquier diseño de flexbox.
Por ejemplo, para colocar etiquetas encima o debajo de un interruptor, o para cambiar la
flex-direction
:
<label for="light-switch" class="gui-switch" style="flex-direction: column">
Default
<input type="checkbox" role="switch" id="light-switch">
</label>
Pista
La entrada de la casilla de verificación tiene el estilo de una pista de interruptor quitando su estado
appearance: checkbox
y, en su lugar, proporciona su propio tamaño:
.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;
}
El seguimiento también crea un área de seguimiento de cuadrícula de una por una celda para que el dedo pulgar reclamar.
Miniatura
El estilo appearance: none
también quita la marca de verificación visual proporcionada por el
navegador. Este componente utiliza un
pseudoelemento y :checked
pseudoclase en la entrada para
reemplaza este indicador visual.
El pulgar es un seudoelemento secundario adjunto a input[type="checkbox"]
y
se apila sobre la pista en lugar de debajo de ella reclamando el área de la cuadrícula
track
.gui-switch > input::before {
content: "";
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
}
Estilos
Las propiedades personalizadas permiten un componente de interruptor versátil que se adapta al color esquemas, idiomas con orientación de derecha a izquierda y preferencias de movimiento.
Estilos de interacción táctil
En dispositivos móviles, los navegadores agregan elementos destacados al presionar y funciones de selección de texto a las etiquetas y
de datos. que afectaron negativamente el estilo y la retroalimentación visual que
necesita este interruptor. Con unas pocas líneas de CSS, puedo quitar esos efectos y agregar
mi propio estilo de cursor: pointer
:
.gui-switch {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
No siempre es recomendable quitar esos estilos, ya que pueden ser elementos visuales valiosos retroalimentación de interacción. Asegúrate de proporcionar alternativas personalizadas si las quitas.
Pista
Los estilos de este elemento se centran principalmente en su forma y color, a los que accede
del elemento superior .gui-switch
mediante el
en cascada.
.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);
}
Hay cuatro opciones de personalización para el segmento de interruptor
propiedades personalizadas. Se agregó border: none
porque appearance: none
no
Elimina los bordes de la casilla de verificación de todos los navegadores.
Miniatura
El elemento miniatura ya se encuentra en el track
correcto, pero necesita estilos de círculo:
.gui-switch > input::before {
background: var(--thumb-color);
border-radius: 50%;
}
Interacción
Usa propiedades personalizadas a fin de prepararte para las interacciones que mostrarán cuando se coloque el cursor sobre un elemento. zonas brillantes y cambios de posición de los dedos. La preferencia del usuario también es verificar antes de realizar la transición movimientos o el desplazamiento del cursor.
.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;
}}
}
Posición del pulgar
Las propiedades personalizadas proporcionan un único mecanismo de fuente para posicionar la miniatura en
la pista. A nuestra disposición están los tamaños de las pistas y de los pulgares que utilizaremos en
cálculos para mantener el pulgar correctamente desplazado y entre dentro de la vía:
0%
y 100%
.
El elemento input
posee la variable de posición --thumb-position
y el círculo.
seudoelemento lo usa como una posición translateX
:
.gui-switch > input {
--thumb-position: 0%;
}
.gui-switch > input::before {
transform: translateX(var(--thumb-position));
}
Ahora podemos cambiar --thumb-position
desde CSS y las seudoclases.
en los elementos de la casilla de verificación. Como configuramos condicionalmente transition: transform
var(--thumb-transition-duration) ease
antes en este elemento, estos cambios
puede animarse cuando se modifique:
/* 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)
);
}
Pensé que esta organización desacoplada había funcionado bien. El elemento del pulgar es
Solo se ocupa de un estilo, una posición de translateX
. La entrada puede administrar todas
la complejidad y los cálculos.
Vertical
La compatibilidad se realizó con una clase de modificador -vertical
, que agrega una rotación con
Transformaciones de CSS al elemento input
.
Sin embargo, un elemento rotado en 3D no cambia la altura general del componente,
lo que puede desestabilizar el diseño de bloques. Ten en cuenta esto con --track-size
y
--track-padding
variables. Calcula la cantidad mínima de espacio requerido para
un botón vertical para que fluya en el diseño como se espera:
.gui-switch.-vertical {
min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));
& > input {
transform: rotate(-90deg);
}
}
(RTL) de derecha a izquierda
Un amigo del CSS, Elad Schecter, y yo fui un prototipo un menú lateral deslizable con transformaciones de CSS que se manejan de derecha a izquierda idiomas cambiando un solo de salida. Lo hicimos porque no hay transformaciones de propiedades lógicas en CSS y puede que nunca los haya. Elad tuvo la gran idea de usar un valor de propiedad personalizada para invertir porcentajes y permitir la administración de una sola ubicación de nuestro lógica para transformaciones lógicas. Usé la misma técnica en este cambio y creo que funcionó muy bien:
.gui-switch {
--isLTR: 1;
&:dir(rtl) {
--isLTR: -1;
}
}
Una propiedad personalizada llamada --isLTR
inicialmente contiene un valor de 1
, lo que significa que
true
, ya que nuestro diseño es de izquierda a derecha de forma predeterminada. Luego, con el código CSS,
seudoclase :dir()
,
el valor se establece en -1
cuando el componente se encuentra en un diseño de derecha a izquierda.
Para usar --isLTR
, utilízalo en una calc()
dentro de una transformación:
.gui-switch.-vertical > input {
transform: rotate(-90deg);
transform: rotate(calc(90deg * var(--isLTR) * -1));
}
Ahora la rotación del interruptor vertical corresponde a la posición del lado opuesto que requiere el diseño de derecha a izquierda.
Las transformaciones translateX
en el seudoelemento Thumb también deben actualizarse a
tienen en cuenta el requisito del lado opuesto:
.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)
);
}
Aunque este enfoque no funcionará para resolver todas las necesidades relacionadas con un concepto como el CSS lógico ofrece algunas transformaciones. DRY para muchos casos de uso.
Estados
El uso del elemento input[type="checkbox"]
integrado no estaría completo sin
que controla los diferentes estados en los que puede estar: :checked
, :disabled
,
:indeterminate
y :hover
. :focus
se dejó solo intencionalmente, con un
ajuste realizado solo a su desplazamiento; el anillo de enfoque se veía muy bien en Firefox y
Safari:
Marcado
<label for="switch-checked" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>
Este estado representa el estado on
. En este estado, la entrada “track”
el fondo está establecido en el color activo y la posición del pulgar se establece en "el
final".
.gui-switch > input:checked {
background: var(--track-color-active);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
Inhabilitado
<label for="switch-disabled" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>
Un botón :disabled
no solo tiene un aspecto visual diferente, sino que también debe
elemento inmutable.La inmutabilidad de la interacción está libre desde el navegador, pero la
los estados visuales necesitan estilos debido al uso de appearance: none
.
.gui-switch > input:disabled {
cursor: not-allowed;
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);
@media (prefers-color-scheme: dark) { & {
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
}}
}
}
Este estado es complicado, ya que necesita temas oscuros y claros, con las opciones estados marcados. Elegí estilísticamente estilos minimalistas en estos estados para facilitar la carga de mantenimiento de las combinaciones de estilos.
Indeterminado
Un estado que se suele olvidar es :indeterminate
, en el que una casilla de verificación no es
marcada o desmarcada. Es un estado divertido, atractivo y discreto. Un buen
como recordatorio de que los estados booleanos pueden pasar de forma indefinida entre estados.
Es difícil configurar una casilla de verificación como "indeterminada", solo JavaScript puede configurarla:
<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>
Como el estado, para mí, es sencillo y acogedor, se sintió apropiado la posición de la perilla del interruptor en el medio:
.gui-switch > input:indeterminate {
--thumb-position: calc(
calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
* var(--isLTR)
);
}
Colocar el cursor sobre un elemento
Las interacciones cuando se coloca el cursor sobre un elemento deben brindar compatibilidad visual con la IU conectada y, además, proporcionan orientación hacia la IU interactiva. Este interruptor destaca el pulgar con Un anillo semitransparente cuando se coloca el cursor sobre la etiqueta o la entrada. Este botón de desplazamiento animación que luego indica la dirección del elemento de miniatura interactivo.
Lo más destacado el efecto se completa con box-shadow
. Cuando colocas el cursor sobre una entrada que no está inhabilitada, aumenta el tamaño de --highlight-size
. Si el usuario está de acuerdo con el movimiento, realizamos la transición de box-shadow
y vemos que crece. Si no está de acuerdo con el movimiento, el resaltado aparece al instante:
.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
Para mí, la interfaz de un switch puede resultar extraña en su intento de emular especial, en este tipo con un círculo dentro de una pista. iOS acertó con su interruptor, puedes arrastrarlas de lado a lado, y es muy satisfactorio tienen la opción. Por el contrario, un elemento de la IU puede sentirse inactivo si un gesto de arrastre se se intenta y no pasa nada.
Dedos arrastrables
El seudoelemento Thumb recibe su posición del .gui-switch > input
.
var(--thumb-position)
con alcance, JavaScript puede proporcionar un valor de estilo intercalado en
la entrada para actualizar dinámicamente la posición del pulgar y hacer que parezca que sigue
el gesto del puntero. Cuando se suelta el puntero, quita los estilos intercalados y
determinar si el arrastre estuvo más cerca de desactivado o activado usando la propiedad personalizada
--thumb-position
Esta es la columna vertebral de la solución: eventos de puntero
realizar un seguimiento condicional de las posiciones de un puntero para modificar las propiedades personalizadas de CSS.
Dado que el componente ya era 100% funcional antes de que se mostrara esta secuencia de comandos se necesita bastante trabajo para mantener el comportamiento existente, como haciendo clic en una etiqueta para activar o desactivar la entrada. Nuestro JavaScript no debería agregar funciones en el gasto de los atributos existentes.
touch-action
Arrastrar es un gesto personalizado, lo que lo convierte en un gran candidato para
touch-action
. En este caso, un gesto horizontal
puede controlarse mediante nuestra secuencia de comandos, o un gesto vertical capturado para el interruptor
variante. Con touch-action
, podemos indicarle al navegador qué gestos debe controlar.
este elemento, de modo que una secuencia de comandos pueda controlar un gesto sin competencia.
El siguiente CSS le indica al navegador que cuando un gesto de puntero se inicie en en esta pista de interruptor, controlar gestos verticales, no hacer nada con los siguientes:
.gui-switch > input {
touch-action: pan-y;
}
El resultado deseado es un gesto horizontal que no también desplaza o desplaza en el . Un puntero puede desplazarse verticalmente desde la entrada y hasta la pero las horizontales se manejan de forma personalizada.
Utilidades Pixel Value
Durante la configuración y durante el arrastre, se deberán tomar varios valores de números calculados
a partir de los elementos. Las siguientes funciones de JavaScript muestran valores de píxeles calculados
según una propiedad de CSS. Se usa en la secuencia de comandos de configuración de esta manera
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,
}
Observa cómo window.getComputedStyle()
acepta un segundo argumento, un seudoelemento de destino. Es bastante bueno que JavaScript pueda leer tantos valores de los elementos, incluso de los seudoelementos.
dragging
Este es un momento central para la lógica de arrastre y hay algunos aspectos con destacar del controlador de eventos de la función:
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`)
}
El héroe del guion es state.activethumb
; el círculo pequeño que es esta secuencia de comandos.
junto con un puntero. El objeto switches
es una Map()
en la que el
son .gui-switch
y los valores son límites y tamaños almacenados en caché que mantienen
de la secuencia de comandos. La orientación de derecha a izquierda se controla con la misma propiedad personalizada.
que CSS es --isLTR
y que puede usarlo para invertir la lógica y continuar
que admite RTL. El event.offsetX
también es valioso, ya que contiene un delta
útil para posicionar el pulgar.
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
La línea final del CSS establece la propiedad personalizada que usa el elemento miniatura. Esta
de asignación de valor cambiaría con el tiempo, pero un puntero anterior
el evento estableció temporalmente --thumb-transition-duration
en 0s
y quitó lo que
habría sido una interacción lenta.
dragEnd
Para que el usuario pueda arrastrar lejos del interruptor y soltarlo, se muestra Se requiere un evento de ventana global registrado:
window.addEventListener('pointerup', event => {
if (!state.activethumb) return
dragEnd(event)
})
Creo que es muy importante que el usuario tenga la libertad de arrastrar libremente y tener la sea lo suficientemente inteligente como para tenerlo en cuenta. No tardó mucho en manejarlo. con este cambio, pero fue necesario considerarlo detenidamente durante el proceso el proceso de administración de recursos.
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()
}
Se completó la interacción con el elemento. Se verifica el tiempo para configurar la entrada.
y quita todos los eventos de gestos. La casilla de verificación se cambia
state.activethumb.checked = determineChecked()
determineChecked()
Esta función, que llama dragEnd
, determina dónde se encuentra la corriente de miniatura.
dentro de los límites de su seguimiento y devuelve el valor true si es igual o superior
medio camino en la vía:
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
}
Pensamientos adicionales
El gesto de arrastre incurrió en cierta deuda de código debido a la estructura inicial HTML
elegido, principalmente encapsulando la entrada en una etiqueta. La etiqueta, que es una superior
recibirían interacciones de clic después de la entrada. Al final del
dragEnd
. Quizás hayas notado que padRelease()
tiene un sonido extraño
.
const padRelease = () => {
state.recentlyDragged = true
setTimeout(_ => {
state.recentlyDragged = false
}, 300)
}
Esto es para tener en cuenta que la etiqueta obtiene este clic posterior, ya que estaría desmarcada, o verificar, la interacción que realizó un usuario.
Si tuviera que volver a hacer esto, podría considerar ajustar el DOM con JavaScript. durante la actualización de UX, para crear un elemento que controle los clics en las etiquetas y no lucha con el comportamiento integrado.
Este tipo de JavaScript es el que menos me gusta para escribir, no quiero administrarlo burbuja de eventos condicionales:
const preventBubbles = event => {
if (state.recentlyDragged)
event.preventDefault() && event.stopPropagation()
}
Conclusión
Este pequeño componente del interruptor terminó siendo el más trabajo de todos los desafíos de la GUI. hasta ahora. Ahora que sabes cómo lo hice, ¿cómo lo harías?‽ 🙂
Diversifiquemos nuestros enfoques y aprendamos todas las formas de desarrollar en la Web. Crear una demostración, twittearme vínculos y la agregaré. a la sección de remixes de la comunidad.
Remixes de la comunidad
- @KonstantinRouda con un elemento personalizado: demostración y código.
- @jhvanderschee con un botón: CodePen.
Recursos
Encuentra el código fuente .gui-switch
en
GitHub.