Cómo compilar un componente de botón dividido

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.

. 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:

Un ejemplo de botón de división tal como se ve en una aplicación de correo electrónico.

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.

Los elementos HTML que conforman el botón de división.

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.

Se inspecciona la clase gui-split-button y se muestran las propiedades de CSS que se usan en esta clase.

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 inspector muestra las reglas de CSS para el elemento del botón.

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.

El inspector muestra las reglas de CSS para la clase gui-popup-button.

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.

El inspector muestra las reglas de CSS para la clase gui-popup.

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.

El inspector muestra las reglas de CSS para el elemento del botón.

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 botón de división.

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.

La parte de la flecha del botón de división que se usa para activar la ventana emergente.

.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.

Un elemento de tarjeta flotante.

.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:

Vínculos e íconos de confirmación de la compra, Pago rápido y Guardar para más tarde.

.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:

Ventana emergente en el tema oscuro.

.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.

Remixes de la comunidad