Como criar um componente de botão

Uma visão geral básica de como criar componentes <button> responsivos, adaptáveis a cores e acessíveis.

Neste post, quero compartilhar minhas ideias sobre como criar um elemento <button> adaptável a cores, responsivo e acessível. Teste a demonstração e confira a fonte.

A interação com os botões é feita pelo teclado e pelo mouse nos temas claro e escuro.

Se preferir vídeos, confira a versão desta postagem no YouTube:

Visão geral

Compatibilidade com navegadores

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 1.
  • Safari: 1.

Origem

O elemento <button> é criado para a interação do usuário. O evento click é acionado pelo teclado, mouse, toque, voz e muito mais, com regras inteligentes sobre o cronômetro. Ele também vem com alguns estilos padrão em cada navegador, para que você possa usá-los diretamente sem personalização. Use color-scheme para ativar os botões claros e escuros fornecidos pelo navegador.

Há também diferentes tipos de botões, cada um mostrado na incorporação do Codepen anterior. Um <button> sem um tipo será adaptado para estar em um <form>, mudando para o tipo de envio.

<!-- buttons -->
<button></button>
<button type="submit"></button>
<button type="button"></button>
<button type="reset"></button>

<!-- button state -->
<button disabled></button>

<!-- input buttons -->
<input type="button" />
<input type="file">

No Desafio de GUI deste mês, cada botão vai receber estilos para ajudar a diferenciar visualmente a intenção dele. Os botões de redefinição têm cores de aviso, já que são destrutivos, e os botões de envio têm texto de destaque em azul para parecerem um pouco mais destacados do que os botões normais.

Prévia do conjunto final de todos os tipos de botões, mostrado em um formulário e não em um formulário, com boas adições para botões de ícone e botões personalizados.
Prévia do conjunto final de todos os tipos de botões, mostrados em um formulário e não em um formulário, com boas adições para botões de ícone e botões personalizados

Os botões também têm pseudoclasses para que o CSS use para estilizar. Essas classes fornecem ganchos CSS para personalizar a sensação do botão: :hover para quando um mouse está sobre o botão, :active para quando um mouse ou teclado está pressionando e :focus ou :focus-visible para ajudar no estilo de tecnologia adaptativa.

button:hover {}
button:active {}
button:focus {}
button:focus-visible {}
Prévia do conjunto final de todos os tipos de botões no tema escuro.
Visualização do conjunto final de todos os tipos de botão no tema escuro

Marcação

Além dos tipos de botão fornecidos pela especificação HTML, adicionei um botão com um ícone e um botão com uma classe personalizada btn-custom.

<button>Default</button>
<input type="button" value="<input>"/>
<button>
  <svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
    <path d="..." />
  </svg>
  Icon
</button>
<button type="submit">Submit</button>
<button type="button">Type Button</button>
<button type="reset">Reset</button>
<button disabled>Disabled</button>
<button class="btn-custom">Custom</button>
<input type="file">

Em seguida, para testes, cada botão é colocado dentro de um formulário. Dessa forma, posso garantir que os estilos sejam atualizados adequadamente para o botão padrão, que se comporta como um botão de envio. Também mudei a estratégia de ícones, de SVG inline para SVG mascarado, para garantir que ambos funcionem igualmente bem.

<form>
  <button>Default</button>
  <input type="button" value="<input>"/>
  <button>Icon <span data-icon="cloud"></span></button>
  <button type="submit">Submit</button>
  <button type="button">Type Button</button>
  <button type="reset">Reset</button>
  <button disabled>Disabled</button>
  <button class="btn-custom btn-large" type="button">Large Custom</button>
  <input type="file">
</form>

A matriz de combinações está bastante confusa neste momento. Entre os tipos de botões, as pseudoclasses e o uso ou não de um formulário, há mais de 20 combinações de botões. É bom que o CSS possa nos ajudar a articular cada um deles com clareza.

Acessibilidade

Os elementos de botão são naturalmente acessíveis, mas há algumas melhorias comuns.

