Uma visão geral básica de como criar um componente de botão dividido acessível.
Nesta postagem, quero compartilhar uma maneira de criar um botão dividido . Teste a demonstração.
Se preferir vídeo, confira uma versão desta postagem no YouTube:
Visão geral
Os botões divididos são botões que ocultam um botão principal e uma lista de botões adicionais. Eles são úteis para expor uma ação comum e aninhar ações secundárias e menos usadas até que sejam necessárias. Um botão dividido pode ser crucial para ajudar um design ocupado a parecer minimalista. Um botão dividido avançado pode até lembrar a última ação do usuário e promovê-la à posição principal.
Um botão de divisão comum pode ser encontrado no seu aplicativo de e-mail. A ação principal é enviar, mas talvez você possa enviar depois ou salvar um rascunho:

A área de ação compartilhada é boa, já que o usuário não precisa procurar. Eles sabem que as ações essenciais de e-mail estão contidas no botão dividido.
Peças
Vamos detalhar as partes essenciais de um botão dividido antes de discutir a orquestração geral e a experiência final do usuário. A ferramenta de inspeção de acessibilidade do VisBug é usada aqui para mostrar uma visão macro do componente, revelando aspectos do HTML, do estilo e da acessibilidade de cada parte principal.

Contêiner do botão dividido de nível superior
O componente de nível mais alto é um flexbox inline, com uma classe de
gui-split-button, que contém a ação principal
e o .gui-popup-button.

O botão de ação principal
O <button> inicialmente visível e focalizável se encaixa no contêiner com dois formatos de canto correspondentes para interações de foco, passar o cursor e ativa, que aparecem contidas em .gui-split-button.

O botão de ativação do pop-up
O elemento de suporte "botão pop-up" é para ativar e fazer alusão à lista de botões secundários. Observe que não é um <button> e não é possível focar nele. No entanto, ele é a âncora de posicionamento para .gui-popup e o host para :focus-within usado para apresentar o pop-up.

O card pop-up
Este é um card flutuante filho da âncora
.gui-popup-button, posicionado de forma absoluta e
encapsulando semanticamente a lista de botões.

As ações secundárias
Um <button> com foco e um tamanho de fonte um pouco menor do que o botão de ação principal tem um ícone e um estilo complementar ao botão principal.

Propriedades personalizadas
As variáveis a seguir ajudam a criar uma harmonia de cores e um local central para modificar valores usados em todo o componente.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);
.gui-split-button {
  --theme:             hsl(220 75% 50%);
  --theme-hover:  hsl(220 75% 45%);
  --theme-active:  hsl(220 75% 40%);
  --theme-text:      hsl(220 75% 25%);
  --theme-border: hsl(220 50% 75%);
  --ontheme:         hsl(220 90% 98%);
  --popupbg:         hsl(220 0% 100%);
  --border: 1px solid var(--theme-border);
  --radius: 6px;
  --in-speed: 50ms;
  --out-speed: 300ms;
  @media (--dark) {
    --theme:             hsl(220 50% 60%);
    --theme-hover:  hsl(220 50% 65%);
    --theme-active:  hsl(220 75% 70%);
    --theme-text:      hsl(220 10% 85%);
    --theme-border: hsl(220 20% 70%);
    --ontheme:         hsl(220 90% 5%);
    --popupbg:         hsl(220 10% 30%);
  }
}
Layouts e cores
Marcação
O elemento começa como um <div> com um nome de classe personalizado.
<div class="gui-split-button"></div>
Adicione o botão principal e os elementos .gui-popup-button.
<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>
Observe os atributos aria aria-haspopup e aria-expanded. Esses indicadores são
essenciais para que os leitores de tela conheçam a capacidade e o estado da experiência
do botão dividido. O atributo title é útil para todos.
Adicione um ícone <svg> e o elemento contêiner .gui-popup.
<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup"></ul>
  </span>
</div>
Para um posicionamento simples do pop-up, .gui-popup é um elemento filho do botão que
o expande. A única desvantagem dessa estratégia é que o contêiner .gui-split-button não pode usar overflow: hidden, porque isso impede que o pop-up apareça visualmente.
Um <ul> preenchido com conteúdo <li><button> se anuncia como uma "lista de botões" para leitores de tela, que é exatamente a interface apresentada.
<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li>
        <button>Schedule for later</button>
      </li>
      <li>
        <button>Delete</button>
      </li>
      <li>
        <button>Save draft</button>
      </li>
    </ul>
  </span>
</div>
Para dar um toque especial e usar cores de forma divertida, adicionei ícones aos botões secundários em https://heroicons.com. Os ícones são opcionais para os botões primários e secundários.
<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
        </svg>
        Schedule for later
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
        </svg>
        Delete
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
        </svg>
        Save draft
      </button></li>
    </ul>
  </span>
