Uma visão geral básica de como criar um componente de interruptor responsivo e acessível.
Nesta postagem, quero compartilhar ideias sobre uma maneira de criar componentes de interruptores. Teste a demonstração.
Se você preferir o vídeo, aqui está uma versão do YouTube desta postagem:
Visão geral
Um switch funciona de forma semelhante a uma caixa de seleção mas representa explicitamente os estados booleanos ativado e desativado.
Esta demonstração usa <input type="checkbox" role="switch">
na maior parte do
funcionalidade de armazenamento, que tem a vantagem de não precisar de CSS ou JavaScript
totalmente funcional e acessível. O carregamento de CSS possibilita o uso de
idiomas, verticalidade, animação e muito mais. Carregar JavaScript faz a troca
arrastáveis e tangíveis.
Propriedades personalizadas
As variáveis a seguir representam as várias partes da chave e os respectivos
. Como a classe de nível superior, .gui-switch
contém propriedades personalizadas usadas
em todos os componentes secundários, e pontos de entrada para
e personalização.
Faixa
O comprimento (--track-size
), o padding e as duas cores:
.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
O tamanho, a cor do plano de fundo e as cores de destaque da interação:
.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 reduzido
Para adicionar um alias claro e reduzir a repetição, um usuário com preferência de movimento reduzida consulta de mídia pode ser colocada em uma propriedade personalizada com o método PostCSS plug-in com base neste rascunho especificação em consultas de mídia 5.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
Marcação
Decidi unir meu elemento <input type="checkbox" role="switch">
com uma
<label>
, agrupando o relacionamento para evitar a associação de caixas de seleção e rótulos
ambiguidade, além de permitir que o usuário interaja com o rótulo para
alternar a entrada.
<label for="switch" class="gui-switch">
Label text
<input type="checkbox" role="switch" id="switch">
</label>
O <input type="checkbox">
vem pré-criado com um
API
e state. A
navegador gerencia
checked
propriedade e input
eventos
como oninput
e onchanged
.
Layouts
Flexbox; grid e custom são essenciais na manutenção dos estilos desse componente. Eles centralizam valores, dão nomes a cálculos ou áreas ambíguas e permitir uma pequena propriedade personalizada para facilitar a personalização de componentes.
.gui-switch
O layout de nível superior do switch é flexbox. A classe .gui-switch
contém
as propriedades personalizadas públicas e particulares que os filhos usam para calcular seus
layouts.
.gui-switch {
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
}
Estender e modificar o layout flexbox é como alterar qualquer layout flexbox.
Por exemplo, para colocar rótulos acima ou abaixo de um interruptor ou para alterar a
flex-direction
:
<label for="light-switch" class="gui-switch" style="flex-direction: column">
Default
<input type="checkbox" role="switch" id="light-switch">
</label>
Faixa
A entrada da caixa de seleção é estilizada como uma faixa de mudança, removendo seu
appearance: checkbox
e fornecendo o próprio tamanho:
.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;
}
A faixa também cria uma área de faixa de grade de célula por vez para que um polegar reivindicação.
Miniatura
O estilo appearance: none
também remove a marca de seleção visual fornecida pelo
navegador. Esse componente usa um
pseudoelemento e o :checked
pseudoclasse na entrada para
substituir esse indicador visual.
O thumb é um pseudoelemento filho anexado à input[type="checkbox"]
e
pilhas no topo da pista em vez de abaixo dela, reivindicando a área da grade
track
:
.gui-switch > input::before {
content: "";
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
}
Estilos
As propriedades personalizadas permitem um componente de interruptor versátil que se adapta à cor esquemas, idiomas da direita para a esquerda e preferências de movimento.
Estilos de interação de toque
Em dispositivos móveis, os navegadores adicionam destaques por toque e recursos de seleção de texto aos marcadores e
de entrada. Eles afetaram negativamente o estilo e o feedback da interação visual que
essa troca era necessária. Com algumas linhas de CSS, posso remover esses efeitos e adicionar
meu próprio estilo cursor: pointer
:
.gui-switch {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
Nem sempre é aconselhável remover esses estilos, já que podem ser valiosos recursos visuais feedback de interação. Lembre-se de fornecer alternativas personalizadas caso você as remova.
Faixa
Os estilos deste elemento têm principalmente a forma e a cor, que ele acessa
do .gui-switch
pai pela
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);
}
Uma grande variedade de opções de personalização para a faixa de mudança vem de quatro
propriedades personalizadas. border: none
foi adicionado porque appearance: none
não foi adicionado
remova as bordas da caixa de seleção em todos os navegadores.
Miniatura
O elemento "thumb" já está à direita da track
, mas precisa de estilos de círculo:
.gui-switch > input::before {
background: var(--thumb-color);
border-radius: 50%;
}
Interação
Use propriedades personalizadas ao se preparar para interações que vão mostrar o passar o cursor destaques e mudanças na posição do polegar. A preferência do usuário também é marcada antes da transição da ou passar o cursor sobre os estilos de destaque.
.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;
}}
}
Posição do polegar
As propriedades personalizadas fornecem um mecanismo de origem única para posicionar a marcação "Gostei"
na trilha. À nossa disposição estão os tamanhos de trilho e polegar que usaremos
cálculos para manter o polegar devidamente deslocado e entre dentro da faixa:
0%
e 100%
.
O elemento input
é proprietário da variável de posição --thumb-position
, e da
Ele é usado como uma posição translateX
:
.gui-switch > input {
--thumb-position: 0%;
}
.gui-switch > input::before {
transform: translateX(var(--thumb-position));
}
Agora, podemos mudar o --thumb-position
do CSS e das pseudoclasses.
fornecidos nos elementos da caixa de seleção. Como já definimos transition: transform
var(--thumb-transition-duration) ease
condicionalmente nesse elemento, essas mudanças
pode ser animado quando alterado:
/* 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)
);
}
Achei que essa orquestração separada funcionou bem. O elemento thumb é
só se preocupa com um estilo, uma posição translateX
. A entrada pode gerenciar todos
a complexidade e os cálculos.
Vertical
O suporte foi feito com uma classe modificadora -vertical
, que adiciona uma rotação com
Transformações CSS para o elemento input
.
No entanto, um elemento 3D girado em 3D não altera a altura total do componente,
o que pode desviar do layout de blocos. Para isso, use --track-size
e
Variáveis --track-padding
. Calcule a quantidade mínima de espaço necessária para
um botão vertical para fluir no layout como esperado:
.gui-switch.-vertical {
min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));
& > input {
transform: rotate(-90deg);
}
}
(RTL) da direita para a esquerda
Um amigo do CSS, Elad Schecter, e eu criamos o protótipo um menu lateral deslizante usando transformações CSS que lidam da direita para a esquerda idiomas virando uma única variável. Fizemos isso porque não há transformações de propriedade lógica no CSS, e talvez nunca haja. Elad teve a ótima ideia de usar um valor de propriedade personalizada para inverter porcentagens e permitir o gerenciamento de um único local lógica para transformações lógicas. Usei essa mesma técnica nessa troca e acho que funcionou muito bem:
.gui-switch {
--isLTR: 1;
&:dir(rtl) {
--isLTR: -1;
}
}
Uma propriedade personalizada chamada --isLTR
inicialmente contém um valor de 1
, o que significa que
true
, já que nosso layout é da esquerda para a direita por padrão. Em seguida, usando o CSS
pseudoclasse :dir()
,
o valor é definido como -1
quando o componente está em um layout da direita para a esquerda.
Coloque --isLTR
em ação usando-o em um calc()
dentro de uma transformação:
.gui-switch.-vertical > input {
transform: rotate(-90deg);
transform: rotate(calc(90deg * var(--isLTR) * -1));
}
Agora, a rotação da chave vertical representa a posição do lado oposto exigidos pelo layout da direita para a esquerda.
As transformações translateX
no pseudoelemento thumb também precisam ser atualizadas para
considere o requisito do lado oposto:
.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)
);
}
Embora essa abordagem não funcione para resolver todas as necessidades relacionadas a um conceito como o CSS lógico isso oferece algumas princípios DRY para muitos em diferentes casos de uso de negócios.
Estados
O uso do input[type="checkbox"]
integrado não seria completo sem
processando os vários estados em que pode estar: :checked
, :disabled
,
:indeterminate
e :hover
. :focus
foi intencionalmente deixado sozinho, com um
ajuste feito apenas em seu deslocamento; o anel de foco ficava ótimo no Firefox e
Safari:
Marcado
<label for="switch-checked" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>
Esse estado representa o estado on
. Neste estado, a entrada "track"
plano de fundo é definido como a cor ativa e a posição do polegar é definida como
fim".
.gui-switch > input:checked {
background: var(--track-color-active);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
Desativado
<label for="switch-disabled" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>
Um botão :disabled
não apenas tem uma aparência visual diferente, mas também deve tornar a
elemento imutável.A imutabilidade de interação é livre no navegador, mas o
os estados visuais precisam de estilos devido ao 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%);
}}
}
}
Esse estado é complicado, porque precisa de temas claros e escuros com as opções desativadas e estados marcados. Escolhi estilos mínimos para esses estados para facilitar a carga de manutenção das combinações de estilos.
Indeterminado
Um estado esquecido com frequência é :indeterminate
, em que uma caixa de seleção não é
está marcada ou desmarcada. Essa situação é divertida, convidativa e despretensiosa. Uma boa
Lembre-se de que os estados booleanos podem ter formas ocultas entre estados.
É complicado definir uma caixa de seleção como indeterminada, apenas o JavaScript pode defini-la:
<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 o estado é despretensioso e convidativo, para mim, era apropriado colocar a posição do botão do círculo no meio:
.gui-switch > input:indeterminate {
--thumb-position: calc(
calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
* var(--isLTR)
);
}
Passar cursor
As interações ao passar o cursor precisam oferecer suporte visual para a interface conectada e também que dão direções para a interface interativa. Essa chave destaca o polegar com um anel semitransparente quando é passado o mouse sobre o rótulo ou a entrada. Este cursor fornece a direção do elemento de polegar interativo.
O "destaque" efeito é feito com box-shadow
. Ao passar o cursor, de uma entrada não desativada, aumente o tamanho de --highlight-size
. Se o usuário concordar com o movimento, fazemos a transição da box-shadow
e a vemos crescer. Se o usuário não concordar com o movimento, o destaque será exibido instantaneamente:
.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 mim, uma interface de switch pode parecer estranha na tentativa de emular uma do usuário, especialmente desse tipo com um círculo dentro de uma faixa. O iOS acertou com o interruptor, é possível arrastá-los de um lado para o outro, e é muito satisfatório têm essa opção. Por outro lado, um elemento da interface pode parecer inativo se um gesto de arrastar for e nada acontece.
Ícones arrastáveis
O pseudoelemento de polegar recebe a posição dele de .gui-switch > input
com escopo var(--thumb-position)
, o JavaScript pode fornecer um valor de estilo in-line no
a entrada para atualizar dinamicamente a posição do polegar, fazendo com que pareça seguir
o gesto do ponteiro. Quando o ponteiro for solto, remova os estilos em linha e
determine se a ação de arrastar estava mais próxima de ativar ou desativar usando a propriedade personalizada
--thumb-position
: Essa é a espinha dorsal da solução. eventos de ponteiro
que rastreia condicionalmente as posições do ponteiro para modificar as propriedades personalizadas do CSS.
Como o componente já estava 100% funcional antes de este script ser exibido é necessário um pouco de trabalho para manter o comportamento existente, como clicando em um rótulo para alternar a entrada. Nosso JavaScript não deve adicionar recursos em detrimento dos recursos existentes.
touch-action
Arrastar é um gesto personalizado, o que o torna uma ótima opção para
Benefícios do touch-action
. No caso dessa chave, um gesto horizontal
ser processada pelo nosso script ou por um gesto vertical capturado para a chave vertical
variante. Com touch-action
, podemos dizer ao navegador quais gestos ele deve processar
esse elemento, para que um script possa lidar com um gesto sem concorrência.
O CSS a seguir instrui o navegador que, quando um gesto de ponteiro é iniciado em nesta faixa de alternância, lida com gestos verticais, não faz nada com horizontal as seguintes:
.gui-switch > input {
touch-action: pan-y;
}
O resultado desejado é um gesto horizontal que não mova nem role a tela página. Um ponteiro pode rolar verticalmente a partir da entrada e rolar o mas as horizontais são personalizadas.
Utilitários de estilo dos valores em pixels
Durante a configuração e durante a ação de arrastar, vários valores numéricos calculados precisam ser capturados
dos elementos. As seguintes funções JavaScript retornam valores de pixels calculados
determinada uma propriedade CSS. Ele é usado no script de configuração
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,
}
Observe como window.getComputedStyle()
aceita um segundo argumento, um pseudoelemento de destino. O JavaScript pode ler tantos valores de elementos, até mesmo de pseudoelementos.
dragging
Esse é um momento importante para a lógica de arrastar e há algumas coisas a observar do manipulador de eventos da função:
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`)
}
O herói do script é state.activethumb
, o pequeno círculo que esse script está.
com um ponteiro. O objeto switches
é um Map()
em que o
são .gui-switch
, e os valores são limites e tamanhos em cache que mantêm
eficiente do script. A escrita da direita para a esquerda é tratada usando a mesma propriedade personalizada
que o CSS é --isLTR
e pode usá-lo para inverter a lógica e continuar
com suporte a RTL. O event.offsetX
também é valioso, porque contém um delta.
útil para posicionar o polegar.
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
Essa linha final de CSS define a propriedade personalizada usada pelo elemento thumb. Isso
A atribuição de valor faria a transição com o tempo, mas um ponteiro anterior
defina --thumb-transition-duration
como 0s
temporariamente, removendo o que
teria sido uma interação lenta.
dragEnd
Para que o usuário possa arrastar para fora da chave e soltar, uma evento de janela global necessário registrado:
window.addEventListener('pointerup', event => {
if (!state.activethumb) return
dragEnd(event)
})
Acho muito importante que o usuário tenha liberdade para arrastar com flexibilidade e interface ser inteligente o suficiente para dar conta disso. Não demorou muito para lidar com isso mas exigiu uma consideração cuidadosa durante o desenvolvimento de desenvolvimento de software.
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()
}
A interação com o elemento foi concluída. É hora de configurar a entrada verificada
e remova todos os eventos de gesto. A caixa de seleção é alterada com
state.activethumb.checked = determineChecked()
:
determineChecked()
Essa função, chamada por dragEnd
, determina onde está a corrente do polegar.
dentro dos limites de sua faixa e retorna verdadeiro se for igual a ou acima
no meio da pista:
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
}
Mais ideias
O gesto de arrastar gerou um pouco de dívida no código devido à estrutura HTML inicial.
escolhido, principalmente em relação à entrada em um rótulo. O rótulo, sendo um pai
receberia interações de clique após a entrada. No fim do ciclo de vida
dragEnd
, talvez você tenha notado padRelease()
como algo estranho
função.
const padRelease = () => {
state.recentlyDragged = true
setTimeout(_ => {
state.recentlyDragged = false
}, 300)
}
Isso serve para considerar o rótulo que recebe esse clique mais tarde, já que seria desmarcado, a interação que um usuário realizou.
Se eu fizesse isso de novo, poderia considerar ajustar o DOM com JavaScript durante o upgrade da experiência do usuário, para criar um elemento que lide com os cliques do rótulo sem conflitos com o comportamento integrado.
Este tipo de JavaScript é o que eu menos gosto de escrever, não quero gerenciar propagação de eventos condicionais:
const preventBubbles = event => {
if (state.recentlyDragged)
event.preventDefault() && event.stopPropagation()
}
Conclusão
Esse pequeno componente de switch acabou sendo o mais trabalhoso de todos os desafios da GUI. até agora. Agora que você sabe como eu fiz isso, o que você faria‽ 🙂
Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie uma demonstração, envie um tweet para mim e adicione os links acesse a seção "Remixes da comunidade" abaixo.
Remixes da comunidade
- @KonstantinRouda com um elemento personalizado: demonstração e código.
- @jhvanderschee com um botão: Codepen.
Recursos
Encontre o código-fonte .gui-switch
em
GitHub.