Uma visão geral básica de como criar um componente de configurações de controles deslizantes e caixas de seleção.
Nesta postagem, quero compartilhar ideias sobre como criar um componente de configurações para a Web que seja responsivo, ofereça suporte a várias entradas de dispositivo e funcione em vários navegadores. Teste a demonstração.
Se você prefere vídeos ou quer uma prévia da IU/UX do que estamos criando, confira este tutorial mais curto no YouTube:
Visão geral
Dividimos os aspectos desse componente nas seguintes seções:
- Layouts
- Cor
- Entrada de intervalo personalizado
- Entrada de caixa de seleção personalizada
- Considerações sobre acessibilidade
- JavaScript
Layouts
Esta é a primeira demonstração do Desafio da GUI que é totalmente em grade CSS. Confira cada grade destacada com o Chrome DevTools para grade:

Just for gap
O layout mais comum:
foo {
display: grid;
gap: var(--something);
}
Eu chamo esse layout de "apenas para espaçamento" porque ele só usa a grade para adicionar espaçamentos entre os blocos.
Cinco layouts usam essa estratégia, todos mostrados aqui:

O elemento fieldset, que contém cada grupo de entrada (.fieldset-item), está usando gap: 1px para
criar as bordas finas entre os elementos. Não há uma solução de fronteira complicada.
.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); } } }
Quebra de grade natural
O layout mais complexo acabou sendo o macro, o sistema de layout lógico
entre <main> e <form>.
Como centralizar o conteúdo agrupado
O Flexbox e o Grid oferecem recursos para align-items ou
align-content. Ao lidar com elementos de quebra de linha, os alinhamentos de layout
content distribuem o espaço entre os filhos como um grupo.
main {
display: grid;
gap: var(--space-xl);
place-content: center;
}
O elemento principal está usando a abreviação de alinhamento place-content: center para que os filhos sejam centralizados vertical e horizontalmente em layouts de uma e duas colunas.
Assista ao vídeo acima para ver como o "conteúdo" permanece centralizado, mesmo que o agrupamento tenha ocorrido.
Repetir minmax de ajuste automático
O <form> usa um layout de grade adaptável para cada seção.
Esse layout alterna entre uma e duas colunas com base no espaço disponível.
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;
}
Essa grade tem um valor diferente para row-gap (--space-xl) em relação a column-gap (--space-xxl)
para dar um toque personalizado ao layout responsivo. Quando as colunas se empilham, queremos
um espaço grande, mas não tão grande quanto em uma tela ampla.
A propriedade grid-template-columns usa três funções CSS: repeat(), minmax() e
min(). Una Kravets tem uma ótima postagem de blog sobre layout sobre isso, chamando-a de RAM.
Há três adições especiais no nosso layout, se compararmos com o da Una:
- Passamos uma função
min()extra. - Especificamos
align-items: flex-start. - Há um estilo
max-width: 89vw.
A função extra min() é bem descrita por Evan Minto no blog dele, na
postagem Intrinsically Responsive CSS Grid with minmax() and
min() (em inglês).
Recomendo a leitura. A correção de alinhamento flex-start é para
remover o efeito de alongamento padrão, para que os filhos desse layout não
precisem ter alturas iguais, mas sim alturas naturais e intrínsecas. O
vídeo do YouTube tem um resumo rápido dessa adição de alinhamento.
max-width: 89vw vale uma pequena análise neste post.
Veja o layout com e sem o estilo aplicado:
O que está acontecendo? Quando max-width é especificado, ele fornece contexto,
dimensionamento explícito ou dimensionamento
definido para que o algoritmo de layout
auto-fit saiba quantas
repetições podem caber no espaço. Embora pareça óbvio que o
espaço é de "largura total", de acordo com a especificação de grade CSS, um tamanho ou tamanho máximo definido precisa
ser fornecido. Forneci um tamanho máximo.
Então, por que 89vw? Porque "funcionou" para o meu layout.
Eu e outras pessoas do Chrome estamos investigando por que um valor mais razoável,
como 100vw, não é suficiente e se isso é de fato um bug.
Espaçamento
A maior parte da harmonia desse layout vem de uma paleta limitada de espaçamento, 7 para ser exato.
:root {
--space-xxs: .25rem;
--space-xs: .5rem;
--space-sm: 1rem;
--space-md: 1.5rem;
--space-lg: 2rem;
--space-xl: 3rem;
--space-xxl: 6rem;
}
O uso deles funciona muito bem com grade, CSS @nest e sintaxe de nível 5
de @media. Veja um exemplo: o conjunto de estilos de layout totalmente <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);
}
}
}
Uma grade com conteúdo centralizado, com preenchimento moderado por padrão (como em dispositivos móveis). No entanto, à medida que mais espaço de janela de visualização fica disponível, ele se espalha aumentando o padding. O CSS de 2021 está muito bom!
Lembra do layout anterior, "apenas para lacunas"? Veja uma versão mais completa de como eles aparecem neste componente:
header {
display: grid;
gap: var(--space-xxs);
}
section {
display: grid;
gap: var(--space-md);
}
Cor
O uso controlado de cores ajudou esse design a se destacar como expressivo, mas minimalista. Eu faço assim:
: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);
}
Eu nomeio as cores da superfície e do texto com números, em vez de nomes como
surface-dark e surface-darker, porque em uma consulta de mídia, eu as inverto,
e claro e escuro não fazem sentido.
Eu as inverto em uma consulta de mídia de preferência como esta:
: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 ter uma visão geral da estratégia antes de entrar nos detalhes da sintaxe de cores. Mas, como me adiantei um pouco, vou voltar um pouco.
LCH?
Sem entrar muito na teoria das cores, o LCH é uma sintaxe voltada para humanos, que atende à forma como percebemos as cores, não como as medimos com matemática (como 255). Isso dá uma vantagem distinta, já que os humanos podem escrever com mais facilidade e outros humanos vão estar em sintonia com esses ajustes.
Hoje, nesta demonstração, vamos focar na sintaxe e nos valores que estou alternando para criar os modos claro e escuro. Vamos analisar uma cor de superfície e uma de texto:
: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) se traduz em 10% de luminosidade, 0 de croma e 0 de matiz: um
cinza muito escuro e sem cor. Em seguida, na consulta de mídia para o modo claro, a luminosidade
é invertida para 90% com --surface1: lch(90 0 0);. E essa é a essência da estratégia. Comece mudando apenas a luminosidade entre os dois temas, mantendo as
proporções de contraste exigidas pelo design ou o que pode manter a acessibilidade.
O bônus com lch() aqui é que a luminosidade é orientada para humanos, e podemos nos sentir
bem com uma mudança de % nela, que será perceptível e consistente
com essa diferença de %. hsl(), por exemplo, não é tão
confiável.
Há mais a
aprender sobre
espaços de cores e lch(), se você tiver interesse. Está chegando!
No momento, o CSS não consegue acessar essas cores. Vou repetir: não temos acesso a um terço das cores na maioria dos monitores modernos. E não são cores quaisquer, mas as cores mais vívidas que a tela pode exibir. Nossos sites estão desbotados porque o hardware do monitor evoluiu mais rápido do que as especificações de CSS e as implementações do navegador.
Lea Verou
Controles de formulário adaptáveis com esquema de cores
Muitos navegadores oferecem controles de tema escuro, como o Safari e o Chromium, mas você precisa especificar no CSS ou HTML que seu design os utiliza.
O exemplo acima demonstra o efeito da propriedade no painel "Estilos" do DevTools. A demonstração usa a tag HTML, que, na minha opinião, geralmente é um local melhor:
<meta name="color-scheme" content="dark light">
Saiba tudo sobre isso neste artigo do color-scheme (em inglês) escrito por Thomas Steiner (em inglês). Há muito mais a ganhar
do que entradas de caixa de seleção escuras.
CSS accent-color
Houve atividades
recentes em torno de
accent-color em elementos de formulário, sendo um único estilo CSS que pode mudar a
cor de tonalidade usada no elemento de entrada dos navegadores. Leia mais sobre isso aqui no
GitHub. Eu o incluí nos meus
estilos para esse componente. Como os navegadores são compatíveis, as caixas de seleção vão ficar
mais no tema com os destaques de cor rosa e roxo.
input[type="checkbox"] {
accent-color: var(--brand);
}

