Como criar um componente de botão

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

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 veja a fonte

Os botões são usados com teclado e mouse nos temas claro e escuro.

Se preferir vídeo, confira uma versão deste post no YouTube:

Visão geral

Browser Support

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

Source

O elemento <button> foi 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 nenhuma personalização. Use color-scheme para ativar os botões claro e escuro 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 vai se adaptar a estar dentro de 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 deles. Os botões de redefinição terão cores de aviso porque são destrutivos, e os botões de envio receberão um texto de destaque azul para que pareçam um pouco mais promovidos do que os botões normais.

Prévia do conjunto final de todos os tipos de botões, mostrados em um formulário e fora dele, com adições interessantes para botões de ícone e personalizados.
Prévia do conjunto final de todos os tipos de botões, mostrados em um formulário e fora dele, com adições interessantes para botões de ícone e personalizados

Os botões também têm pseudoclasses para o CSS usar na estilização. Essas classes fornecem hooks de CSS para personalizar a aparência 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 na estilização de tecnologia assistiva.

button:hover {}
button:active {}
button:focus {}
button:focus-visible {}
Prévia do conjunto final de todos os tipos de botões no tema escuro.
Prévia do conjunto final de todos os tipos de botões 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 teste, cada botão é colocado dentro de um formulário. Assim, 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 mudo a estratégia de ícones, de SVG in-line para um 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 é bem grande neste momento. Entre tipos de botões, pseudoclasses e estar dentro ou fora de um formulário, há mais de 20 combinações de botões. Ainda bem que o CSS pode 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 ao mesmo tempo

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

button:is(:hover, :focus) {
  
}
Teste uma demonstração!

Círculo de foco interativo

Gosto de animar o anel de foco para usuários de teclado e tecnologia adaptativa. Para isso, animo o contorno para longe do botão em 5 px, mas apenas quando ele não está ativo. Isso cria um efeito que faz o anel de foco encolher de volta ao tamanho do botão quando pressionado.

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

Como garantir um contraste de cor adequado

Há pelo menos quatro combinações de cores diferentes em 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:

Ocultar ícones de pessoas que não podem ver

Ao criar um botão de ícone, o ícone precisa dar suporte visual ao texto do botão. Isso também significa que o ícone não é útil 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 de botões decorativos:

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

Na próxima seção, vou estabelecer um sistema de propriedades personalizadas 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 adaptativa de propriedade personalizada

A estratégia de propriedade personalizada usada neste desafio de GUI é muito semelhante à usada para criar um esquema de cores. Para um sistema adaptável de cores claras e escuras, uma propriedade personalizada para cada tema é definida e nomeada de acordo. Em seguida, uma única propriedade personalizada é usada para manter 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, atualizando 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. O redirecionamento e a abstração são descarregados na 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 só é aplicado condicionalmente.

Preparação para consistência de design

O seletor compartilhado

O seletor a seguir é usado para segmentar todos os tipos de botões e é um pouco complexo 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 serão incluídas nesse seletor. É hora de revisar todas as propriedades personalizadas. Há algumas propriedades personalizadas usadas nesse botão. Vou descrever cada grupo à medida que avançamos e, no final da seção, vou compartilhar os contextos de movimento reduzido e escuro.

Cor de destaque do botão

Botões de envio e ícones são ótimos lugares para 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 branco ou preto, mas versões escurecidas ou clareadas de --_accent usando hsl() e mantendo o 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 brancos para que a superfície deles pareça estar perto do 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);

Bloco de plano de fundo do botão

Essa cor de plano de fundo faz com que uma superfície apareça atrás de outras, o que é ú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, um comprimento relativo ao tamanho da fonte. Isso se torna crítico quando botões grandes podem simplesmente aumentar o font-size e as escalas de botões 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 corresponda aos outros botões. As cores da borda seguem o sistema de cores adaptáveis 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 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, são usados para um efeito de 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 sutil de sombra de texto. Isso ajuda o texto a ficar em cima do botão, melhorando a legibilidade e adicionando uma camada de apresentação refinada.

