12k
All articles

Cómo dar estilo a un desplegable select con base-select

Estiliza menús desplegables select nativos con appearance: base-select. Explora el picker, las opciones, los iconos, la accesibilidad, la compatibilidad y sus límites.

OpenReplay Team
OpenReplay Team
Cómo dar estilo a un desplegable select con base-select

Aplicar appearance: base-select a un <select> y a su pseudoelemento ::picker(select) permite dar estilo a todo el control con CSS: el botón, el desplegable, la flecha, la marca de verificación y cada una de las opciones.

Muchos equipos mantienen un componente de desplegable personalizado por una única razón: el <select> nativo no se podía estilizar para que coincidiera con el diseño. La gestión del teclado, el manejo del foco y la configuración de ARIA llegaron como efecto secundario. appearance: base-select elimina esa razón.

Esta guía recorre todas las posibilidades de estilo, desde la activación hasta las opciones enriquecidas, y explica a qué se renuncia. Para las técnicas clásicas con appearance: none, styling select elements with modern CSS ofrece una visión más amplia, aunque solo menciona base-select brevemente. Los enfoques más antiguos se tratan en styling select dropdowns without JavaScript.

Puntos clave

  • appearance: base-select debe aplicarse tanto a select como a ::picker(select). Aplicarlo solo al picker no tiene ningún efecto.
  • El picker es un popover en la capa superior (top layer), anclado al botón del select mediante una referencia de anclaje implícita, por lo que no se necesitan z-index, portales ni anchor-name.
  • <selectedcontent> va dentro de un <button> que es el primer hijo del <select>, y muestra un clon de la opción seleccionada en el control cerrado.
  • Todas las opciones deben contener texto, ya que las opciones que solo tienen un icono no ofrecen nada que anunciar a los lectores de pantalla y aparecen vacías en los navegadores sin soporte.
  • Con base-select se renuncia al selector nativo del sistema operativo en móviles, a que el desplegable se muestre fuera del área del navegador y al ajuste automático del ancho según la opción más larga.

El componente que puedes eliminar

Un componente select personalizado suele existir únicamente para corregir la apariencia del elemento <select> nativo y, al hacerlo, asume una gran cantidad de comportamiento. La publicación de lanzamiento del equipo de Chrome indica que a partir de Chrome 135 es posible dar estilo al elemento <select> con CSS, conservando la accesibilidad y el comportamiento estándar de un control integrado. WebKit le siguió en Safari 27.0, donde el <select> real ahora puede reestilizarse de arriba abajo. Una vez que un navegador lo admite, desaparece el principal motivo para mantener un listbox basado en divs, y con él todo el comportamiento que había que reconstruir para sostenerlo.

¿Cómo se activa appearance: base-select?

Para activarlo, aplica appearance: base-select tanto al <select> como a su picker. La guía de select personalizable de MDN explica que el select puede activarse por sí solo, pero el picker solo funciona cuando el select también está activado. Las recomendaciones de WebKit sobre el select personalizable aconsejan envolver estas mejoras en @supports (appearance: base-select). Mantén todas las reglas posteriores dentro de ese mismo bloque, para que los navegadores sin soporte muestren el select normal.

@supports (appearance: base-select) {
  select,
  ::picker(select) {
    appearance: base-select;
  }
  /* all following rules go here */
}
ParteSelectorNotas
Control cerradoselectEl botón del select
Flecha::picker-iconNo forma parte del árbol de accesibilidad
Estado abierto:openCoincide mientras se muestra el picker
Desplegable::picker(select)Todo excepto el botón que es primer hijo
Cada opciónoptiondisplay: flex por defecto
Opción seleccionadaoption:checked
Marca de selección::checkmarkNo forma parte del árbol de accesibilidad
Contenido en estado cerradoselectedcontentClon de la opción seleccionada

Estilizar el control cerrado

El control cerrado es el elemento select. La flecha es ::picker-icon, y :open coincide mientras se muestra el picker, por lo que puedes rotar la flecha cuando el select se abre.

select {
  border: 2px solid #d4d4d8;
  background: #f4f4f5;
  padding: 10px;
  transition: 0.4s;
}

select:hover,
select:focus {
  background: #e4e4e7;
}

select::picker-icon {
  color: #71717a;
  transition: 0.4s rotate;
}

select:open::picker-icon {
  rotate: 180deg;
}