Passar o cursor e focar juntos

Gosto de agrupar :hover e :focus com o pseudoseletor funcional :is(). Isso ajuda a garantir que minhas interfaces sempre considerem os estilos de teclado e tecnologia adaptativa.

button:is(:hover, :focus) {
  
}
Confira uma demonstração.

Anel de foco interativo

Gosto de animar o anel de foco para usuários de teclado e tecnologias adaptativas. Para fazer isso, animação o contorno para longe do botão em 5 px, mas apenas quando o botão não está ativo. Isso cria um efeito que faz com que o anel de foco encolha de volta para o tamanho do botão quando pressionado.

:where(button, input):where(:not(:active)):focus-visible {
  outline-offset: 5px;
}

Como garantir o contraste de cor aprovado

Há pelo menos quatro combinações de cores diferentes entre claro e escuro que precisam considerar o contraste de cores: botão, botão de envio, botão de redefinição e botão desativado. O VisBug é usado aqui para inspecionar e mostrar todas as pontuações de uma vez:

Como ocultar ícones de pessoas que não podem ver

Ao criar um botão de ícone, o ícone precisa oferecer suporte visual ao texto do botão. Isso também significa que o ícone não é valioso para alguém com perda de visão. Felizmente, o navegador oferece uma maneira de ocultar itens da tecnologia de leitor de tela para que pessoas com perda de visão não sejam incomodadas com imagens decorativas de botões:

<button>
  <svg … aria-hidden="true">...</svg>
  Icon Button
</button>
Chrome DevTools mostrando a árvore de acessibilidade do botão. A árvore ignora a imagem do botão porque o atributo aria-hidden está definido como &quot;true&quot;.
As ferramentas do Chrome DevTools mostrando a árvore de acessibilidade do botão. A árvore ignora a imagem do botão porque o atributo aria-hidden está definido como "true"

Estilos

Nesta próxima seção, primeiro estabeleço um sistema de propriedade personalizado para gerenciar os estilos adaptáveis do botão. Com essas propriedades personalizadas, posso começar a selecionar elementos e personalizar a aparência deles.

Uma estratégia de propriedade personalizada adaptativa

A estratégia de propriedade personalizada usada neste Desafio de GUI é muito semelhante à usada na criação de um esquema de cores. Para um sistema de cores claras e escuras adaptável, uma propriedade personalizada para cada tema é definida e nomeada de acordo. Em seguida, uma única propriedade personalizada é usada para armazenar o valor atual do tema e é atribuída a uma propriedade CSS. Mais tarde, a única propriedade personalizada pode ser atualizada para um valor diferente e, em seguida, o estilo do botão.

button {
  --_bg-light: white;
  --_bg-dark: black;
  --_bg: var(--_bg-light);

  background-color: var(--_bg);
}

@media (prefers-color-scheme: dark) {
  button {
    --_bg: var(--_bg-dark);
  }
}

O que eu gosto é que os temas claros e escuros são declarativos e claros. A indireção e a abstração são transferidas para a propriedade personalizada --_bg, que agora é a única propriedade "reativa". --_bg-light e --_bg-dark são estáticos. Também fica claro que o tema claro é o padrão e o escuro é aplicado apenas condicionalmente.

Como preparar a consistência do design

O seletor compartilhado

O seletor a seguir é usado para segmentar todos os tipos de botões e pode ser um pouco confuso no início. :where() é usado para que a personalização do botão não exija especificidade. Os botões geralmente são adaptados para cenários alternativos, e o seletor :where() garante que a tarefa seja fácil. Dentro de :where(), cada tipo de botão é selecionado, incluindo o ::file-selector-button, que não pode ser usado dentro de :is() ou :where().

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"],
  input[type="file"]
),
:where(input[type="file"])::file-selector-button {
  
}

Todas as propriedades personalizadas vão ser definidas dentro desse seletor. É hora de analisar todas as propriedades personalizadas. Há várias propriedades personalizadas usadas neste botão. Vou descrever cada grupo conforme avançamos e compartilhar os contextos de movimento escuro e reduzido no final da seção.

