Como criar um componente Configurações

Uma visão geral básica de como criar um componente de configuração de controles deslizantes e caixas de seleção.

Nesta postagem, quero compartilhar ideias sobre a criação de um componente Configurações para o da Web que é responsiva, oferece suporte a várias entradas de dispositivos e funciona em navegadores da Web. Confira a demonstração.

Demonstração

Se você prefere vídeo ou quer uma prévia da interface/UX do que estamos criando, aqui está uma 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 desafio de GUI para ser all CSS Grid. Aqui está cada grade destacado com Chrome DevTools para grade:

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

Apenas para aumentar o tempo

O layout mais comum:

foo {
  display: grid;
  gap: var(--something);
}

Eu chamo esse layout de "apenas para lacuna" porque só usa a grade para adicionar lacunas entre os blocos.

Cinco layouts usam essa estratégia. Veja todos eles exibidos:

Layouts de grade vertical destacados com contornos e lacunas preenchidas

O elemento fieldset, que contém cada grupo de entrada (.fieldset-item), usa gap: 1px para criar bordas finas entre os elementos. Nenhuma solução de borda complicada.

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

  & > .fieldset-item {
    background: var(--bg-surface-2);
  }
}
Truque de 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 layout macro, o layout lógico entre <main> e <form>.

Centralizar conteúdo

O Flexbox e a grade oferecem recursos para align-items ou align-content e, ao lidar com elementos de união, o layout content os alinhamentos vão distribuir o espaço entre os filhos como um grupo.

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
}

O elemento principal está usando o alinhamento de place-content: center abreviação que os filhos sejam centralizados vertical e horizontalmente em layouts de uma e duas colunas.

Assista ao vídeo acima como a visualização do "conteúdo" permanece centralizada, mesmo que envolva ocorreu.

Repetir o ajuste automático mínimo

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) e column-gap (--space-xxl) para dar um toque personalizado ao layout responsivo. Quando as colunas são empilhadas, querem um espaço maior, mas não tão grande quanto em um widescreen.

A propriedade grid-template-columns usa três funções CSS: repeat(), minmax() e min(). Una Kravets tem um ótimo blog de layouts uma postagem sobre isso, chamando RAM.

Há três adições especiais no layout, se você comparar com o de Una:

  • Vamos transmitir uma função min() extra.
  • Especificamos align-items: flex-start.
  • Existe um estilo max-width: 89vw.

A função min() extra é bem descrita por Evan Minto no blog da série poste Grade CSS intrinsecamente responsiva com minmax() e min(). recomendo que leiam isso. A correção de alinhamento flex-start é para remova o efeito de alongamento padrão, para que os filhos desse layout não precisam ter alturas iguais, podem ter alturas naturais e intrínsecas. A O vídeo do YouTube tem um detalhamento rápido dessa adição de alinhamento.

Vamos falar um pouco 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, tamanho explícito ou definido o tamanho de auto-fit algoritmo de layoutpara saber como muitas repetições que ela pode caber no espaço. Embora pareça óbvio que os o espaço é "largura total", de acordo com a especificação da grade do CSS, um tamanho definido ou tamanho máximo deve ser fornecidas. Eu forneci um tamanho máximo.

Então, por que 89vw? Porque "Funcionou" para o meu layout. Eu e outros dois colegas do Chrome estamos investigando por que um valor mais razoável, como 100vw não é suficiente, e se isso for 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 desses fluxos muito bem com grid, CSS @nest e sintaxe de nível 5 de @media. Confira um exemplo do conjunto de estilos totalmente <main> do layout.

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 preenchida por padrão (como em dispositivos móveis). Mas conforme mais espaço da janela de visualização se torna disponível, ele se espalha aumentando o padding. O CSS de 2021 está muito bom!

Lembra-se do layout anterior, "apenas para lacuna"? Aqui está uma versão mais completa neste componente:

header {
  display: grid;
  gap: var(--space-xxs);
}

section {
  display: grid;
  gap: var(--space-md);
}

Cor

O uso controlado de cores ajudou este design a se destacar como expressivo, mas mínimas. Faço o seguinte:

: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 minha superfície e cores de texto com números, em vez de nomes como surface-dark e surface-darker porque em uma consulta de mídia, vou inverter e claro e escuro não são relevantes.

Eu os altero 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 rápida do quadro geral e da estratégia antes vamos analisar os detalhes da sintaxe de cores. Mas, como já me adiantou um pouco, Vou voltar um pouco.

LCH?

