Como criar um componente de botão

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

Nesta postagem, quero compartilhar minha opinião sobre como criar um elemento <button> adaptável, responsivo e acessível. Experimente a demonstração e veja a fonte

Os botões interagem com o teclado e o mouse nos temas claro e escuro.

Se preferir vídeo, aqui está uma versão do YouTube desta postagem:

Visão geral

Compatibilidade com navegadores

  • 1
  • 12
  • 1
  • ≤4

Origem

O elemento <button> foi criado para 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 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 uma <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 terá estilos para ajudar a diferenciar visualmente a intent. Os botões de redefinição terão cores de aviso, por serem destrutivos, e os botões de envio vão ter um texto em destaque azul para que apareçam um pouco mais promovidos do que os botões comuns.

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 ícones e botões personalizados.
Visualização 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

Os botões também têm pseudoclasses que o CSS pode usar para definir o estilo. Essas classes fornecem ganchos CSS para personalizar a funcionalidade 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 auxiliar 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ã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 outro 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. Dessa forma, posso garantir que os estilos sejam atualizados corretamente para o botão padrão, que se comporta como um botão de envio. Também mudo a estratégia de ícones, do SVG inline para um SVG mascarado, para garantir que os dois 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 é bastante complexa neste momento. Entre tipos de botão, 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 do 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) {
  …
}
Faça uma demonstração.

Anel de foco interativo

Gosto de animar o anel de foco para usuários de teclado e tecnologia assistiva. Posso fazer isso animando o contorno do botão em 5px, mas somente quando o botão não está ativo. Isso cria um efeito que faz o anel de foco reduzir ao tamanho do botão quando pressionado.

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

Garantir a transmissão do contraste de cores

Há pelo menos quatro combinações diferentes de cores claras e escuras 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 só vez:

Ocultando ícones de pessoas que não conseguem 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 tem valor 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 tem aria-hidden definido como true.
Chrome DevTools mostrando a árvore de acessibilidade do botão. A árvore ignora a imagem do botão porque ele tem aria-hidden definido como true.

Estilos

Na próxima seção, primeiro estabelecemos 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 de propriedade personalizada adaptável

A estratégia de propriedade personalizada usada neste desafio da GUI é muito semelhante à usada na criação de 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 armazenar o valor atual do tema e é atribuída a uma propriedade CSS. Depois, a única propriedade personalizada pode ser atualizada para um valor diferente e, em seguida, atualizar 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 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 fica claro que o tema claro é o tema padrão e o escuro só é aplicado condicionalmente.

Como se preparar para a consistência do design

Seletor compartilhado

O seletor a seguir é usado para segmentar todos os vários tipos de botões e é um pouco complicado no início. O :where() é usado. Portanto, a personalização do botão não requer 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 terão o escopo definido dentro desse seletor. É hora de revisar todas as propriedades personalizadas. Há algumas propriedades personalizadas usadas nesse botão. Vou descrever cada grupo conforme avançamos, depois compartilhar os contextos de movimento escuros e reduzidos no final da seção.

Cor de destaque do botão

Botões e ícones de envio são um ótimo lugar 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. Elas são versões escurecidas de --_accent usando hsl() e mantendo a tonalidade 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 pelos botões do tema claro, que são definidos como brancos para que a superfície deles apareça perto do usuário ou na frente de outras plataformas:

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

Bom plano de 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 do 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 fundamental quando botões grandes podem simplesmente aumentar a font-size e as escalas de forma proporcional:

--_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 do arquivo possa corresponder aos outros botões. As cores das bordas 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 do mouse

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áveis. Abordaremos como eles interagem mais adiante nesta postagem, mas, em última instância, 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 sutil de sombra de texto. Isso ajuda o texto a ficar em cima do botão, melhorando a legibilidade e adicionando uma boa camada de polimento da 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 ao tamanho relativo da unidade ch novamente, o que ajuda o ícone a ser dimensionado proporcionalmente ao texto do botão. A cor do ícone se baseia na --_accent-color para 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 corretamente à luz e ao escuro, elas precisam mudar a cor e a opacidade. Sombras do tema claro são melhores quando são sutis e coloridas em relação à cor da superfície sobreposta. As sombras do tema escuro precisam ser mais escuras e mais saturadas para que possam 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 adaptáveis, é possível 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 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, 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 no tema 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 uma boa leitura, os consumidores desses botões personalizados podem usar os acessórios básicos com a certeza de que eles se adaptarão adequadamente às preferências do usuário.

Redução das adaptações de movimento

Se o movimento for aceitável para 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

As fontes dos botões e entradas precisam ser definidas como inherit para que correspondam ao restante das fontes da página. Caso contrário, elas serão estilizadas pelo navegador. Isso também se aplica a letter-spacing. Definir line-height como 1.5 define o tamanho da caixa de texto 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 após a aplicação dos estilos anteriores.

Como definir o estilo de botões

Ajuste do seletor