Cor de destaque do botão

Botões de envio e ícones são ótimos para dar um toque de cor:

--_accent-light: hsl(210 100% 40%);
--_accent-dark: hsl(210 50% 70%);
--_accent: var(--_accent-light);

Cor do texto do botão

As cores do texto do botão não são brancas ou pretas, são versões mais claras ou mais escuras de --_accent usando hsl() e respeitando a matiz 210:

--_text-light: hsl(210 10% 30%);
--_text-dark: hsl(210 5% 95%);
--_text: var(--_text-light);

Cor do plano de fundo do botão

Os planos de fundo dos botões seguem o mesmo padrão hsl(), exceto os botões do tema claro, que são definidos como branco para que a superfície os faça parecer próximos ao usuário ou na frente de outras superfícies:

--_bg-light: hsl(0 0% 100%);
--_bg-dark: hsl(210 9% 31%);
--_bg: var(--_bg-light);

Fundo do botão

Essa cor de plano de fundo serve para fazer com que uma superfície apareça atrás de outras, útil para o plano de fundo da entrada de arquivo:

--_input-well-light: hsl(210 16% 87%);
--_input-well-dark: hsl(204 10% 10%);
--_input-well: var(--_input-well-light);

Padding do botão

O espaçamento ao redor do texto no botão é feito usando a unidade ch, uma extensão relativa ao tamanho da fonte. Isso se torna crítico quando botões grandes podem simplesmente aumentar o font-size e o botão é dimensionado proporcionalmente:

--_padding-inline: 1.75ch;
--_padding-block: .75ch;

Borda do botão

O raio da borda do botão é armazenado em uma propriedade personalizada para que a entrada de arquivo possa combinar com os outros botões. As cores da borda seguem o sistema de cores adaptativas estabelecido:

--_border-radius: .5ch;

--_border-light: hsl(210 14% 89%);
--_border-dark: var(--_bg-dark);
--_border: var(--_border-light);

Efeito de destaque ao passar o cursor sobre o botão

Essas propriedades estabelecem uma propriedade de tamanho para a transição na interação, e a cor de destaque segue o sistema de cores adaptável. Vamos abordar como eles interagem mais adiante nesta postagem, mas, em última análise, eles são usados para um efeito box-shadow:

--_highlight-size: 0;

--_highlight-light: hsl(210 10% 71% / 25%);
--_highlight-dark: hsl(210 10% 5% / 25%);
--_highlight: var(--_highlight-light);

Sombra do texto do botão

Cada botão tem um estilo de sombra de texto sutil. Isso ajuda o texto a ficar na parte de cima do botão, melhorando a legibilidade e adicionando uma camada de apresentação.

--_ink-shadow-light: 0 1px 0 var(--_border-light);
--_ink-shadow-dark: 0 1px 0 hsl(210 11% 15%);
--_ink-shadow: var(--_ink-shadow-light);

Ícone de botão

Os ícones têm o tamanho de dois caracteres, graças à unidade de comprimento relativo ch, que ajuda a escalar o ícone proporcionalmente ao texto do botão. A cor do ícone se baseia no --_accent-color para uma cor adaptativa e dentro do tema.

--_icon-size: 2ch;
--_icon-color: var(--_accent);

Sombra do botão

Para que as sombras se adaptem adequadamente à luz e à escuridão, elas precisam mudar a cor e a opacidade. As sombras do tema claro são melhores quando são sutis e tingidas para a cor da superfície que elas sobrepõem. As sombras do tema escuro precisam ser mais escuras e mais saturadas para sobrepor cores de superfície mais escuras.

--_shadow-color-light: 220 3% 15%;
--_shadow-color-dark: 220 40% 2%;
--_shadow-color: var(--_shadow-color-light);

--_shadow-strength-light: 1%;
--_shadow-strength-dark: 25%;
--_shadow-strength: var(--_shadow-strength-light);