Destaques de cor com gradientes fixos e foco interno
As cores se destacam mais quando usadas com moderação, e uma das maneiras de conseguir isso é com interações coloridas na interface.
Há muitas camadas de feedback e interação da interface no vídeo acima, que ajudam a dar personalidade à interação:
- Destaque de contexto.
- Fornecer feedback da IU sobre "quão cheio" o valor está no intervalo.
- Fornecer feedback da interface de que um campo está aceitando entrada.
Para fornecer feedback quando um elemento está sendo interagido, o CSS usa a pseudoclasse
:focus-within
para mudar a aparência de vários elementos. Vamos analisar o
.fieldset-item, que é superinteressante:
.fieldset-item {
...
&:focus-within {
background: var(--surface2);
& svg {
fill: white;
}
& picture {
clip-path: circle(50%);
background: var(--brand-bg-gradient) fixed;
}
}
}
Quando um dos filhos desse elemento tem focus-within:
- O plano de fundo
.fieldset-itemrecebe uma cor de superfície de maior contraste. - O
svganinhado é preenchido em branco para aumentar o contraste. - O
<picture>clip-pathaninhado se expande para um círculo completo, e o plano de fundo é preenchido com o gradiente fixo brilhante.
Intervalo personalizado
Considerando o elemento de entrada HTML a seguir, vou mostrar como personalizei a aparência dele:
<input type="range">
Há três partes desse elemento que precisamos personalizar:
Estilos de elementos de intervalo
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 */
}
As primeiras linhas de CSS são as partes personalizadas dos estilos, e espero que rotulá-las claramente ajude. O restante dos estilos são, em grande parte, estilos de redefinição, para fornecer uma base consistente para criar as partes complicadas do componente.
Estilos de faixa
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;
}
O truque é "revelar" a cor de preenchimento vibrante. Isso é feito com o gradiente de parada brusca na parte de cima. O gradiente é transparente até a porcentagem de preenchimento e, depois disso, usa a cor da superfície da faixa não preenchida. Atrás dessa superfície não preenchida, há uma cor de largura total, esperando que a transparência a revele.
Estilo de preenchimento da faixa
Meu design precisa de JavaScript para manter o estilo de preenchimento. Há estratégias somente CSS, mas elas exigem que o elemento de controle tenha a mesma altura da faixa, e não consegui encontrar uma harmonia dentro desses limites.
/* 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));
})
})
Acho que isso é um bom upgrade visual. O controle deslizante funciona muito bem sem
JavaScript, a propriedade --track-fill não é obrigatória, ele simplesmente não terá um
estilo de preenchimento se não estiver presente. Se o JavaScript estiver disponível, preencha a propriedade personalizada e observe as mudanças do usuário, sincronizando a propriedade personalizada com o valor.
Confira esta ótima
postagem no
CSS-Tricks (em inglês) de Ana
Tudor, que demonstra uma solução somente CSS para
preenchimento de faixa. Também achei este
elemento range muito inspirador.
Estilos de 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;
}
A maioria desses estilos serve para criar um círculo bonito.
Novamente, você vê o gradiente de fundo fixo que unifica as cores dinâmicas dos controles deslizantes, das faixas e dos elementos SVG associados.
Separei os estilos da interação para ajudar a isolar a técnica box-shadow
usada para o destaque ao passar o cursor:
@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;
}
}
O objetivo era criar um destaque visual animado e fácil de gerenciar para o feedback do usuário. Ao usar uma sombra de caixa, posso evitar acionar o layout com o efeito. Para isso, crio uma sombra que não é desfocada e corresponde ao formato circular do elemento de polegar. Em seguida, mudo e faço a transição do tamanho da propagação ao passar o cursor.
Se o efeito de destaque fosse tão fácil nas caixas de seleção…
Seletores entre navegadores
Descobri que precisava desses seletores -webkit- e -moz- para alcançar consistência entre navegadores:
input[type="range"] {
&::-webkit-slider-runnable-track {}
&::-moz-range-track {}
&::-webkit-slider-thumb {}
&::-moz-range-thumb {}
}
Caixa de seleção personalizada
Considerando o elemento de entrada HTML a seguir, vou mostrar como personalizei a aparência dele:
<input type="checkbox">
Há três partes desse elemento que precisamos personalizar:
Elemento de caixa de seleção
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;
}
Os estilos transform-style e position preparam o pseudoelemento que vamos introduzir mais tarde
para estilizar o destaque. Caso contrário, são apenas
algumas pequenas opiniões minhas sobre estilo. Gosto que o cursor seja um ponteiro, gosto de
deslocamentos de contorno, as caixas de seleção padrão são muito pequenas e, se accent-color for
compatível, traga essas
caixas de seleção para o esquema de cores da marca.
Rótulos de caixas de seleção
É importante fornecer rótulos para caixas de seleção por dois motivos. A primeira é representar para que o valor da caixa de seleção é usado, para responder "ativado ou desativado para quê?" O segundo é para UX, os usuários da Web se acostumaram a interagir com caixas de seleção usando os rótulos associados.
<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>
No rótulo, coloque um atributo for que aponte para uma caixa de seleção por ID: <label for="text-notifications">. Na caixa de seleção, duplique o nome e o ID para
garantir que ela seja encontrada com várias ferramentas e tecnologias, como um mouse ou leitor de tela:
<input type="checkbox" id="text-notifications" name="text-notifications">.
:hover, :active e outros recursos são oferecidos sem custo financeiro com a conexão, aumentando as
formas de interação com o formulário.
Destaque da caixa de seleção
Quero manter minhas interfaces consistentes, e o elemento deslizante tem um destaque de miniatura
legal que eu gostaria de usar com a caixa de seleção. A miniatura conseguiu usar box-shadow e a propriedade spread para aumentar e diminuir uma sombra. No entanto, esse efeito não funciona aqui porque as caixas de seleção são, e devem
ser, quadradas.
Consegui o mesmo efeito visual com um pseudoelemento e uma quantidade infeliz de CSS complicado:
@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;
}
Criar um pseudoelemento de círculo é um trabalho simples, mas colocá-lo atrás do elemento ao qual ele está anexado foi mais difícil. Veja o antes e o depois da correção:
É uma microinteração, mas é importante para mim manter a consistência
visual. A técnica de dimensionamento de animação é a mesma que usamos em
outros lugares. Definimos uma propriedade personalizada com um novo valor e deixamos que o CSS faça a transição
com base nas preferências de movimento. O recurso principal aqui é translateZ(-1px). O
pai criou um espaço 3D, e esse filho de pseudoelemento o acessou
colocando-se um pouco para trás no espaço Z.
Acessibilidade
O vídeo do YouTube faz uma ótima demonstração das interações de mouse, teclado e leitor de tela para esse componente de configurações. Vou destacar alguns detalhes aqui.
Opções de elementos HTML
<form>
<header>
<fieldset>
<picture>
<label>
<input>
Cada um deles contém dicas para a ferramenta de navegação do usuário. Alguns elementos fornecem dicas de interação, outros conectam a interatividade e outros ajudam a moldar a árvore de acessibilidade que um leitor de tela navega.
Atributos HTML
Podemos ocultar elementos que não são necessários para leitores de tela, neste caso, o ícone ao lado do controle deslizante:
<picture aria-hidden="true">
O vídeo acima demonstra o fluxo do leitor de tela no Mac OS. Observe como o foco de entrada passa diretamente de um controle deslizante para o outro. Isso ocorre porque ocultamos o ícone que pode ter sido uma parada no caminho para o próximo controle deslizante. Sem esse atributo, o usuário precisaria parar, ouvir e passar pela imagem que talvez não consiga ver.
O SVG é um monte de matemática. Vamos adicionar um elemento <title> para um título de passagem do mouse
e um comentário legível por humanos sobre o que a matemática está criando:
<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>
Além disso, usamos HTML suficiente claramente marcado para que o formulário seja testado muito bem com mouse, teclado, controles de videogame e leitores de tela.
JavaScript
Já abordamos como a cor de preenchimento da faixa estava sendo gerenciada pelo JavaScript.
Agora, vamos analisar o JavaScript relacionado a <form>:
const form = document.querySelector('form');
form.addEventListener('input', event => {
const formData = Object.fromEntries(new FormData(form));
console.table(formData);
})
Sempre que o formulário é interagido e alterado, o console registra o formulário como um objeto em uma tabela para facilitar a revisão antes de enviar para um servidor.

Conclusão
Agora que você sabe como eu fiz, como você faria?! Isso torna a arquitetura de componentes bem divertida. Quem vai criar a primeira versão com slots no framework favorito? 🙂
Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie uma demonstração, envie um tweet com os links, e eu vou adicionar à seção Remixes da comunidade abaixo.
Remixes da comunidade
- @tomayac com o estilo dele em relação à área de passagem do cursor para os rótulos das caixas de seleção. Esta versão não tem lacuna de passagem entre elementos: demonstração e fonte.