</div>
Estilos
Com o HTML e o conteúdo no lugar, os estilos estão prontos para fornecer cor e layout.
Como estilizar o contêiner do botão dividido
Um tipo de exibição inline-flex funciona bem para esse componente de encapsulamento, já que ele
precisa se ajustar em linha com outros botões divididos, ações ou elementos.
.gui-split-button {
  display: inline-flex;
  border-radius: var(--radius);
  background: var(--theme);
  color: var(--ontheme);
  fill: var(--ontheme);
  touch-action: manipulation;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

O estilo <button>
Os botões são muito bons para disfarçar a quantidade de código necessária. Talvez seja necessário desfazer ou substituir os estilos padrão do navegador, mas também será necessário aplicar alguma herança, adicionar estados de interação e se adaptar a várias preferências do usuário e tipos de entrada. Os estilos de botão se acumulam rapidamente.
Esses botões são diferentes dos normais porque compartilham um plano de fundo com um elemento pai. Normalmente, um botão tem a própria cor de plano de fundo e de texto. No entanto, eles compartilham e aplicam apenas o próprio plano de fundo na interação.
.gui-split-button button {
  cursor: pointer;
  appearance: none;
  background: none;
  border: none;
  display: inline-flex;
  align-items: center;
  gap: 1ch;
  white-space: nowrap;
  font-family: inherit;
  font-size: inherit;
  font-weight: 500;
  padding-block: 1.25ch;
  padding-inline: 2.5ch;
  color: var(--ontheme);
  outline-color: var(--theme);
  outline-offset: -5px;
}
Adicione estados de interação com algumas pseudoclasses do CSS e use propriedades personalizadas correspondentes para o estado:
.gui-split-button button {
  …
  &:is(:hover, :focus-visible) {
    background: var(--theme-hover);
    color: var(--ontheme);
    & > svg {
      stroke: currentColor;
      fill: none;
    }
  }
  &:active {
    background: var(--theme-active);
  }
}
O botão principal precisa de alguns estilos especiais para concluir o efeito de design:
.gui-split-button > button {
  border-end-start-radius: var(--radius);
  border-start-start-radius: var(--radius);
  & > svg {
    fill: none;
    stroke: var(--ontheme);
  }
}
Por fim, para dar um toque especial, o botão e o ícone do tema claro recebem uma sombra:
.gui-split-button {
  @media (--light) {
    & > button,
    & button:is(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--theme-active);
    }
    & > .gui-popup-button > svg,
    & button:is(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--theme-active));
    }
  }
}
Um ótimo botão presta atenção às microinterações e aos pequenos detalhes.
Uma observação sobre :focus-visible
Observe como os estilos de botão usam :focus-visible em vez de :focus. :focus
é um toque crucial para criar uma interface do usuário acessível, mas tem uma
desvantagem: não é inteligente sobre se o usuário precisa ver ou
não. Ele será aplicado a qualquer foco.
O vídeo abaixo tenta detalhar essa microinteração para mostrar como o
:focus-visible é uma alternativa inteligente.
Estilizar o botão pop-up
Um flexbox 4ch para centralizar um ícone e ancorar uma lista de botões pop-up. Assim como o botão principal, ele é transparente até que seja passado o cursor ou interagido, e esticado para preencher.

.gui-popup-button {
  inline-size: 4ch;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-inline-start: var(--border);
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
}
Adicione estados de passar o cursor, foco e ativo com CSS
Nesting e o
seletor funcional :is():
.gui-popup-button {
  …
  &:is(:hover,:focus-within) {
    background: var(--theme-hover);
  }
  /* fixes iOS trying to be helpful */
  &:focus {
    outline: none;
  }
  &:active {
    background: var(--theme-active);
  }
}
Esses estilos são o principal gancho para mostrar e ocultar o pop-up. Quando o
.gui-popup-button tiver focus em qualquer um dos filhos, defina opacity, posição
e pointer-events no ícone e no pop-up.
.gui-popup-button {
  …
  &:focus-within {
    & > svg {
      transition-duration: var(--in-speed);
      transform: rotateZ(.5turn);
    }
    & > .gui-popup {
      transition-duration: var(--in-speed);
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }
  }
}
Com os estilos de entrada e saída concluídos, a última parte é fazer a transição condicional das transformações dependendo da preferência de movimento do usuário:
.gui-popup-button {
  …
  @media (--motionOK) {
    & > svg {
      transition: transform var(--out-speed) ease;
    }
    & > .gui-popup {
      transform: translateY(5px);
      transition:
        opacity var(--out-speed) ease,
        transform var(--out-speed) ease;
    }
  }
}
Um olhar atento no código notaria que a opacidade ainda é transferida para usuários que preferem movimento reduzido.
Estilizar o pop-up
O elemento .gui-popup é uma lista de botões de card flutuante que usa propriedades personalizadas
e unidades relativas para ser sutilmente menor, combinada de forma interativa com o botão
principal e alinhada à marca com o uso de cores. Os ícones têm menos contraste, são mais finos e a sombra tem um toque de azul da marca. Assim como com botões, uma interface e uma experiência do usuário fortes são resultado desses pequenos detalhes.