Com cores e intensidades adaptativas, posso criar duas profundidades de sombras:

--_shadow-1: 0 1px 2px -1px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 9%));

--_shadow-2: 
  0 3px 5px -2px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 3%)),
  0 7px 14px -5px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 5%));

Além disso, para dar aos botões uma aparência um pouco em 3D, uma caixa de sombra 1px cria a ilusão:

--_shadow-depth-light: 0 1px var(--_border-light);
--_shadow-depth-dark: 0 1px var(--_bg-dark);
--_shadow-depth: var(--_shadow-depth-light);

Transições de botões

Seguindo o padrão de cores adaptáveis, criei duas propriedades estáticas para armazenar as opções do sistema de design:

--_transition-motion-reduce: ;
--_transition-motion-ok:
  box-shadow 145ms ease,
  outline-offset 145ms ease
;
--_transition: var(--_transition-motion-reduce);

Todas as propriedades juntas no seletor

Todas as propriedades personalizadas em um seletor

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"],
  input[type="file"]
),
:where(input[type="file"])::file-selector-button {
  --_accent-light: hsl(210 100% 40%);
  --_accent-dark: hsl(210 50% 70%);
  --_accent: var(--_accent-light);

--_text-light: hsl(210 10% 30%); --_text-dark: hsl(210 5% 95%); --_text: var(--_text-light);

--_bg-light: hsl(0 0% 100%); --_bg-dark: hsl(210 9% 31%); --_bg: var(--_bg-light);

--_input-well-light: hsl(210 16% 87%); --_input-well-dark: hsl(204 10% 10%); --_input-well: var(--_input-well-light);

--_padding-inline: 1.75ch; --_padding-block: .75ch;

--_border-radius: .5ch; --_border-light: hsl(210 14% 89%); --_border-dark: var(--_bg-dark); --_border: var(--_border-light);

--_highlight-size: 0; --_highlight-light: hsl(210 10% 71% / 25%); --_highlight-dark: hsl(210 10% 5% / 25%); --_highlight: var(--_highlight-light);

--_ink-shadow-light: 0 1px 0 hsl(210 14% 89%); --_ink-shadow-dark: 0 1px 0 hsl(210 11% 15%); --_ink-shadow: var(--_ink-shadow-light);

--_icon-size: 2ch; --_icon-color-light: var(--_accent-light); --_icon-color-dark: var(--_accent-dark); --_icon-color: var(--accent, var(--_icon-color-light));

--_shadow-color-light: 220 3% 15%; --_shadow-color-dark: 220 40% 2%; --_shadow-color: var(--_shadow-color-light); --_shadow-strength-light: 1%; --_shadow-strength-dark: 25%; --_shadow-strength: var(--_shadow-strength-light); --_shadow-1: 0 1px 2px -1px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 9%)); --_shadow-2: 0 3px 5px -2px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 3%)), 0 7px 14px -5px hsl(var(--_shadow-color)/calc(var(--_shadow-strength) + 5%)) ;

--_shadow-depth-light: hsl(210 14% 89%); --_shadow-depth-dark: var(--_bg-dark); --_shadow-depth: var(--_shadow-depth-light);

--_transition-motion-reduce: ; --_transition-motion-ok: box-shadow 145ms ease, outline-offset 145ms ease ; --_transition: var(--_transition-motion-reduce); }

Os botões padrão aparecem lado a lado nos temas claro e escuro.

Adaptações do tema escuro

O valor do padrão de propriedades estáticas -light e -dark fica claro quando as propriedades do tema escuro são definidas:

@media (prefers-color-scheme: dark) {
  :where(
    button,
    input[type="button"],
    input[type="submit"],
    input[type="reset"],
    input[type="file"]
  ),
  :where(input[type="file"])::file-selector-button {
    --_bg: var(--_bg-dark);
    --_text: var(--_text-dark);
    --_border: var(--_border-dark);
    --_accent: var(--_accent-dark);
    --_highlight: var(--_highlight-dark);
    --_input-well: var(--_input-well-dark);
    --_ink-shadow: var(--_ink-shadow-dark);
    --_shadow-depth: var(--_shadow-depth-dark);
    --_shadow-color: var(--_shadow-color-dark);
    --_shadow-strength: var(--_shadow-strength-dark);
  }
}

