Diseños personalizados de selectores de selección

Funciones web: <select> personalizable

La propiedad de CSS appearance: base-select desbloquea la capacidad de aplicar diseño a un elemento <select> y a su selector (a través de ::picker(select)) como elementos HTML normales. En combinación con las técnicas modernas de diseño de CSS, como Grid o Flexbox, puedes salir de la lista vertical apilada tradicional y crear una experiencia visual enriquecida sin JS personalizado. Por ejemplo, puedes usar display: grid para aplicar diseño a las opciones como una cuadrícula de tarjetas en 2D.

Esto es ideal para selectores de color, selectores de emojis o variantes de productos en los que un menú visual enriquecido es más eficaz que una lista.

Cómo implementarla

1. Activa el diseño de base

Aplica appearance: base-select al elemento <select> y a su pseudoelemento selector interno select::picker(select):

select.grid-picker {
  &, &::picker(select) {
    appearance: base-select;
  }
}

2. Asigna un estilo al contenedor del selector

Segmenta select:open::picker(select) y aplica display: grid (o display: flex). Define las columnas y los espacios como lo harías con cualquier contenedor.

select.grid-picker:open::picker(select) {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
}

IMPORTANTE: El :open es necesario. Si aplicas diseño a select::picker(select), el selector será visible incluso cuando el elemento select esté cerrado.

3. Cómo aplicar diseño a las opciones

Orienta los elementos <option> para darles estilo como elementos de cuadrícula o tarjetas. Puedes agregar imágenes, SVG o diseños complejos dentro de ellos.

/* Style options as grid cards */
.grid-picker option {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* padding, border-radius, background, etc. */
}

4. Cómo aplicar diseño a la opción seleccionada

Para aplicar diseño a la opción seleccionada, usa option:checked. Por ejemplo:

.grid-picker option:checked {
  position: relative;
  border: 2px solid var(--primary-color);
  background-color: oklab(from var(--primary-color) l a b / 8%);
}

Al igual que con cualquier control de la IU, no comuniques el estado solo con el color y usa varios indicadores visuales (p.ej., colores, grosor del borde, marca de verificación) para comunicar qué opción está seleccionada.

5. (Opcional) Aplica un estilo a la marca de verificación de la opción seleccionada

De forma predeterminada, el navegador renderiza una marca de verificación como el primer elemento secundario de cada <option>.

Se comporta como un ::before con content: "✓" / "", que se renderiza antes de un ::before proporcionado por el autor, si existe.

Si no se selecciona la opción, la marca de verificación se oculta a través de visibility: hidden.

Puedes aplicarle diseño con el seudoelemento ::checkmark o, si quieres transmitir la selección solo con el diseño o usar un indicador personalizado, puedes ocultarlo por completo.

Por ejemplo, para colocarlo en la esquina superior derecha de la opción, puedes hacer lo siguiente:

select.grid-picker {
  > option::checkmark {
    /* Hide the checkmark for unselected options */
    display: none;
  }

  > option:checked {
    position: relative;

    &::checkmark {
      position: absolute;
      inset-block-start: -.4em;
      inset-inline-end: -.5em;
      display: flex;
      align-items: center;
      justify-content: center;
      box-sizing: border-box;
      min-inline-size: 1.6em;
      aspect-ratio: 1;
      padding-right: 0.12em;
      padding-left: 0.05em;
      border-radius: 50%;
      background: var(--primary-color);
      color: white;
      font-size: 70%;
      font-weight: bold;
    }
  }
}

Sugerencia: Cuando pintes un contenedor (p.ej., un círculo) alrededor de la marca de verificación, muévelo un poco hacia la parte superior (como hicimos aquí con padding-bottom y padding-left) para lograr un centrado óptico que tenga en cuenta su forma. NO uses padding-inline-start ni padding-block-end para esto, ya que cambiarán según el modo de escritura, mientras que la forma de la marca de verificación no lo hará.

