Como criar um componente Configurações

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.

Demo

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:

  1. Layouts
  2. Cor
  3. Entrada de intervalo personalizado
  4. Entrada de caixa de seleção personalizada
  5. Considerações sobre acessibilidade
  6. 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:

Contornos coloridos e sobreposições de espaçamento de lacunas que ajudam a mostrar todas as caixas que compõem o layout de configurações

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:

Layouts de grade vertical com contornos e preenchimento de lacunas

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.

Lacuna preenchida
.grid {
  display: grid;
  gap: 1px;
  background: var(--bg-surface-1);

  & > .fieldset-item {
    background: var(--bg-surface-2);
  }
}
Truque da borda
.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.

Captura de tela da página pod.link/csspodcast, com o episódio &quot;Color 2: Perception&quot; aberto
Saiba mais sobre a cor perceptual (e muito mais) no CSS Podcast

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.

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);
}

Captura de tela do Chromium no Linux com caixas de seleção rosas

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:

  1. O plano de fundo .fieldset-item recebe uma cor de superfície de contraste mais alto.
  2. O svg aninhado é preenchido com branco para maior contraste.
  3. 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:

  1. Elemento / contêiner de intervalo
  2. Acompanhamento
  3. Polegar

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:

  1. Elemento de caixa de seleção
  2. Rótulos associados
  3. Efeito de destaque

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.

entrada
<input
  type="checkbox"
  id="text-notifications"
  name="text-notifications"
>
o rótulo.
<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.

Uma captura de tela dos resultados de console.table(), em que os dados do formulário são mostrados em uma tabela

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.

Remixes da comunidade

  • @tomayac com o estilo deles em relação à área de passar o cursor para os rótulos de caixas de seleção. Esta versão não tem intervalo de passagem do cursor entre elementos: demo e source.