La transition sobre rotate hace que la flecha gire suavemente cada vez que el picker se abre y se cierra.

Estilizar la lista abierta

La lista desplegable abierta de un select personalizable es ::picker(select). El navegador agrupa en ella todo lo que hay dentro del <select>, salvo el botón que aparece en primer lugar. Las opciones son contenedores flex por defecto. :checked apunta a la opción seleccionada y ::checkmark a la marca que aparece junto a ella.

::picker(select) {
  border: none;
  border-radius: 8px;
}

option {
  display: flex;
  gap: 16px;
  padding: 10px;
}

option:hover,
option:focus {
  background: #ede9fe;
}

option:checked {
  font-weight: bold;
}

option::checkmark {
  order: 1;
  margin-left: auto;
  content: "✔";
}

Establecer order: 1 junto con margin-left: auto desplaza la marca de verificación del inicio al final de la fila.

Posicionamiento y animación

En un select personalizable, el desplegable es un popover que se abre en la capa superior. El navegador lo vincula por sí mismo al botón del select mediante CSS anchor positioning, por lo que no necesitas z-index, portales ni configurar anchor-name. El botón y el picker mantienen la misma relación invocador/popover que la guía de invoker commands describe para botones y popovers. Los estilos por defecto también reubican el picker en otro lado si fuera a salirse de la pantalla, y puedes definir tu propia posición con anchor():

::picker(select) {
  top: calc(anchor(bottom) + 1px);
  left: anchor(10%);
}

Para que el picker aparezca y desaparezca con un fundido, aplica una transición entre sus estados de popover:

::picker(select) {
  opacity: 0;
  transition: all 0.4s allow-discrete;
}

:open::picker(select) {
  opacity: 1;
}

@starting-style {
  :open::picker(select) {
    opacity: 0;
  }
}

allow-discrete permite que las propiedades discretas display y overlay participen en la transición. Sin ella, el picker abandonaría la capa superior antes de que se pudiera ver el fundido de salida. @starting-style define la opacidad desde la que comienza el fundido de entrada.

Opciones enriquecidas y selectedcontent

Coloca <selectedcontent> dentro de un <button> que sea el primer hijo del <select>. Este elemento muestra un clon del contenido de la opción seleccionada en el control cerrado, y puedes darle estilo de forma independiente a las opciones del desplegable. Las opciones ahora pueden contener marcado, como iconos e imágenes.

<label for="status">Status</label>
<select id="status" name="status">
  <button>
    <selectedcontent></selectedcontent>
  </button>
  <option value="draft">
    <span class="icon" aria-hidden="true">📝</span>
    <span class="option-label">Draft</span>
  </option>
  <option value="review">
    <img class="icon" src="review.svg" alt="">
    <span class="option-label">In review</span>
  </option>
  <option value="published">
    <span class="icon" aria-hidden="true">✅</span>
    <span class="option-label">Published</span>
  </option>
</select>

MDN señala que los iconos pueden hacer que el botón cerrado sea más alto y desplazar la flecha de su sitio. Puedes ocultarlos solo en el estado cerrado:

selectedcontent .icon {
  display: none;
}

Las opciones del picker abierto conservan sus iconos. El botón del select es inerte, por lo que los enlaces o botones colocados en su interior no pueden recibir el foco ni pulsarse por separado: todo el conjunto actúa como un único botón. MDN también advierte de que el contenido adicional en el botón puede cambiar lo que los lectores de pantalla anuncian como valor del select, así que limita el contenido del botón a <selectedcontent>.

¿Qué se encarga de gestionar el navegador?

Con base-select, el navegador gestiona todo aquello que un desplegable personalizado tiene que reimplementar: la interacción por teclado, el foco, el apilamiento en la capa superior, el envío del valor en el formulario y la semántica para lectores de pantalla. WebKit afirma que el soporte de teclado y la accesibilidad vienen integrados, porque sigue siendo el control propio del navegador. Reproducir sesiones de usuario sobre un desplegable basado en divs suele sacar a la luz errores de teclado y de foco, precisamente los errores que un select nativo evita por diseño. Para hacerte una idea de cuánto código requiere el enfoque manual, consulta esta guía para construir a mano un desplegable select accesible.