Sem nos aprofundarmos muito na teoria das cores, LCH é uma sintaxe orientada por humanos, que considera como percebemos a cor, não como medimos as cores com cálculos (como 255). Isso dá uma vantagem distinta, já que humanos podem escrevê-lo mais facilmente e outros humanos estarão sintonizados com esses ajustes.

Uma captura de tela da página da Web pod.link/csspodcast, com o episódio &quot;Color 2: Perception&quot; na tela.
Saiba mais sobre cor perceptiva e muito mais no Podcast CSS

Hoje, nesta demonstração, vamos nos concentrar na sintaxe e nos valores que estou invertendo para tornar 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) significa iluminação de 10%, chroma 0 e matiz 0: um cinza muito escuro. Depois, na consulta de mídia para o modo claro, o brilho é alterado para 90% com --surface1: lch(90 0 0);. Essa é a essência do estratégia. Comece mudando o brilho entre os dois temas, mantendo o taxas de contraste que o design exige ou o que pode manter a acessibilidade.

O bônus do lch() aqui é que a leveza é orientada pelas pessoas, e podemos sentir boa sobre uma mudança de %, que será percebida e consistente que % é diferente. hsl(), por exemplo, não é confiáveis.

mais para aprender sobre espaços de cor e lch(), se estiver interessado. Está chegando!

No momento, o CSS não pode acessar essas cores. Repetindo: não temos acesso a um terço das cores na maioria monitores. E não se trata apenas de qualquer cor, mas das cores mais vívidas pode ser exibida. Nossos sites foram desbotados porque o hardware do monitor evoluiu mais rápido do que as especificações CSS e as implementações em navegadores.

Lea Verou (em espanhol, francês, grego, inglês, italiano e português de Portugal)

Controles adaptáveis de formulários com esquema de cores

Muitos navegadores oferecem controles de tema escuro, atualmente o Safari e o Chromium, mas você você precisa especificar em CSS ou HTML que seu design os utiliza.

O exemplo acima demonstra o efeito da propriedade do painel "Estilos" de 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 color-scheme artigo de Thomas Steiner. Há muito mais a ganhar do que entradas com caixas de seleção escuras.

CSS accent-color

Houve recentes atividade relacionada accent-color nos elementos de formulário, sendo um único estilo CSS que pode mudar cor de tonalidade usada no elemento de entrada de navegadores. Leia mais sobre isso neste link GitHub (em inglês). Eu o incluí no meu deste componente. Como os navegadores são compatíveis, minhas caixas de seleção serão mais sobre o tema com os destaques das cores rosa e roxo.

input[type="checkbox"] {
  accent-color: var(--brand);
}

Uma captura de tela do Chromium no Linux de caixas de seleção rosa

Destaque de cor com gradientes fixos e foco interno

A cor se destaca quando usada com moderação, e uma das maneiras que gosto de alcançar isso acontece por meio de interações coloridas na interface.

Há muitas camadas de feedback e interação sobre a interface no vídeo acima, que ajudam a dar personalidade à interação:

  • Destaque do contexto.
  • Fornecer feedback sobre o nível de saciedade da IU o valor está no intervalo.
  • Fornecer feedback da interface de que um campo está aceitando entrada.

Para fornecer feedback quando houver interação com um elemento, o CSS usa o :focus-within pseudoclasse para alterar a aparência de vários elementos, vamos analisar .fieldset-item, isso é 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 em:

  1. O plano de fundo .fieldset-item recebe uma cor de superfície com contraste mais alto.
  2. O svg aninhado é preenchido de branco para aumentar o contraste.
  3. A clip-path <picture> aninhada se expande para um círculo completo, e a é preenchido com o gradiente fixo brilhante.

Período personalizado

Considerando o elemento de entrada HTML a seguir, vou mostrar como personalizei o aparência:

<input type="range">

Há três partes nesse elemento que precisamos personalizar:

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

Estilos dos 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 e rotulá-los claramente ajuda. O resto dos estilos são principalmente estilos redefinidos, para fornecem 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 vibrante de preenchimento. Para isso, usamos gradiente de ponto fixo na parte superior. O gradiente é transparente até a porcentagem de preenchimento e, após que usa a cor de superfície da faixa não preenchida. Atrás dessa superfície não preenchida, cor largura total, aguardando a transparência revelá-la.

Estilo de preenchimento da faixa

Meu design requer JavaScript para manter o estilo de preenchimento. são apenas estratégias de CSS, mas exigem que o elemento thumb tenha a mesma altura como a pista, 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 é uma boa atualização visual. O controle deslizante funciona muito bem JavaScript, a propriedade --track-fill não é necessária, ele simplesmente não terá uma o estilo de preenchimento caso ele não esteja presente. Se o JavaScript estiver disponível, preencha o e observar quaisquer alterações do usuário, sincronizando a propriedade personalizada com o valor.

