Cómo compilar un componente de interruptor

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.

. 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.

R
etiqueta natural y sin estilo, y casilla de verificación.

<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.

Herramientas para desarrolladores de Flexbox superpuestas a una etiqueta horizontal y un interruptor, que muestran su diseño
distribución del espacio.

.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:

Herramientas para desarrolladores de Flexbox superpuestas a una etiqueta vertical y un interruptor.

<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:

Herramientas para desarrolladores de cuadrícula que se superpone al segmento de interruptores y se muestra el segmento de cuadrícula con nombre
las áreas con el nombre &quot;track&quot;.

.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

Herramientas para desarrolladores que muestran la miniatura del pseudoelemento ubicado dentro de una cuadrícula de CSS.

.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.

Una comparación en paralelo del tema claro y oscuro del interruptor y su
estados.

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.

Se cambian las variantes con tamaños y colores personalizados del segmento.

.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%;
}

Se muestra Herramientas para desarrolladores destacando el seudoelemento del círculo.

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:

Captura de pantalla de un anillo de enfoque enfocado en un interruptor 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%);
    }}
  }
}

El interruptor con estilo oscuro está inhabilitado, marcado y desmarcado
estados.

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>

El estado indeterminado que tiene el círculo de seguimiento en la
medio, para indicar “indeciso”.

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

Recursos

Encuentra el código fuente .gui-switch en GitHub.