Selector de contenido multimedia enriquecido (selección personalizable)

Funciones web: <select> personalizable

Históricamente, el elemento nativo <select> era difícil de diseñar y solo podía contener opciones de texto sin formato. La propiedad appearance: base-select ofrece una forma declarativa, solo de CSS, de habilitar un estado personalizable para el elemento <select>. Esto permite que los desarrolladores incluyan contenido HTML enriquecido, como imágenes, SVG y diseños complejos, dentro de los elementos <option>, a la vez que conservan la accesibilidad nativa del teclado y la integración de formularios. Usa este patrón para reemplazar los componentes de selección pesados y personalizados por elementos nativos estándar.

Cómo implementarla

Para implementar un selector de contenido multimedia enriquecido con la API de Customizable Select, sigue estos pasos:

  1. Habilita los estilos base: Aplica appearance: base-select tanto al elemento <select> como a su selector interno con el pseudoelemento ::picker(select). Esto cambia el analizador de HTML del navegador para el contenido dentro de <select>.
  2. Define el contenido del botón: Usa elementos <button> y <selectedcontent> estándar dentro de <select> para definir lo que se muestra cuando se cierra el selector. El elemento <selectedcontent> refleja automáticamente el contenido de la opción seleccionada. Esto es obligatorio si quieres mostrar el contenido enriquecido seleccionado actualmente en el botón.
  3. Usa contenido enriquecido dentro de las opciones: Ahora puedes colocar imágenes, SVG y otras etiquetas HTML dentro de las etiquetas <option>. Antes de esta API, el navegador quitaba las etiquetas de las etiquetas <option> y renderizaba solo texto sin formato.
  4. Asigna un estilo al cuadro emergente: Asigna un estilo a la lista de opciones desplegable con el pseudoelemento ::picker(select). Se renderiza en la capa superior, lo que significa que no necesitas pelear con z-index. Las opciones se posicionan con la API de Anchor Positioning de forma nativa.

Código de ejemplo: Selector de roles enriquecido

<label for="role-picker">Select your role</label>
<select class="custom-select" name="role" id="role-picker">
  <button>
    <selectedcontent></selectedcontent> <!-- Mirrors the selected option's content automatically so you do not need JS to update the button -->
  </button>

  <!-- Define concise aria-label values on options whose mirrored rich content would otherwise read awkwardly as a concatenated string -->
  <option value="frontend" aria-label="Frontend Developer">
    <svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
      <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
    </svg>
    <div class="option-text">
      <span class="option-title">Frontend Developer</span>
      <span class="option-desc">React, Vue, CSS</span>
    </div>
  </option>

  <option value="backend" aria-label="Backend Developer">
    <svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
      <rect x="2" y="12" width="20" height="14" rx="2" ry="2"></rect>
    </svg>
    <div class="option-text">
      <span class="option-title">Backend Developer</span>
      <span class="option-desc">Node.js, Python</span>
    </div>
  </option>
</select>
select.custom-select,
select.custom-select::picker(select) {
  appearance: base-select; /* MUST opt-in both the select and its picker to enable the customizable state, otherwise browser standard rendering applies */
}

select.custom-select {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 12px;
  background-color: #1e293b;
  border: 1px solid #334155;
  border-radius: 8px;
  color: #f1f5f9;
  cursor: pointer;
}

select.custom-select::picker(select) {
  background-color: #0f172a;
  border: 1px solid #334155;
  border-radius: 8px;
  padding: 8px;
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
  width: anchor-size(width); /* Uses Anchor Positioning API to keep the dropdown precisely aligned to the button trigger width */
}

select.custom-select option {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px;
  border-radius: 4px;
  cursor: pointer;
}

select.custom-select option:hover {
  background-color: #1e293b;
}

/* Remove standard OS checkmark for base-select */
select.custom-select option::before {
  display: none;
}

/* MANDATORY: Provide multiple visual indicators (e.g., prominent background color and bold title font) to communicate the checked state cleanly */
select.custom-select option:checked {
  background-color: #3b82f6;
  color: #ffffff;
}
select.custom-select option:checked .option-title {
  font-weight: 700;
}

Implementación estratégica y prácticas recomendadas

  • SÍ usa appearance: base-select cuando necesites diseños de opciones complejos (íconos, descripciones, imágenes) que antes solo eran posibles con frameworks de IU de JavaScript pesados.
  • NO uses elementos ad hoc si notas retrasos en el rendimiento; el navegador controla el enfoque del teclado nativo de forma nativa.
  • SÍ se debe tener en cuenta la renderización de la capa superior. El selector se renderiza en la capa superior, lo que significa que anula el z-index relativo del contenido de la página.
  • SÍ oculta los detalles secundarios (como las descripciones) en el estado del botón si ocupan demasiado espacio. Para ello, aplica diseño a .custom-select selectedcontent .option-desc { display: none; }.
  • 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.

Estrategias 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.
  }
});