--_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 dimensionar o ícone proporcionalmente ao texto do botão. A cor do ícone usa o --_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 corretamente à luz e à escuridão, elas precisam mudar a cor e a opacidade. As sombras do tema claro ficam melhores quando são sutis e têm uma tonalidade em direção à cor da superfície em que estão sobrepostas. As sombras do tema escuro precisam ser mais escuras e saturadas para que possam se sobrepor a 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 adaptáveis, posso montar 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 caixa-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, crio duas propriedades estáticas para manter 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 ter uma boa leitura, os consumidores desses botões personalizados podem usar as propriedades simples com a confiança de que elas vão se adaptar adequadamente às preferências do usuário.

Adaptações de movimento reduzido

Se o movimento não for um problema para o 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 entradas precisam ter as fontes definidas como inherit para corresponderem ao restante das fontes da página. Caso contrário, o navegador vai definir o estilo. Isso também se aplica a letter-spacing. Definir line-height como 1.5 define o tamanho da caixa de carta 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, mas o pseudoelemento ::file-selector-button é. Por isso, 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, eu estilizo o cursor para o estilo pointer, o que ajuda o botão a indicar para usuários de 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, fazendo com que os botões pareçam 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, personalizo 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 é adaptável a claro e escuro, criando uma aparência sutil agradável do texto do botão assentado bem em cima do 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 o olho que faz o botão parecer um pouco chanfrado. A última sombra é para o destaque ao passar o cursor, inicialmente com tamanho 0, mas ela vai receber um tamanho depois e será transicionada para que pareça crescer 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

Dei ao botão um layout flexbox, especificamente um layout inline-flex que se ajusta ao conteúdo. Em seguida, centralizo o texto e alinho os filhos vertical e horizontalmente ao centro. Isso ajuda os ícones e outros elementos de botão a se alinharem 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 dos botões, usei gap para evitar que elementos irmãos se toquem e propriedades lógicas para padding. Assim, o espaçamento dos botões funciona 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 dispositivos móveis com tela sensível ao toque. A primeira propriedade, user-select, é para todos os usuários e impede que o texto do botão seja destacado. Isso é mais perceptível em dispositivos sensíveis ao toque quando um botão é tocado e pressionado, e o sistema operacional destaca o texto do botão.

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

: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 estiver pressionando ativamente, ajuste o tamanho do destaque da sombra para dar uma aparência de foco agradável que parece crescer de dentro do botão:

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

Ao focar, aumente o deslocamento do contorno de foco do botão, a ele uma aparência de foco agradável que parece crescer de 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() adicional 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 está definido como 0 para que o ícone nunca seja comprimido. Por fim, seleciono ícones com linhas e atribuo esses estilos aqui com terminações e junções de linha fill: none e round:

: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

Eu queria que os botões de envio tivessem uma aparência um pouco mais destacada, e consegui isso definindo a cor do texto dos botões como 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

Eu queria que os botões de redefinição tivessem alguns sinais de alerta integrados para alertar os usuários sobre o comportamento potencialmente destrutivo deles. 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 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 se a cor do contorno de foco correspondesse ao destaque do vermelho. A cor do texto muda de um vermelho escuro para um vermelho claro. 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 cores ruim durante a tentativa de reduzir o botão desativado para que ele pareça menos ativo. Teste cada conjunto de cores e verifique se eles foram aprovados, ajustando o valor de luminosidade 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.

Como personalizar botões de entrada de arquivo

O botão de entrada de arquivo é um contêiner para um intervalo e um botão. O CSS consegue estilizar um pouco o contêiner de entrada e o botão aninhado, mas não o span. O contêiner recebe max-inline-size para não crescer mais do que o necessário, enquanto inline-size: 100% se encolhe e se ajusta a contêineres menores. A cor de fundo é definida como uma cor adaptável mais escura que outras superfícies, para que ela apareça 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 arquivos e os botões de tipo de entrada recebem especificamente appearance: none para remover 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 intervalo 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 fundo mais escuro para um texto de maior contraste, conferindo-lhes uma aparência ligeiramente 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

Por diversão e praticidade, 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, sobrescrevi as propriedades básicas diretamente com cores azuis. Embora isso tenha sido rápido e fácil, ele remove as propriedades 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%);
}

Botão personalizado mostrado em claro e escuro. É um azul muito vibrante, como costumam ser os botões de ação principais típicos.

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, sendo dimensionados proporcionalmente com o novo tamanho.

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

Um 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 de CSS e como o botão lida bem com ícones que não são 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 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, me envie um tweet com o link, e eu vou adicionar à seção de remixes da comunidade abaixo.

Remixes da comunidade

Ainda não há nada aqui.

Recursos