6. Personaliza la visualización de la opción seleccionada (opcional)

De forma predeterminada, el <select> mostrará una versión de solo texto de la opción seleccionada. Para mostrar contenido enriquecido para el valor seleccionado, usa <selectedcontent> dentro de un <button> o para mostrar la opción seleccionada de manera diferente (p.ej., ocultar ciertos detalles).

NO uses <selectedcontent> sin <button> alrededor, ya que <selectedcontent> se convertirá en parte del selector, como cualquier otro elemento de contenido.

Prácticas recomendadas

  • Prueba el comportamiento del diseño. Si configuras appearance: base-select, se quita el comportamiento predeterminado del navegador de ajustar el tamaño del elemento select en función del ancho de la opción más larga. Es posible que debas establecer un ancho fijo o usar restricciones de flex/grid para evitar cambios en el diseño.
  • Asegúrate de que tu <select> tenga un atributo name y un <label> asociado. Esto garantiza que, incluso con una IU personalizada, el componente siga siendo accesible para los lectores de pantalla y funcione correctamente con los envíos de formularios estándar.
  • Usa atributos value estándar en las opciones para asegurarte de que el envío del formulario funcione exactamente como antes.

Accesibilidad

Un <select> nativo solo controla la navegación lineal con las flechas hacia arriba y abajo, por lo que las flechas hacia la izquierda y derecha no hacen nada en el selector abierto, y las flechas hacia arriba y abajo siguen navegando por los elementos de forma secuencial, independientemente de las filas y columnas.

Para implementar la navegación espacial que sigue la cuadrícula, puedes interceptar keydown y enrutar el enfoque a la opción más cercana en la dirección presionada (p.ej., con getBoundingClientRect()) cuando se usan las teclas de flecha. Para determinar el elemento siguiente o anterior adecuado, usa métodos del DOM que operen en la geometría real (p.ej., getBoundingClientRect()) en lugar de analizar cadenas de CSS como grid-template-columns o grid-column, que pueden ser frágiles y propensas a errores en cuadrículas complejas, subcuadrículas, RTL y otras situaciones de diseño. Dicho esto, no realices un diseño excesivo innecesario; usa las invariantes que sepas que son verdaderas sobre el diseño para simplificar esta lógica.

Nota: Esto solo aborda la navegación con el teclado. El <select> y su ::picker() aún tienen sus roles predeterminados (menu o listbox), que no se pueden anular ni pueden contener un elemento con role="grid". Anidar un wrapper con role="grid" dentro del selector funciona en la práctica, pero no es válido y puede sorprender a los usuarios de TA. En algunos casos, usar <optgroup> para agrupar opciones puede producir una buena UX del lector de pantalla sin role="grid". De lo contrario, es posible que, en última instancia, sea necesario cambiar a una solución personalizada con la API de popover, el posicionamiento de anclaje y role="grid" para lograr la UX deseada para los usuarios de lectores de pantalla.

Estrategia de resguardo

Resguardos y compatibilidad de navegadores para <select> personalizable

En el caso de los navegadores que aún no admiten appearance: base-select, el elemento <select> se degrada de forma elegante a un menú desplegable estándar del sistema operativo.

  • Se ignora el contenido que no es de texto: Los navegadores más antiguos quitan las etiquetas HTML (como <svg> o <div>) dentro de las etiquetas <option> y renderizan solo los nodos de texto. Asegúrate de que el contenido de texto de <option> sea legible y significativo por sí solo.
  • Manejo de la estructura HTML: Los analizadores estándar pueden ignorar las etiquetas <button> y <selectedcontent> dentro de <select> o tratarlas como no válidas. No se requieren polyfills de JavaScript pesados para la mejora progresiva si ves el texto estándar como un resguardo legible.
document.addEventListener("DOMContentLoaded", () => {
  // Check if browser supports base-select value
  if (!CSS.supports("appearance", "base-select")) {
    // Custom select overrides are not supported natively.
  }
});