Além de ser mais fácil de ler, os consumidores desses botões personalizados podem usar as propriedades simples com a confiança de que elas serão adaptadas adequadamente às preferências do usuário.

Adaptações de movimento reduzidas

Se o movimento estiver OK com esse usuário visitante, atribua --_transition a var(--_transition-motion-ok):

@media (prefers-reduced-motion: no-preference) {
  :where(
    button,
    input[type="button"],
    input[type="submit"],
    input[type="reset"],
    input[type="file"]
  ),
  :where(input[type="file"])::file-selector-button {
    --_transition: var(--_transition-motion-ok);
  }
}

Alguns estilos compartilhados

Os botões e as entradas precisam ter as fontes definidas como inherit para corresponder ao resto das fontes da página. Caso contrário, elas serão estilizadas pelo navegador. Isso também se aplica a letter-spacing. A definição de line-height como 1.5 define o tamanho da caixa de letras para dar ao texto algum espaço acima e abaixo:

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"],
  input[type="file"]
),
:where(input[type="file"])::file-selector-button {
  /* …CSS variables */

  font: inherit;
  letter-spacing: inherit;
  line-height: 1.5;
  border-radius: var(--_border-radius);
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Estilizar botões

Ajuste do seletor

O seletor input[type="file"] não é a parte do botão da entrada, o pseudoelemento ::file-selector-button é. Portanto, removi input[type="file"] da lista:

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"],
  input[type="file"]
),
:where(input[type="file"])::file-selector-button {
  
}

Ajustes de cursor e toque

Primeiro, estilizo o cursor com o estilo pointer, que ajuda o botão a indicar aos usuários do mouse que ele é interativo. Em seguida, adiciono touch-action: manipulation para que os cliques não precisem esperar e observar um possível clique duplo, deixando os botões mais rápidos:

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
  cursor: pointer;
  touch-action: manipulation;
}

Cores e bordas

Em seguida, personalizei o tamanho da fonte, o plano de fundo, o texto e as cores da borda usando algumas das propriedades personalizadas adaptáveis estabelecidas anteriormente:

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
  

  font-size: var(--_size, 1rem);
  font-weight: 700;
  background: var(--_bg);
  color: var(--_text);
  border: 2px solid var(--_border);
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Sombras

Os botões têm algumas técnicas excelentes aplicadas. O text-shadow se adapta à luz e à escuridão, criando uma aparência sutil e agradável do texto do botão sobre o plano de fundo. Para o box-shadow, três sombras são atribuídas. A primeira, --_shadow-2, é uma sombra de caixa normal. A segunda sombra é um truque para os olhos que faz com que o botão pareça um pouco chanfrado. A última sombra é para o destaque do cursor, inicialmente com um tamanho de 0, mas vai receber um tamanho mais tarde e fazer a transição para parecer que está crescendo do botão.

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
  

  box-shadow: 
    var(--_shadow-2),
    var(--_shadow-depth),
    0 0 0 var(--_highlight-size) var(--_highlight)
  ;
  text-shadow: var(--_ink-shadow);
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Layout

Eu dei ao botão um layout de flexbox, especificamente um layout inline-flex que se encaixa no conteúdo. Em seguida, centralizo o texto e alinhei as filhas vertical e horizontalmente ao centro. Isso vai ajudar os ícones e outros elementos do botão a se alinhar corretamente.

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
  

  display: inline-flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Espaçamento

Para o espaçamento de botões, usei gap para evitar que irmãos se toquem e propriedades lógicas para padding, para que o espaçamento de botões funcione para todos os layouts de texto.

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
  

  gap: 1ch;
  padding-block: var(--_padding-block);
  padding-inline: var(--_padding-inline);
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

