Uma visão geral básica de como criar um componente de configurações de controles deslizantes e caixas de seleção.
Neste post, 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 dispositivos e funcione em navegadores. Teste a demonstração.
Se você prefere vídeo ou quer uma prévia da interface/experiência do que estamos criando, confira um tutorial mais curto no YouTube:
Visão geral
Dividi 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 GUI Challenge a usar apenas CSS Grid. Confira cada grade destacada com as Chrome DevTools para grade:
Apenas para a lacuna
O layout mais comum:
foo {
display: grid;
gap: var(--something);
}
Chamo esse layout de "apenas para lacuna" porque ele usa apenas a grade para adicionar lacunas entre os blocos.
Cinco layouts usam essa estratégia, confira todos eles:
O elemento fieldset
, que contém cada grupo de entrada (.fieldset-item
), usa gap: 1px
para
criar as bordas finas entre os elementos. Não há 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 natural de grade
O layout mais complexo acabou sendo o macro layout, o sistema de layout lógico
entre <main>
e <form>
.
Como centralizar o conteúdo de agrupamento
O Flexbox e a grade oferecem recursos para align-items
ou
align-content
. Além disso, ao lidar com elementos de união, 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 usa a abreviatura de
alinhamento place-content: center
para
que os filhos fiquem centralizados vertical e horizontalmente nos layouts de uma e duas colunas.
Assista no vídeo acima como o "conteúdo" permanece centralizado, mesmo que a formatação tenha ocorrido.
Repetir o ajuste automático mínimo/máximo
O <form>
usa um layout de grade adaptável para cada seção.
Esse layout muda de uma para 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) do que column-gap
(--space-xxl)
para colocar esse toque personalizado no layout responsivo. Quando as colunas são empilhadas, queremos uma grande lacuna, mas não tão grande quanto se estivéssemos 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
de layout sobre isso, chamando-a de
RAM.
Há três adições especiais no nosso layout, se você comparar com o de Una:
- Transmitimos uma função
min()
extra. - Especificamos
align-items: flex-start
. - Há um estilo
max-width: 89vw
.
A função min()
extra é bem descrita por Evan Minto no blog dele na
postagem Grade CSS responsiva intrínseca com minmax() e
min().
Recomendo que você leia. A correção de alinhamento flex-start
serve para
remover o efeito de alongamento padrão, para que os filhos desse layout não
precisem ter alturas iguais, mas possam ter alturas naturais e intrínsecas. O
vídeo do YouTube tem um resumo rápido dessa adição de alinhamento.
max-width: 89vw
merece uma pequena explicação nesta postagem.
Vamos mostrar o layout com e sem o estilo aplicado:
O que está acontecendo? Quando max-width
é especificado, ele fornece contexto,
tamanho explícito ou tamanho
definido para o auto-fit
algoritmo de layout para saber quantas
repetições ele pode ajustar no espaço. Embora pareça óbvio que o
espaço é de "largura total", de acordo com a especificação da grade CSS, é necessário
fornecer um tamanho definido ou um tamanho máximo. Informei um tamanho máximo.
Por que 89vw
? Porque "funcionou" para meu layout.
Eu e algumas outras pessoas do Chrome estamos investigando por que um valor mais razoável,
como 100vw
, não é suficiente e se isso é 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 desses fluxos é muito bom com a grade, o CSS @nest e a sintaxe de nível 5
de @media. Confira um exemplo, o conjunto de estilos de layout <main>
completo.
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). Mas, à medida que mais espaço da viewport fica disponível, ele se espalha aumentando o padding. O CSS de 2021 está ótimo!
Lembra do layout anterior, "just for gap"? Confira uma versão mais completa de como elas ficam 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 e 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, vou invertê-los
e as cores claras e escuras não serão significativas.
Eu as inverto em uma consulta de mídia de preferência, desta forma:
: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 entrarmos nos detalhes da sintaxe de cores. Mas, como me adiantei um pouco, vamos voltar um pouco.
LCH?
Sem se aprofundar muito na teoria das cores, o LCH é uma sintaxe orientada para humanos, que atende à forma como percebemos a cor, não como medimos a cor 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 nos concentrar na sintaxe e nos valores que estou invertendo para deixar claro e escuro. Vamos analisar uma superfície e uma cor 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)
é traduzido para a luminosidade 10%
, 0 croma e 0 matiz: um
cinza sem cor muito escuro. 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 do lch()
aqui é que a leveza é orientada para humanos, e podemos nos sentir
bem com uma mudança %
para ela, que será perceptiva e consistentemente
diferente.%
hsl()
, por exemplo, não é tão
confiável.
Há mais para
aprender sobre
espaços de cores e lch()
, se você tiver interesse. Está chegando!
No momento, o CSS não pode acessar essas cores. Vamos 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 vivas que a tela pode mostrar. Nossos sites ficam desbotados porque o hardware do monitor evoluiu mais rápido do que as especificações do CSS e as implementações do navegador.
Lea Verou
Controles de formulário adaptáveis com esquema de cores
Muitos navegadores enviam controles de tema escuro, atualmente o Safari e o Chromium, mas você precisa especificar em CSS ou HTML que seu design os usa.
O exemplo acima demonstra o efeito da propriedade no painel de estilos das Ferramentas do desenvolvedor. A demonstração usa a tag HTML, que, na minha opinião, é geralmente um local melhor:
<meta name="color-scheme" content="dark light">
Saiba mais sobre isso neste artigo
color-scheme
de Thomas
Steiner. Há muito mais a ganhar
do que entradas de caixas de seleção escuras.
CSS accent-color
Houve atividade
recente em torno de
accent-color
em elementos de formulário, sendo um único estilo CSS que pode mudar a
cor de matiz usada no elemento de entrada do navegador. Leia mais sobre isso neste link
do GitHub. Incluí isso nos meus
estilos para este componente. Como os navegadores oferecem suporte a isso, minhas caixas de seleção vão
estar mais no tema com as cores rosa e roxo.
input[type="checkbox"] {
accent-color: var(--brand);
}
Destaques de cor com gradientes fixos e foco dentro
As cores se destacam mais quando são usadas com moderação, e uma das maneiras que eu gosto de fazer 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 do contexto.
- Fornecer feedback da IU sobre o "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 é muito interessante:
.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 foco:
- O plano de fundo
.fieldset-item
recebe uma cor de superfície de contraste mais alto. - O
svg
aninhado é preenchido com branco para maior contraste. - O
clip-path
<picture>
aninhado se expande para um círculo completo, e o plano de fundo é preenchido com o gradiente fixo brilhante.
Período personalizado
Com o seguinte elemento de entrada HTML, vou mostrar como personalizar a aparência dele:
<input type="range">
Há três partes desse elemento que precisamos personalizar:
Estilos de elemento 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 esperamos que rotulá-las claramente ajude. A maioria dos outros estilos são 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 dura 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, aguardando a transparência para ser revelada.
Estilo de preenchimento da faixa
Meu design precisa de JavaScript para manter o estilo de preenchimento. Há estratégias exclusivas do CSS, mas elas exigem que o elemento de miniatura tenha a mesma altura que a 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 é uma boa melhoria visual. O controle deslizante funciona muito bem sem
JavaScript. A propriedade --track-fill
não é necessária, mas 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 este ótimo
post no
CSS-Tricks (em inglês) do Ana
Tudor, que demonstra uma solução exclusiva de CSS para
preenchimento de faixa. Também achei esse
elemento range
muito inspirador.
Estilos de polegar
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 é para fazer um círculo bonito.
Você também vai encontrar o gradiente de plano de fundo fixo que unifica as
cores dinâmicas dos miniaturas, faixas e elementos SVG associados.
Separei os estilos da interação para ajudar a isolar a técnica box-shadow
usada para o destaque do 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 fácil de gerenciar para o feedback do usuário. Ao usar uma caixa de sombra, posso evitar o gatilho do layout com o efeito. Para fazer isso, crie uma sombra que não seja desfocada e corresponda à forma circular do elemento de miniatura. Em seguida, mudo e faço a transição do tamanho da abertura 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 a 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
Com o seguinte elemento de entrada HTML, vou mostrar como personalizar 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
se preparam para o pseudoelemento que vamos apresentar mais adiante
para estilizar o destaque. Caso contrário, são
pequenas coisas de estilo opinativas. Gosto que o cursor seja um ponteiro. Gosto
de contornos de desfoque. As caixas de seleção padrão são muito pequenas. Se accent-color
for
compatível, coloque essas
caixas de seleção no esquema de cores da marca.
Rótulos de caixas de seleção
É importante fornecer rótulos para as 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ê?". Em segundo lugar, para UX, os usuários da Web se acostumaram a interagir com as 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 ele seja encontrado com ferramentas e tecnologias diferentes, 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 a consistência das minhas interfaces, e o elemento de controle deslizante tem um bom
destaque de miniatura que gostaria de usar com a caixa de seleção. A miniatura
poderia usar box-shadow
e a propriedade spread
para dimensionar uma sombra para cima e
para baixo. No entanto, esse efeito não funciona aqui porque nossas caixas de seleção são, e devem
ser, quadradas.
Consegui alcançar o mesmo efeito visual com um pseudoelemento e uma quantidade infeliz de CSS complicados:
@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. Confira como era antes e depois da correção:
É definitivamente 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 permitimos que o CSS faça a transição dela
com base nas preferências de movimento. O recurso principal aqui é translateZ(-1px)
. O
elemento pai criou um espaço 3D, e esse pseudoelemento filho o usou
colocando-se um pouco para trás no espaço z.
Acessibilidade
O vídeo do YouTube é uma ótima demonstração das interações do mouse, do teclado e do leitor de tela para esse componente de configurações. Vou destacar alguns detalhes aqui.
Opções de elemento HTML
<form>
<header>
<fieldset>
<picture>
<label>
<input>
Cada um deles contém dicas e sugestões 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 muda diretamente de um controle deslizante para o próximo. 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
livre 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 marcado claramente o suficiente para que o formulário seja testado bem em mouses, teclados, controles de videogame e leitores de tela.
JavaScript
Já expliquei como a cor de preenchimento da faixa era gerenciada pelo JavaScript.
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);
})
Toda vez que o formulário é alterado, o console registra o formulário como um objeto em uma tabela para facilitar a análise antes do envio a um servidor.
Conclusão
Agora que você sabe como eu fiz, como você faria? Isso cria uma arquitetura de componentes divertida. Quem vai criar a primeira versão com slots na framework favorita? 🙂
Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie uma demonstração, envie links para mim e vou adicionar à seção Remixes da comunidade abaixo.