Como criar um componente de botão

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

Nesta postagem, 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 por teclado, mouse, toque, voz e muito mais, com regras inteligentes sobre o tempo. 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 também os botões claros e escuros fornecidos pelo navegador.

Há também diferentes tipos de botões, cada um mostrado na incorporação anterior do Codepen. Um <button> sem um tipo se adaptará a 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 da GUI deste mês, cada botão vai receber estilos para ajudar a diferenciar visualmente a intenção. 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, mostrados em um formulário e não em um formulário, com boas adições de 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 de CSS para personalizar a sensação do botão: :hover para quando um mouse estiver sobre o botão, :active para quando um mouse ou teclado estiver pressionando e :focus ou :focus-visible para auxiliar no estilo de tecnologia adaptativa.

button:hover {}
button:active {}
button:focus {}
button:focus-visible {}
Visualização do conjunto final de todos os tipos de botão 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">

Para testar, 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 de tecnologia assistiva. 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 o anel de foco recolher ao tamanho do botão quando pressionado.

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

Garantir um contraste de cor passageiro

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 com deficiência visual

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 leitura de tela para que pessoas com perda de visão não sejam incomodadas com imagens de botões decorativos:

<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 sua aparência.

Uma estratégia de propriedade personalizada adaptável

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 claro e escuro são declarativos e claros. A indireção e a abstração são descarregadas na propriedade personalizada --_bg, que agora é a única propriedade "reativa". --_bg-light e --_bg-dark são estáticas. Também é claro que o tema claro é o padrão e o escuro é aplicado apenas condicionalmente.

Como se preparar para a consistência do design

O seletor compartilhado

O seletor a seguir é usado para direcionar todos os vários tipos de botões e é um pouco complicado 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 dos botões 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 relativa ch novamente, o que ajudará a escalar o ícone proporcionalmente ao texto do botão. A cor do ícone depende do --_accent-color para ter uma cor adaptável 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 levemente 3D, uma sombra de caixa 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.

Como definir o estilo dos 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, defina o estilo do cursor para o estilo pointer, que ajuda o botão a indicar para os usuários do mouse que ele é interativo. Em seguida, adicione touch-action: manipulation para fazer com que os cliques não precisem esperar e observar um possível clique duplo, fazendo os botões parecerem 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 ótimas técnicas 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 que 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 da aplicação dos estilos anteriores.

Layout

Dei ao botão um layout flexbox, especificamente um layout inline-flex que se encaixará 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

A próxima seção é destinada principalmente a usuários de toque em dispositivos móveis. A primeira propriedade, user-select, é para todos os usuários. Ela impede que o texto destaque o texto do botão. Isso é mais notório 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 --_transition adaptável é atribuída à propriedade de transição:

: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, dando 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 comprimido. Por fim, seleciono ícones de linhas e atribuo esses estilos aqui com limites de linha fill: none e round e junções de linha:

: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 da aplicação dos estilos anteriores.

Personalizar botões de envio

Eu queria que os botões de envio tivessem uma aparência um pouco promovida e consegui transformar a cor de texto dos botões na 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 subjacente clara ou escura adequada, e o botão atualiza 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 legal a cor do contorno do foco combinar com o destaque do vermelho. A cor do texto adapta um vermelho escuro a um vermelho-claro. Faço a correspondência entre a cor do contorno e a palavra-chave currentColor:

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

Captura de tela mostrando os botões depois da aplicação dos estilos anteriores.

Personalizar botões desativados

É muito comum que botões desativados tenham baixo contraste de cores durante a tentativa de subtrair 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 da aplicação dos estilos anteriores.

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 fornecidos 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 um texto mais contrastante, proporcionando uma aparência um pouco mais promovida.

@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.

Como 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 tenha sido rápido e fácil, ele remove os acessórios adaptáveis e tem a mesma aparência em temas claros e escuros.

.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 conseguir isso com apenas algumas propriedades CSS e como o botão processa ícones que não estão no formato SVG 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 nos temas claro e escuro.

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 um tweet para mim (link em inglês) e eu vou adicionar o conteúdo à seção de remixes da comunidade abaixo.

Remixes da comunidade

Ainda não há nada aqui.

Recursos