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.