.gui-popup {
  --shadow: 220 70% 15%;
  --shadow-strength: 1%;
  opacity: 0;
  pointer-events: none;
  position: absolute;
  bottom: 80%;
  left: -1.5ch;
  list-style-type: none;
  background: var(--popupbg);
  color: var(--theme-text);
  padding-inline: 0;
  padding-block: .5ch;
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  font-size: .9em;
  transition: opacity var(--out-speed) ease;
  box-shadow:
    0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
    0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
    0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
    0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
    0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
    0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
  ;
}
Os ícones e botões recebem cores da marca para ter um estilo agradável em cada card com tema claro e escuro:

.gui-popup {
  …
  & svg {
    fill: var(--popupbg);
    stroke: var(--theme);
    @media (prefers-color-scheme: dark) {
      stroke: var(--theme-border);
    }
  }
  & button {
    color: var(--theme-text);
    width: 100%;
  }
}
O pop-up do tema escuro tem adições de texto e sombra de ícone, além de uma sombra de caixa um pouco mais intensa:

.gui-popup {
  …
  @media (--dark) {
    --shadow-strength: 5%;
    --shadow: 220 3% 2%;
    & button:not(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--ontheme);
    }
    & button:not(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--ontheme));
    }
  }
}
Estilos de ícones genéricos <svg>
Todos os ícones são dimensionados em relação ao botão font-size em que são usados com a unidade ch como inline-size. Cada um também recebe alguns estilos para ajudar a delinear ícones suaves e
uniformes.
.gui-split-button svg {
  inline-size: 2ch;
  box-sizing: content-box;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2px;
}
Layout da direita para a esquerda
As propriedades lógicas fazem todo o trabalho complexo.
Esta é a lista de propriedades lógicas usadas:
- display: inline-flex cria um elemento flex inline.
- padding-block e padding-inline como um par, em vez do atalho padding, para aproveitar os benefícios do padding nos lados lógicos.
- border-end-start-radius e
  amigos vão
  arredondar cantos com base na direção do documento.
- inline-size em vez de width garante que o tamanho não esteja vinculado a dimensões físicas.
- border-inline-start adiciona uma borda ao início, que pode estar à direita ou à esquerda, dependendo da direção do script.
JavaScript
Quase todo o JavaScript a seguir é para melhorar a acessibilidade. Duas das minhas bibliotecas de ajuda são usadas para facilitar um pouco as tarefas. O BlingBlingJS é usado para consultas DOM concisas e configuração fácil de listeners de eventos, enquanto o roving-ux ajuda a facilitar interações acessíveis de teclado e gamepad para o pop-up.
import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'
const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')
Com as bibliotecas acima importadas e os elementos selecionados e salvos em variáveis, a atualização da experiência está a algumas funções de ser concluída.
Índice móvel
Quando um teclado ou leitor de tela foca o .gui-popup-button, queremos
encaminhar o foco para o primeiro botão (ou o mais recentemente focado) no
.gui-popup. A biblioteca ajuda a fazer isso com os parâmetros element e target.
popupButtons.forEach(element =>
  rovingIndex({
    element,
    target: 'button',
  }))
Agora, o elemento passa o foco para os filhos <button> de destino e ativa
a navegação padrão com as teclas de seta para procurar opções.
Alternando aria-expanded
Embora seja visualmente aparente que um pop-up está aparecendo e desaparecendo, um leitor de tela precisa de mais do que pistas visuais. O JavaScript é usado aqui para complementar a interação :focus-within orientada por CSS alternando um atributo adequado para leitores de tela.
popupButtons.on('focusin', e => {
  e.currentTarget.setAttribute('aria-expanded', true)
})
popupButtons.on('focusout', e => {
  e.currentTarget.setAttribute('aria-expanded', false)
})
Como ativar a chave Escape
O foco do usuário foi enviado intencionalmente para uma armadilha, o que significa que precisamos
oferecer uma maneira de sair. A maneira mais comum é permitir o uso da chave Escape.
Para isso, observe as teclas pressionadas no botão pop-up, já que todos os eventos de teclado nos
filhos vão subir para esse pai.
popupButtons.on('keyup', e => {
  if (e.code === 'Escape')
    e.target.blur()
})
Se o botão pop-up detectar qualquer pressionamento de tecla Escape, ele vai remover o foco de si mesmo com blur().
Cliques no botão de divisão
Por fim, se o usuário clicar, tocar ou interagir com os botões usando o teclado, o
aplicativo precisará realizar a ação adequada. O bubbling de eventos é usado novamente aqui, mas desta vez no contêiner .gui-split-button, para capturar cliques de botão de um pop-up filho ou da ação principal.
splitButtons.on('click', event => {
  if (event.target.nodeName !== 'BUTTON') return
  console.info(event.target.innerText)
})
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.