Aun así, se mantienen dos reglas:

  • Incluye texto en todas las opciones. La regla principal de WebKit es que cada opción necesita texto real o un atributo de texto accesible. El texto que ocultas visualmente sigue siendo leído por los lectores de pantalla. Las imágenes decorativas llevan alt="" y dejan que la etiqueta de texto transmita el significado.
  • Trata los iconos personalizados como decoración. Los lectores de pantalla ignoran todo lo que añadas con content en ::checkmark y ::picker-icon, por lo que una flecha o marca de verificación personalizada nunca se lee en voz alta.

El código JavaScript existente que lee el valor del select sigue funcionando. Si una opción no tiene atributo value, el valor del select es el textContent de la opción seleccionada sin los espacios en blanco iniciales y finales, así que define siempre value cuando las opciones contengan marcado enriquecido.

Soporte, fallback y compromisos

appearance: base-select está disponible en Chrome 135 y posteriores, en otros navegadores basados en Chromium como Edge y en Safari 27.0. Firefox ha empezado a incorporarlo tras una preferencia: el valor appearance: base-select llegó a Firefox 149 tras un flag, pero no está activado por defecto. MDN todavía lo marca como de disponibilidad limitada, así que consulta las tablas de compatibilidad en las páginas de referencia de MDN. Safari incluye sus propios estilos base-select por defecto, por lo que los controles sin estilos no se verán igual en todos los navegadores.

Los navegadores sin soporte para base-select ignoran el marcado de <button> y <selectedcontent>. Renderizan un select normal que funciona con normalidad, con cada opción reducida a su texto. Ese fallback solo tiene sentido si el texto está presente.

La publicación de Chrome también enumera tres comportamientos que se pierden:

  • La lista abierta permanece dentro de la ventana del navegador.
  • En teléfonos y tabletas, se obtiene el picker del navegador en lugar del propio del sistema operativo.
  • El ancho del select ya no se ajusta a su opción más larga, por lo que debes definir tú mismo un width en select, ::picker(select).

Base-select también cambia la forma en que el parser de HTML interpreta el contenido de un <select>. MDN advierte de que esto puede entrar en conflicto con frameworks de JavaScript: algunos bloquean estas funcionalidades y, en otros, el renderizado del lado del servidor puede romper la hidratación. Prueba la funcionalidad en tu framework antes de eliminar tu componente existente.

Conclusión

Un select nativo con appearance: base-select te da control sobre los estilos, mientras el navegador sigue gestionando el teclado, el foco, el apilamiento y la accesibilidad. Para probarlo, elige un desplegable personalizado sencillo, reconstrúyelo dentro de un bloque @supports con texto en todas las opciones y compruébalo en un navegador que no admita base-select. Si allí sigue funcionando como un select normal y los compromisos descritos son aceptables, puedes eliminar el componente personalizado.

Preguntas frecuentes

¿Cuál es la diferencia entre appearance: none y appearance: base-select en un select?

appearance: none solo elimina el estilo nativo del botón cerrado. La lista desplegable y sus opciones siguen usando el picker del navegador o del sistema operativo, y la mayor parte del CSS no llega a ellos. appearance: base-select, aplicado tanto a select como a ::picker(select), permite además dar estilo al picker, las opciones, la flecha y la marca de verificación, y admite HTML enriquecido dentro de las opciones. Los navegadores sin soporte lo ignoran y mantienen el control clásico.

¿Cómo detecto el soporte de base-select en JavaScript?

Llama a CSS.supports('appearance', 'base-select'). Devuelve true solo en los navegadores que reconocen ese valor. Comprueba lo mismo que la condición CSS @supports (appearance: base-select), por lo que puedes usarlo para decidir si cargar un componente de desplegable alternativo o añadir una clase que active los estilos personalizados. Utiliza la detección de características en lugar de analizar el user agent, ya que cada motor incorporó el soporte en un momento distinto.

¿Funciona base-select con select multiple o con el atributo size?

Sí, en modo listbox. El atributo multiple, o un valor de size mayor que 1, muestra las opciones como un cuadro en la página en lugar de un botón que abre un popup. A partir de Chrome 145, base-select también funciona en este modo, tanto para selección simple como múltiple. Todavía no cubre el popup de selección múltiple que se obtiene con multiple y size=1. Un listbox personalizable no tiene picker, por lo que ::picker(select), ::picker-icon y selectedcontent no se aplican. Consulta las tablas de compatibilidad de MDN para conocer el soporte en otros motores.

Digital experience platform

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.