UX de toque e mouse

Esta próxima seção é principalmente para usuários de dispositivos móveis com tela touch. A primeira propriedade, user-select, é para todos os usuários. Ela impede que o texto destaque o texto do botão. Isso é mais perceptível em dispositivos touchscreen quando um botão é pressionado e mantido e o sistema operacional destaca o texto do botão.

Geralmente, essa não é a experiência do usuário com botões em apps integrados. Por isso, desativei essa opção definindo user-select como "none". Toque em cores de destaque (-webkit-tap-highlight-color) e menus de contexto do sistema operacional (-webkit-touch-callout) são outros recursos de botão muito focados na Web que não estão alinhados com as expectativas gerais dos usuários, então também os removi.

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
  

  user-select: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}

Transições

A variável adaptativa --_transition é atribuída à propriedade transition:

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
),
:where(input[type="file"])::file-selector-button {
  

  transition: var(--_transition);
}

Ao passar o cursor, enquanto o usuário não está pressionando ativamente, ajuste o tamanho do destaque de sombra para dar uma aparência de foco que parece crescer dentro do botão:

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
):where(:not(:active):hover) {
  --_highlight-size: .5rem;
}

Ao receber o foco, aumente o deslocamento do contorno do botão, a ele uma aparência de foco que parece crescer dentro do botão:

:where(button, input):where(:not(:active)):focus-visible {
  outline-offset: 5px;
}

Ícones

