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 navegadores. Teste a demonstração.
Se você preferir um vídeo ou quiser uma prévia da interface/UX do que estamos criando, aqui está 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 personalizada da caixa de seleção
- Considerações sobre acessibilidade
- JavaScript
Layouts
Esta é a primeira demonstração do desafio de GUI do tipo todos os CSS Grid. Veja cada grade destacada com o Chrome DevTools para grade:
Apenas para lacunas
O layout mais comum:
foo {
display: grid;
gap: var(--something);
}
Eu chamo esse layout de "apenas para lacunas", 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
), está usando gap: 1px
para
criar as bordas entre os elementos. Nenhuma solução de borda 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 layout de macro, o sistema de layout
lógico entre <main>
e <form>
.
Centralizar o conteúdo do wrapper
O flexbox e a grade oferecem recursos para align-items
ou
align-content
e, ao lidar com elementos de agrupamento, os alinhamentos de layout
content
distribuem 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 nos layouts de uma e duas colunas.
Veja no vídeo acima como o "conteúdo" permanece centralizado, mesmo que o wrapper tenha ocorrido.
Repetir minmax de ajuste automático
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) de column-gap
(--space-xxl)
para colocar esse toque personalizado no layout responsivo. Quando as colunas empilham, queremos
uma grande lacuna, mas não tão grande quanto em uma tela widescreen.
A propriedade grid-template-columns
usa três funções CSS: repeat()
, minmax()
e min()
. Una Kravets tem uma ótima postagem do blog
sobre layout (link em inglês) com o nome
RAM.
Há três adições especiais no nosso layout, se compararmos com o de Una:
- Transmitimos 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() e
min().
Recomendo que leia esse texto. A correção de alinhamento flex-start
remove o
efeito de alongamento padrão para que os filhos desse layout não
precisem ter a mesma altura, mas sim alturas intrínsecas naturais. O vídeo do YouTube detalha essa adição de alinhamento.
Confira detalhes sobre max-width: 89vw
nesta postagem.
Vou mostrar 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 o algoritmo de layout
auto-fit
para saber quantas
repetições ele pode encaixar no espaço. Embora pareça óbvio que o espaço é "largura total", de acordo com a especificação da grade CSS, é necessário fornecer um tamanho ou tamanho máximo definido. Forneci um tamanho máximo.
Então, por que 89vw
? Porque "funcionava" no 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 é realmente 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 grade, CSS @nest e sintaxe de nível 5 de @media. Veja um exemplo: o conjunto de estilos 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, moderadamente preenchido por padrão (como em dispositivos móveis). No entanto, à medida que mais espaço da janela de visualização se torna disponível, ele se espalha pelo aumento do padding. O CSS de 2021 está ótimo!
Lembra do layout anterior, "apenas para lacunas"? Confira uma versão mais completa de como eles ficam nesse componente:
header {
display: grid;
gap: var(--space-xxs);
}
section {
display: grid;
gap: var(--space-md);
}
Cor
O uso controlado da cor ajudou esse design a se destacar como expressivo, mas mínimo. 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);
}
Nomeo as cores de superfície e texto com números, em vez de nomes como
surface-dark
e surface-darker
, porque, em uma consulta de mídia, vou inverter
as cores, e claro e escuro não terão significado.
Eu as inverto em uma consulta de mídia de preferência assim:
: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 noção rápida do panorama geral e da estratégia antes de nos aprofundarmos nos detalhes de sintaxe de cores. Mas, como me adiantou um pouco, vou voltar um pouco.
ALCO?
Sem se aprofundar muito no campo da teoria das cores, a LCH é uma sintaxe orientada por humanos, que atende ao modo como percebemos a cor, não a como medimos as cores com matemática (como 255). Isso proporciona uma vantagem exclusiva, já que os humanos podem programar com mais facilidade e outros usuários estarão em sintonia com esses ajustes.
![Uma captura de tela da página da Web pod.link/csspodcast com a opção "Cor 2: percepção" aberta](https://web.dev/static/articles/building/a-settings-component/image/a-screenshot-podlinkcs-a40f130bf81b8.png?authuser=5&hl=pt)
Por hoje, nesta demonstração, focaremos na sintaxe e nos valores que estou mudando 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)
se traduz em luminosidade 10%
, 0 chroma e 0 hue: um cinza incolor muito escuro. Em seguida, na consulta de mídia para o modo claro, o brilho
é alterado para 90%
com --surface1: lch(90 0 0);
. Essa é a essência da estratégia. Comece apenas mudando a claridade entre os dois temas, mantendo as
proporções de contraste que o design exige ou o que pode manter a acessibilidade.
O bônus com lch()
aqui é que a claridade é orientada pelas pessoas e podemos nos sentir bem
com uma mudança de %
nela, que será perceptiva e consistentemente
que %
diferente. hsl()
, por exemplo, não é tão confiável.
Há mais para
saber sobre
espaços de cores e lch()
, se você tiver interesse. Está chegando!
No momento, o CSS não pode acessar essas cores. Vou repetir: não temos acesso a um terço das cores na maioria dos monitores modernos. E essas não são qualquer cor, mas as cores mais vívidas que a tela pode exibir. Nossos sites estão desbotados porque o hardware de monitor evoluiu mais rápido do que as especificações de CSS e as implementações em navegadores.
Lea Verou
Controles adaptáveis de formulários com esquema de cores
Muitos navegadores incluem controles de tema escuro, atualmente o Safari e o Chromium, mas você precisa especificar em CSS ou HTML que seu design os use.
A imagem acima demonstra o efeito da propriedade do 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 sobre color-scheme
de Thomas
Steiner (em inglês). Há muito mais a ganhar do que
entradas de caixas de seleção escuras.
CSS accent-color
Houve uma atividade
recente em
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 (em inglês). Eu o incluí nos meus
estilos desse componente. À medida que os navegadores forem compatíveis, minhas caixas de seleção estarão
mais sobre o tema, com os destaques de cor rosa e roxo.
input[type="checkbox"] {
accent-color: var(--brand);
}
As cores se destacam com gradientes fixos e dentro do foco
A cor tem mais destaque quando é usada com moderação, e uma das maneiras que gosto 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 porque:
- Destacar o contexto.
- Fornecer feedback da interface sobre o "quão cheio" o valor está no intervalo.
- Enviar feedback da interface informando que um campo está aceitando entrada.
Para fornecer feedback durante a interação com um elemento, o CSS está usando 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 está em foco:
- O plano de fundo
.fieldset-item
recebe uma cor de superfície de maior contraste. - O
svg
aninhado é preenchido em branco para maior contraste. - O
<picture>
clip-path
aninhado se expande para um círculo completo e o segundo plano é 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">
Precisamos personalizar três partes desse elemento:
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á-los claramente ajude. Os demais estilos são, na maioria, redefinidos com o objetivo de 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 segredo é "revelar" a cor vibrante de preenchimento. Isso é feito com o gradiente de parada forçada na parte superior. O gradiente é transparente até a porcentagem de preenchimento e, depois, usa a cor de 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.
Estilo de preenchimento da faixa
Meu design exige JavaScript para manter o estilo de preenchimento. Existem estratégias exclusivas de CSS, mas elas exigem que o elemento de polegar 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 contribui para um bom upgrade visual. O controle deslizante funciona muito bem sem
JavaScript. A propriedade --track-fill
não é necessá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 enquanto observa as alterações do usuário, sincronizando a propriedade com o valor.
Confira uma ótima
postagem sobre
CSS-Tricks de Ana
Tudor, que demonstra uma solução exclusiva do CSS para
preenchimento de faixas. Também achei este
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 serve para fazer um bom círculo.
Você vai notar novamente o gradiente fixo de plano de fundo, que unifica as
cores dinâmicas das miniaturas, faixas e elementos SVG associados.
Separei os estilos da interação para ajudar a isolar a técnica box-shadow
que está sendo 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 oferecer um destaque visual animado e fácil de gerenciar para o feedback dos usuários. Ao usar uma sombra de caixa, posso evitar o acionamento do layout com esse efeito. Para isso, crio uma sombra que não esteja desfocada e corresponda à forma circular do elemento de polegar. Depois, mudo e faço a transição do tamanho da propagação ao passar o cursor.
Se só o efeito de destaque fosse tão fácil nas caixas de seleção...
Seletores de navegadores diferentes
Achei que precisava desses seletores -webkit-
e -moz-
para manter 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">
Precisamos personalizar três partes desse elemento:
Elemento da 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 posteriormente
para definir o estilo do destaque. Fora isso, são principalmente
coisas de estilo menores e opinativas. Gosto que o cursor seja ponteiro, gosto de
deslocamentos de contorno, caixas de seleção padrão são muito pequenas e, se accent-color
tiver
suporte, inclua essas
caixas de seleção no esquema de cores da marca.
Identificadores da caixa de seleção
É importante fornecer etiquetas nas caixas de seleção por dois motivos. A primeira é representar para que o valor da caixa de seleção é usado, responder "ligado ou desativado para quê?" Em segundo lugar, para UX, os usuários da Web se acostumaram a interagir com caixas de seleção pelos 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 pelo 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 variadas, como um mouse ou um leitor de tela:
<input type="checkbox" id="text-notifications" name="text-notifications">
.
:hover
, :active
e muito mais vêm sem custo financeiro com a conexão, aumentando as
maneiras de interagir com o formulário.
Destaque da caixa de seleção
Quero manter minhas interfaces consistentes, e o elemento deslizante tem um
bom destaque na miniatura que eu gostaria de usar com a caixa de seleção. A miniatura conseguiu
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 precisam ser quadradas.
Consegui alcançar o mesmo efeito visual com um pseudoelemento e uma infeliz quantidade 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 por trás do elemento ao qual ele está anexado foi mais difícil. Veja antes e depois da correção:
É definitivamente uma interação micro, mas é importante para mim manter a consistência
visual. A técnica de dimensionamento da animação é a mesma usada em
outros lugares. Definimos um novo valor a uma propriedade personalizada e deixamos o CSS fazer 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 pseudoelemento filho tocou nele
colocando-se levemente de volta no espaço z.
Acessibilidade
O vídeo do YouTube faz uma ótima demonstração das interações do mouse, teclado e leitor de tela para esse componente de configurações. Vou chamar alguns dos detalhes aqui.
Opções de elementos HTML
<form>
<header>
<fieldset>
<picture>
<label>
<input>
Cada um desses contém dicas para a ferramenta de navegação do usuário. Alguns elementos oferecem dicas de interação, alguns de conexão de interatividade e outros ajudam a moldar a árvore de acessibilidade que um leitor de tela percorre.
Atributos HTML
Podemos ocultar elementos que não são necessários para os 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 se move direto 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, detectar e passar a imagem, que talvez ele não consiga ver.
O SVG é um monte de cálculos. Vamos adicionar um elemento <title>
para um título que fica ao passar o mouse
e um comentário legível sobre o que o cálculo 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 com clareza suficiente para que o formato funcione muito bem com mouse, teclado, controles de videogame e leitores de tela.
JavaScript
Já expliquei como a cor de preenchimento da faixa era gerenciada no JavaScript,
então vamos conferir o JavaScript relacionado ao <form>
:
const form = document.querySelector('form');
form.addEventListener('input', event => {
const formData = Object.fromEntries(new FormData(form));
console.table(formData);
})
Sempre que houver interação e alteração com o formulário, o console o registra como um objeto em uma tabela para facilitar a revisão antes do envio para um servidor.
Conclusão
Agora que você sabe como eu fiz isso, como você iria?! Isso resulta em uma arquitetura de componentes divertida. Quem vai fazer a 1a 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 para mim e vou adicioná-los à seção Remixes da comunidade abaixo.