O seletor input[type="file"] não é a parte do botão da entrada, 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, defina o estilo do cursor como pointer, que ajuda o botão a indicar aos usuários do mouse que ele é interativo. Em seguida, adiciono touch-action: manipulation para fazer com que os cliques não precisem esperar e notar 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, personalizo o tamanho da fonte, o plano de fundo, o texto e as cores da borda usando algumas 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 após a aplicação dos estilos anteriores.

Sombras

Os botões têm algumas ótimas técnicas aplicadas. O text-shadow é adaptável à iluminação e ao escuro, criando uma aparência sutil e agradável do texto do botão sobre o plano de fundo. Para a 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 o botão parecer um pouco chanfrado. A última sombra é para o destaque ao passar o cursor, inicialmente em um tamanho de 0, mas recebe um tamanho depois e é transferido para que pareça crescer a partir 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 após a aplicação dos estilos anteriores.

Layout

Dei ao botão um layout flexbox, especificamente um layout inline-flex que vai se adequar ao conteúdo. Em seguida, centralizo o texto e alinho os filhos vertical e horizontalmente ao centro. Isso ajudará 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 após a aplicação dos estilos anteriores.

Espaçamento

Para o espaçamento dos botões, usei gap para evitar que irmãos toquem e propriedades lógicas para preenchimento, para que o espaçamento dos botões funcione em 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 após a aplicação dos estilos anteriores.

UX por toque e mouse

A próxima seção é voltada principalmente para usuários de toque em dispositivos móveis. A primeira propriedade, user-select, é destinada a todos os usuários e impede que o texto do botão seja destacado. Isso é principalmente perceptível em dispositivos touchscreen quando um botão é tocado e pressionado e o sistema operacional destaca o texto dele.

Geralmente, descobri que essa não é a experiência do usuário com botões em apps integrados, então desativo user-select como nenhum. As cores de destaque de toque (-webkit-tap-highlight-color) e os 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 do usuário, então eu 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 --_transition adaptável é 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 boa aparência de foco, que pareça crescer dentro do botão:

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

No foco, aumente o deslocamento do contorno do foco do botão, oferecendo uma melhor aparência de foco que pareça crescer dentro do botão:

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

Ícones

Para o processamento de ícones, o seletor tem um seletor :where() adicionado para filhos ou elementos SVG diretos 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 e uma drop-shadow para corresponder ao text-shadow. flex-shrink está definido como 0 para que o ícone nunca seja comprimido. Por fim, seleciono ícones alinhados 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 após a aplicação dos estilos anteriores.

Como personalizar os botões de envio

Eu queria que os botões de envio tivessem uma aparência ligeiramente promovida e consegui isso tornando a cor do texto dos botões a cor de destaque:

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

Captura de tela mostrando os botões após a aplicação dos estilos anteriores.

Personalizar botões de redefinição

Eu 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 de tema claro com mais tons de vermelho do que o tema escuro. A personalização é feita mudando a cor clara ou escura subjacente apropriada, 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 bom a cor do contorno do foco combinar com o destaque do vermelho. A cor do texto muda de vermelho escuro para vermelho-claro. Faço a correspondência com a cor do contorno com a palavra-chave currentColor:

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

Captura de tela mostrando os botões após a aplicação dos estilos anteriores.

Personalizar botões desativados

É muito comum que os botões desativados tenham um contraste de cor ruim durante a tentativa de diminuir o botão desativado para que ele pareça menos ativo. Testei cada conjunto de cores e verifiquei se eles eram aprovados, alertando o valor de luminosidade do HSL até que a pontuação fosse transmitida 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 após a aplicação dos estilos anteriores.

Como personalizar botões de entrada de arquivo

O botão de entrada de arquivo é um contêiner para um período e um botão. O CSS pode estilizar um pouco o contêiner de entrada e o botão aninhado, mas não o período. O contêiner recebe max-inline-size para que não fique maior do que o necessário, enquanto inline-size: 100% permite reduzir e ajustar contêineres menores do que é. A cor de fundo é definida como uma cor adaptável mais escura do que outras superfícies, por isso fica atrás do botão seletor de arquivos.

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

O botão seletor de arquivos e os botões de tipo de entrada recebem 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 empurrar o texto do período para longe 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 após a aplicação dos estilos anteriores.

Exceções especiais do tema escuro

Apliquei aos botões de ação principais um plano de fundo mais escuro para que o texto tenha um contraste mais alto, 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 após a aplicação dos estilos anteriores.

Criar variantes

Por diversão e por ser 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 alcançar esse estilo de botão, substituí os acessórios de base 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 é exibido em tons claros e escuros. É um azul muito vibrante, como os botões de ação principais comuns.

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, escalonando proporcionalmente ao 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 os estilos de botão, mas mostra como conseguir isso com apenas algumas propriedades CSS e como o botão lida com ícones que não estão inline no SVG.

[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 isso, o que você faria ‽ 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web.

Crie uma demonstração, envie um tweet para mim e os adicionarei à seção de remixes da comunidade abaixo.

Remixes da comunidade

Ainda não há nada aqui.

Recursos