Descripción general de los conceptos básicos para compilar un componente de botón dividido accesible
En esta publicación, quiero compartir ideas sobre cómo crear un botón dividido . Prueba la demostración.
Si prefieres ver un video, aquí tienes una versión de YouTube de esta publicación:
Descripción general
Los botones de división son botones que ocultan un botón principal y una lista de botones adicionales. Son útiles para exponer una acción común mientras se anida el secundario, de uso menos frecuente acciones hasta que sea necesario. Un botón de división puede ser crucial para ayudar a un diseño atareado parezca mínima. Un botón de división avanzado incluso puede recordar la última acción del usuario y ascenderla a una posición principal.
Puedes encontrar un botón de división común en tu aplicación de correo electrónico. La acción principal se envió, pero tal vez puedas enviarlo más tarde o guardar un borrador:
El área de acción compartida es agradable, ya que el usuario no necesita mirar a su alrededor. Ellas sepa que las acciones esenciales de correo electrónico están incluidas en el botón de división.
Piezas
Vamos a desglosar las partes esenciales de un botón de división antes de analizar su la organización general y la experiencia final del usuario. Accesibilidad de VisBug la herramienta de inspección se usa aquí para mostrar una vista macro del componente, en la que se muestra aspectos del HTML, el estilo y la accesibilidad de cada parte principal.
Contenedor del botón de división de nivel superior
El componente de mayor nivel es una flexbox integrada, con una clase de
gui-split-button
, que contiene la acción principal
y el .gui-popup-button
.
El botón de acción principal
El elemento <button>
visible y enfocable inicialmente cabe dentro del contenedor con
dos formas de esquinas coincidentes para
enfoque,
colocar el cursor y
las interacciones activas para
aparezcan dentro de .gui-split-button
.
El botón de activación de la ventana emergente
El “botón emergente” de asistencia es para activar y hacer referencia a la lista de
botones secundarios. Ten en cuenta que no es <button>
ni enfocable. Sin embargo,
Es el anclaje de posicionamiento de .gui-popup
y el host de :focus-within
que se usa
para presentar la ventana emergente.
La tarjeta emergente
Esta es una tarjeta flotante secundaria a su ancla
.gui-popup-button
, posicionadas como valores absolutos y
uniendo semánticamente la lista de botones.
Las acciones secundarias
Un objeto <button>
enfocable con un tamaño de fuente un poco más pequeño que la primaria
botón de acción cuenta con un ícono y un
estilo al botón principal.
Propiedades personalizadas
Las siguientes variables ayudan a crear una armonía de colores y un lugar central para y modificar los valores que se usan en todo el 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%);
}
}
Diseños y color
Marca
El elemento comienza como una <div>
con un nombre de clase personalizado.
<div class="gui-split-button"></div>
Agrega el botón principal y los 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>
Observa los atributos de ARIA aria-haspopup
y aria-expanded
. Estas señales son
es fundamental que los lectores de pantalla conozcan la capacidad y el estado de la división
experiencia de botones. El atributo title
es útil para todos.
Agrega un ícono <svg>
y el elemento contenedor .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 ubicar una ventana emergente de forma directa, .gui-popup
es un elemento secundario del botón que
lo expande. El único atractivo de esta estrategia es el .gui-split-button
.
no puede usar overflow: hidden
, ya que recortará la ventana emergente para que no se muestre
presentes visualmente.
Un <ul>
lleno con contenido de <li><button>
se anunciará como un "botón
lista" a los lectores de pantalla, que es precisamente la interfaz que se presenta.
<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 darle estilo y para divertirme con los colores, agregué iconos a los botones secundarios de https://heroicons.com. El uso de íconos es opcional en ambos casos los botones principal y secundario.
<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
Con HTML y contenido en su lugar, los estilos están listos para proporcionar color y diseño.
Cómo aplicar diseño al contenedor del botón de división
Un tipo de visualización inline-flex
funciona bien para este componente de unión, ya que
debe encajar en línea con otros botones, acciones o elementos divididos.
.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;
}
El estilo <button>
Los botones son muy buenos para ocultar la cantidad de código que se necesita. Es posible que debas deshacer o reemplazar los estilos predeterminados del navegador, pero también deberás aplicar algunos la herencia, agregar estados de interacción y adaptarse a diversas preferencias del usuario y tipos de entrada. Los estilos de botones se suman rápidamente.
Estos botones son diferentes de los botones normales porque comparten un fondo. con un elemento superior. Por lo general, un botón posee el color de fondo y del texto. Sin embargo, los comparten y solo aplican sus propios antecedentes sobre la interacción.
.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;
}
Agrega estados de interacción con algunos CSS pseudoclases y el uso de coincidencias propiedades personalizadas para el 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);
}
}
El botón principal necesita algunos estilos especiales para completar el efecto de diseño:
.gui-split-button > button {
border-end-start-radius: var(--radius);
border-start-start-radius: var(--radius);
& > svg {
fill: none;
stroke: var(--ontheme);
}
}
Por último, para darle estilo, el botón y el ícono del tema claro tienen una 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));
}
}
}
Un buen botón ha prestado atención a las microinteracciones y a los pequeños detalles.
Nota sobre :focus-visible
Observa cómo los estilos de los botones usan :focus-visible
en lugar de :focus
. :focus
es un toque crucial para hacer que una interfaz de usuario sea accesible, pero sí
no es inteligente respecto de si el usuario necesita verlo o no
se aplicará a cualquier enfoque.
En el siguiente video, se intenta desglosar esta microinteracción para mostrar cómo
:focus-visible
es una alternativa inteligente.
Cómo definir el diseño del botón emergente
Un flexbox 4ch
para centrar un ícono y anclar una lista de botones emergentes. Me gusta
el botón principal, es transparente hasta que se coloca el cursor sobre el botón o se interactúa con él
y estirado para llenar.
.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);
}
Aplica capas en los estados activos, de enfoque y de colocar el cursor sobre ellos con CSS.
Período de prueba y
Selector 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);
}
}
Estos estilos son el gancho principal para mostrar y ocultar la ventana emergente. Cuando
.gui-popup-button
tiene focus
en cualquiera de sus elementos secundarios, establece opacity
, posición
y pointer-events
, en el ícono y la ventana emergente.
.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;
}
}
}
Una vez completados los estilos de entrada y salida, la última pieza es condicionalmente transformaciones de transición según la preferencia de movimiento del usuario:
.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;
}
}
}
Si se presta atención al código, se nota que la opacidad todavía está en transición para los usuarios. que prefieren menos movimiento.
Cómo aplicar diseño a la ventana emergente
El elemento .gui-popup
es una lista de botones de tarjeta flotante que usa propiedades personalizadas
y las unidades relativas sean un poco más pequeñas y se combinen de forma interactiva con el
y en la marca con su uso del color. Observa que los iconos tienen menos contraste,
son más finas, y la sombra tiene un toque de marca azul. Al igual que con los botones,
IU y UX sólidas es el resultado de la acumulación de estos pequeños detalles.
.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%))
;
}
Los íconos y los botones reciben colores de la marca para crear un estilo agradable en cada oscuridad. y la tarjeta con tema claro:
.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%;
}
}
La ventana emergente del tema oscuro tiene adiciones de sombra de íconos y texto, además de otras sombras Sombra de cuadro 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 ícono <svg>
genéricos
Todos los íconos tienen un tamaño relativo respecto del botón font-size
en el que se usan
usando la unidad ch
como
inline-size
A cada uno también se le dan algunos estilos para ayudar a delinear los íconos suaves y
sin problemas.
.gui-split-button svg {
inline-size: 2ch;
box-sizing: content-box;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2px;
}
Diseño de derecha a izquierda
Las propiedades lógicas hacen todo el trabajo complejo.
Esta es la lista de propiedades lógicas usadas:
- display: inline-flex
crea un elemento flexible intercalado.
- padding-block
y padding-inline
como un par, en lugar de padding
abreviaremos, obtendrás los beneficios del relleno los lados lógicos.
- border-end-start-radius
y
amigos hará
redondear las esquinas según la dirección del documento.
- inline-size
en lugar de width
garantiza que el tamaño no esté vinculado a dimensiones físicas.
- border-inline-start
agrega un borde al inicio, que puede estar a la derecha o a la izquierda, según la dirección de la secuencia de comandos.
JavaScript
Casi todo el siguiente código JavaScript se usa para mejorar la accesibilidad. Dos de mis se usan bibliotecas auxiliares para facilitar un poco las tareas. BlingBlingJS se usa para texto las consultas del DOM y la configuración sencilla de receptores de eventos, mientras roving-ux ayuda a facilitar el acceso las interacciones del teclado y el control de mando para la ventana emergente.
import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'
const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')
Con las bibliotecas anteriores importadas y los elementos seleccionados y guardados en variables, actualizar la experiencia está a unas pocas funciones de estar completa.
Índice itinerante
Cuando un teclado o un lector de pantalla enfoca el .gui-popup-button
, queremos
reenviar el enfoque hacia el primer botón (o en el que se enfocó más recientemente) en la
.gui-popup
La biblioteca nos ayuda a hacerlo con element
y target
.
parámetros.
popupButtons.forEach(element =>
rovingIndex({
element,
target: 'button',
}))
El elemento ahora pasa el enfoque a los elementos secundarios <button>
de destino y permite
navegación estándar con las teclas de flecha para navegar por las opciones
Activando o desactivando aria-expanded
Si bien es visualmente evidente que una ventana emergente se muestra y se oculta, el lector de pantalla necesita más que señales visuales. En este caso, se usa JavaScript para complementar la interacción de :focus-within
impulsada por CSS mediante la activación de un atributo adecuado para el lector de pantalla.
popupButtons.on('focusin', e => {
e.currentTarget.setAttribute('aria-expanded', true)
})
popupButtons.on('focusout', e => {
e.currentTarget.setAttribute('aria-expanded', false)
})
Habilita la tecla Escape
El foco del usuario se ha enviado intencionalmente a una trampa, lo que significa que debemos
proporcionan una manera de salir. La forma más común es permitir el uso de la clave Escape
.
Para ello, presta atención a las pulsaciones de teclas en el botón emergente, ya que los eventos del teclado
los niños se presentarán ante este padre.
popupButtons.on('keyup', e => {
if (e.code === 'Escape')
e.target.blur()
})
Si el botón emergente ve que se presiona la tecla Escape
, quita el foco
con
blur()
Divide los clics en el botón
Por último, si el usuario hace clic en los botones, los presiona o el teclado interactúa con ellos, la
la aplicación necesita realizar la acción adecuada. Se usa la burbujas de eventos
pero esta vez en el contenedor .gui-split-button
,
clics provenientes de una ventana emergente secundaria o de la acción principal.
splitButtons.on('click', event => {
if (event.target.nodeName !== 'BUTTON') return
console.info(event.target.innerText)
})
Conclusión
Ahora que sabes cómo lo hice, ¿cómo lo harías?‽ 🙂
Diversifiquemos nuestros enfoques y aprendamos todas las formas de desarrollar en la Web. Crear una demostración, twittearme vínculos y la agregaré. a la sección de remixes de la comunidad.