Para processar ícones, o seletor tem um seletor :where() adicionado para filhos SVG diretos ou elementos com o atributo personalizado data-icon. O tamanho do ícone é definido com a propriedade personalizada usando propriedades lógicas inline e de bloco. A cor do traço é definida, assim como um drop-shadow para corresponder ao text-shadow. flex-shrink é definido como 0 para que o ícone nunca seja reduzido. Por fim, seleciono ícones com linhas e atribuo esses estilos aqui com fill: none e round, com as letras maiúsculas e as junções de linhas:

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
) > :where(svg, [data-icon]) {
  block-size: var(--_icon-size);
  inline-size: var(--_icon-size);
  stroke: var(--_icon-color);
  filter: drop-shadow(var(--_ink-shadow));

  flex-shrink: 0;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Personalizar botões de envio

Queria que os botões de envio tivessem uma aparência um pouco mais destacada. Para isso, fiz a cor do texto dos botões ser a cor de destaque:

:where(
  [type="submit"], 
  form button:not([type],[disabled])
) {
  --_text: var(--_accent);
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Personalizar botões de redefinição

Queria que os botões de redefinição tivessem alguns sinais de aviso integrados para alertar os usuários sobre o comportamento potencialmente destrutivo. Também escolhi estilizar o botão do tema claro com mais detalhes em vermelho do que o tema escuro. A personalização é feita mudando a cor de fundo clara ou escura apropriada, e o botão vai atualizar o estilo:

:where([type="reset"]) {
  --_border-light: hsl(0 100% 83%);
  --_highlight-light: hsl(0 100% 89% / 20%);
  --_text-light: hsl(0 80% 50%);
  --_text-dark: hsl(0 100% 89%);
}

Também achei que seria bom se a cor do contorno de foco correspondesse ao destaque vermelho. A cor do texto adapta um vermelho escuro para um vermelho claro. Eu faço com que a cor do contorno corresponda a isso com a palavra-chave currentColor:

:where([type="reset"]):focus-visible {
  outline-color: currentColor;
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Personalizar botões desativados

É muito comum que botões desativados tenham um contraste de cor ruim durante a tentativa de atenuar o botão desativado para que ele pareça menos ativo. Testei cada conjunto de cores e me certifiquei de que eles foram aprovados, ajustando o valor de luminosidade do HSL até que a pontuação fosse aprovada no DevTools ou no VisBug.

:where(
  button,
  input[type="button"],
  input[type="submit"],
  input[type="reset"]
)[disabled] {
  --_bg: none;
  --_text-light: hsl(210 7% 40%);
  --_text-dark: hsl(210 11% 71%);

  cursor: not-allowed;
  box-shadow: var(--_shadow-1);
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Personalizar botões de entrada de arquivo

O botão de entrada de arquivo é um contêiner para um span e um botão. O CSS pode dar um pouco de estilo ao contêiner de entrada, bem como ao botão aninhado, mas não ao span. O contêiner recebe max-inline-size para não crescer mais do que precisa, enquanto inline-size: 100% permite que ele encolha e se encaixe em contêineres menores. A cor de fundo é definida como uma cor adaptativa mais escura do que outras superfícies, para que ela fique atrás do botão do seletor de arquivos.

:where(input[type="file"]) {
  inline-size: 100%;
  max-inline-size: max-content;
  background-color: var(--_input-well);
}

O botão de seleção de arquivo e os botões de tipo de entrada são especificamente appearance: none para remover todos os estilos fornecidos pelo navegador que não foram substituídos pelos outros estilos de botão.

:where(input[type="button"]),
:where(input[type="file"])::file-selector-button {
  appearance: none;
}

Por fim, a margem é adicionada ao inline-end do botão para afastar o texto do span do botão, criando algum espaço.

:where(input[type="file"])::file-selector-button {
  margin-inline-end: var(--_padding-inline);
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Exceções especiais do tema escuro

Dei aos botões de ação principais um plano de fundo mais escuro para aumentar o contraste do texto, a eles uma aparência um pouco mais destacada.

@media (prefers-color-scheme: dark) {
  :where(
    [type="submit"],
    [type="reset"],
    [disabled],
    form button:not([type="button"])
  ) {
    --_bg: var(--_input-well);
  }
}

Captura de tela mostrando os botões depois que os estilos anteriores foram aplicados.

Criar variantes

Para fins de diversão e porque é prático, escolhi mostrar como criar algumas variantes. Uma variante é muito vibrante, semelhante à aparência dos botões principais. Outra variante é grande. A última variante tem um ícone preenchido com gradiente.

Botão vibrante

Para conseguir esse estilo de botão, substituí as propriedades básicas diretamente por cores azuis. Embora isso tenha sido rápido e fácil, ele remove os elementos adaptáveis e tem a mesma aparência nos temas claro e escuro.

.btn-custom {
  --_bg: linear-gradient(hsl(228 94% 67%), hsl(228 81% 59%));
  --_border: hsl(228 89% 63%);
  --_text: hsl(228 89% 100%);
  --_ink-shadow: 0 1px 0 hsl(228 57% 50%);
  --_highlight: hsl(228 94% 67% / 20%);
}

O botão personalizado é mostrado em temas claro e escuro. Ele é azul muito vibrante, como os botões de ação principais tendem a ser.

Botão grande

Esse estilo de botão é alcançado modificando a propriedade personalizada --_size. O padding e outros elementos de espaço são relativos a esse tamanho, dimensionando-se proporcionalmente com o novo tamanho.

.btn-large {
  --_size: 1.5rem;
}

O botão grande é mostrado ao lado do botão personalizado, cerca de 150 vezes maior.

Botão de ícone

Esse efeito de ícone não tem nada a ver com nossos estilos de botão, mas mostra como fazer isso com apenas algumas propriedades do CSS e como o botão processa ícones que não são SVGs inline.

[data-icon="cloud"] {
  --icon-cloud: url("https://api.iconify.design/mdi:apple-icloud.svg") center / contain no-repeat;

  -webkit-mask: var(--icon-cloud);
  mask: var(--icon-cloud);
  background: linear-gradient(to bottom, var(--_accent-dark), var(--_accent-light));
}

Um botão com um ícone é mostrado em temas claros e escuros.

Conclusão

Agora que você sabe como eu fiz, como você faria? 🙂

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 de remixes da comunidade abaixo.

Remixes da comunidade

Ainda não há nada aqui.

Recursos