Esta é uma ótima postagem em CSS-Tricks por Ana Tudor, que demonstra uma solução exclusiva de CSS para preenchimento de faixa. Também achei O 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 fazer um círculo legal. Note de novo o gradiente fixo de fundo ali, que unifica a cores dinâmicas dos polegares, faixas e elementos SVG associados. Separei os estilos da interação para ajudar a isolar o box-shadow. usada para destacar o recurso 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;
  }
}

A meta era um destaque visual animado e fácil de gerenciar para o feedback dos usuários. Usando a sombra de uma caixa, posso evitar acionar layout com o efeito. Eu faço isso criando uma sombra que não seja desfocada e corresponda à forma circular thumb. Depois, mudo e faço a transição do tamanho da propagação ao passar o cursor.

Se apenas o efeito de destaque fosse tão fácil nas caixas de seleção...

Seletores entre navegadores

Achei que precisava desses seletores -webkit- e -moz- para alcançar vários navegadores consistência:

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 o aparência:

<input type="checkbox">

Há três partes nesse elemento que precisamos personalizar:

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

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 preparam o pseudoelemento que vamos apresentar mais tarde. para estilizar o destaque. Caso contrário, é principalmente pequenas coisas de estilo opinativo. Gosto que o cursor seja o ponteiro, deslocamentos de contorno, as caixas de seleção padrão são muito pequenas, e se accent-color for compatível, traga esses no esquema de cores da marca.

Marcadores de caixa de seleção

É importante fornecer rótulos para as caixas de seleção por dois motivos. A primeira é representam 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 por meio de seus 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 marcador, 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 verifique se ele pode ser encontrado com várias ferramentas e tecnologias, como mouse ou leitor de tela: <input type="checkbox" id="text-notifications" name="text-notifications">: :hover, :active e muitos outros vêm sem custo financeiro com a conexão, aumentando o formas 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 da miniatura que eu quero usar com a caixa de seleção. A miniatura era poder usar a box-shadow e a propriedade spread para dimensionar uma sombra e para baixo. Entretanto, esse efeito não funciona aqui porque nossas caixas de seleção são, e devem ser, quadrado.

Eu consegui alcançar o mesmo efeito visual com um pseudoelemento 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 circular é um trabalho simples, mas colocá-lo atrás do elemento ao qual ele está anexado era mais difícil. Aqui está antes e depois da correção:

É com certeza uma micro interação, mas é importante para mim manter o visual consistência. 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 o CSS fazer a transição dele com base nas preferências de movimento. O principal recurso aqui é o translateZ(-1px). A pai criou um espaço 3D, e esse pseudoelemento filho tocou nele colocando-se ligeiramente de volta no espaço-z.

Acessibilidade

O vídeo do YouTube faz uma ótima demonstração do mouse, teclado e interações do leitor de tela para esse componente de configurações. Vou destacar alguns dos 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, alguns conectam interatividade e outras ajudam a moldar o árvore de acessibilidade pela qual um leitor de tela navega.

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 a entrada o foco se move diretamente de um controle deslizante para o próximo. Isso acontece porque ocultamos no ícone que pode ter sido uma parada no caminho para o próximo controle deslizante. Sem isso o usuário precisaria parar, escutar e ir além da imagem, que eles podem não conseguir ver.

O SVG é um monte de cálculos, vamos adicionar um elemento <title> para passar o cursor livremente título e um comentário legível por humanos 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, que o formulário testa muito bem em mouses, teclados, controles de videogame e leitores de tela.

JavaScript

Já expliquei como a cor de preenchimento da faixa era gerenciada no JavaScript. Então, vamos analisar o JavaScript relacionado ao <form>:

const form = document.querySelector('form');

form.addEventListener('input', event => {
  const formData = Object.fromEntries(new FormData(form));
  console.table(formData);
})

Toda vez que alguém interage e altera o formulário, o console registra o formulário como um objeto em uma tabela para facilitar a análise antes do envio para um servidor.

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

Conclusão

Agora que você sabe como eu fiz isso, como faria?! Com isso, vamos nos divertir da arquitetura de componentes. Quem vai criar a primeira versão com slots nos 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 adicione os links acesse a seção Remixes da comunidade abaixo!

Remixes da comunidade

  • @tomayac com o estilo relacionado à área de passar o cursor para os marcadores das caixas de seleção! Esta versão não tem uma lacuna ao passar o cursor elementos: demo e fonte.