Cambiar estilos de un select con CSS: la guía real (2026)

Cambiar estilos de un select con CSS ha sido durante quince años el trabajo más frustrante de cualquier formulario. Le pones un borde bonito, y la flecha sigue siendo la del sistema. Intentas darle color a las opciones, y en Mac no pasa nada. Esa era la realidad, y desde 2025 ha cambiado de verdad.

Empiezo reconociendo algo: la versión anterior de este artículo, de 2013, enseñaba a poner la flecha con background: url(flecha.png) y llevaba dentro un comentario admitiendo que no funcionaba. Tenía razón quien lo avisó. Faltaba la pieza que lo hace funcionar, y aquí está explicada.

Te cuento las tres capas: qué se puede estilar de verdad y qué no, la receta clásica que funciona en todos los navegadores, y el modo nuevo que por fin te deja personalizar el desplegable entero.

Qué estilos de un select se pueden cambiar

Antes de tocar los estilos de un select conviene entender por qué se resiste. Un select no es una caja normal: sus tripas las pinta el sistema operativo, no el navegador. El desplegable que se abre al pulsar es un widget nativo, y el CSS no llega ahí.

De ahí sale este reparto de qué estilos de un select puedes cambiar y cuáles no, que es el resumen honesto del modo clásico:

Qué quieres estilar¿Se puede?
La caja cerrada del select (borde, fondo, tipografía, padding)Sí, sin problema
La flechaSí, pero hay que quitar la nativa primero
La caja del desplegable abiertoNo
Espaciado o altura de las filas del optionNo
Color y fondo de cada optionA veces, y solo en Chromium
Borde o esquinas redondeadas en un optionNo
Cualquier cosa del option en SafariNo

Ese «no» de la caja del desplegable es el que manda. Si tu diseño exige un desplegable con esquinas redondeadas, sombra y filas con icono, en el modo clásico no hay CSS que lo consiga. Ahí es donde entra el modo nuevo, más abajo.

Método clásico: appearance none y flecha propia

Esta receta para cambiar los estilos de un select funciona hoy en todos los navegadores y debe ir de base en cualquier proyecto. La pieza que faltaba en la versión antigua de este artículo es appearance: none.

appearance es la propiedad que existe precisamente para decidir cuánto estilo del sistema operativo se aplica a un control de formulario. Con el valor none le quitas al select su apariencia nativa, incluida la flecha. Sin esta línea, tu flecha se dibuja debajo de la del sistema y se ven las dos. Ese era el bug exactamente.

select {
  appearance: none;
  -webkit-appearance: none; /* por si soportas Safari antiguo */

  display: block;
  width: 100%;
  font-family: inherit;
  font-size: 100%;
  padding: 8px 2.2em 8px 12px; /* hueco a la derecha para la flecha */
  border: 1px solid #333;
  border-radius: 6px;
  background-color: #fff;

  /* La flecha, como SVG en data URI */
  background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolygon points='5,5 15,5 10,15' fill='%23333'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 1em 1em;
}

Dos detalles de sintaxis en los que casi todo el mundo pierde media hora. Dentro de url() hay que escapar los signos de menor y mayor como %3C y %3E, y usar comillas simples en los atributos del SVG. Y el SVG necesita su xmlns o no se pinta nada. Si quieres el detalle de la propiedad, está en la documentación de appearance en MDN.

Girar la flecha al abrir el desplegable

Esto es reciente y poca gente lo usa todavía: la pseudoclase :open te dice si el select está desplegado, y funciona también en selects clásicos. Disponible en Chrome 133+, Firefox 136+ y Safari 26.5+.

select:open {
  background-color: #f8f2dc;
  /* misma flecha con el triángulo al revés */
  background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolygon points='5,15 10,5 15,15' fill='%23333'/%3E%3C/svg%3E");
}

Por qué no puedes usar ::after para la flecha

Es la pregunta que llega siempre al tocar los estilos de un select. Los pseudoelementos ::before y ::after no funcionan sobre un select, porque su contenido lo controla el navegador de principio a fin. Si quieres generar la flecha con contenido en vez de con una imagen de fondo, necesitas envolver el select:

<div class="select-wrapper">
  <select id="pais" name="pais">
    <option value="es">España</option>
    <option value="mx">México</option>
  </select>
</div>
.select-wrapper {
  position: relative;
  background-color: #fff; /* el fondo, mejor aquí que en el select */
}

.select-wrapper select {
  appearance: none;
  width: 100%;
  background: transparent;
}

.select-wrapper::after {
  content: "▼";
  position: absolute;
  top: 50%;
  right: 12px;
  transform: translateY(-50%);
  pointer-events: none; /* imprescindible: si no, roba el clic */
}

Ese pointer-events: none no es opcional. Sin él, la flecha tapa el select y el usuario pulsa justo encima sin que se abra nada.

Poner el fondo en el contenedor y no en el select tiene otra ventaja: en Firefox los option heredan el fondo del select, y eso te puede dejar unas opciones tintadas con un aspecto raro que no habías pedido.

Estilar el option: hasta dónde llegas de verdad

Esta es la sección que buscabas si has llegado aquí desde Google, y la que ningún tutorial sobre estilos de un select explica del todo. Te la doy sin adornos.

En el modo clásico, lo que puedes aplicar a un option depende del navegador y del sistema operativo a la vez. No es una cuestión de escribir mejor el CSS: es que el desplegable lo pinta el sistema.

Lo que puede funcionar

  • font-size heredado del select: se respeta en Firefox y en Chromium, y aun así depende del sistema operativo.
  • color, background-color, font-family, font-variant y text-align: solo en Chromium, y sin garantía. La documentación usa el verbo «puede», no «hace».

Lo que no funciona nunca

  • Espaciado: padding, margin o altura de fila en un option. Olvídalo.
  • border, border-radius, box-shadow, display o flexbox dentro del option.
  • La caja que contiene las opciones: colores, espaciado, esquinas. Solo heredas la tipografía del padre.
  • En Safari y navegadores WebKit, nada. No hay estilo de option que llegue.
  • El fondo de un optgroup en Firefox: no hay forma de estilarlo. Puedes acabar con options del color de tu select y optgroups clavados en el gris por defecto.

Mi consejo tras años peleándome con esto: en el modo clásico, mantén el estilo de option y optgroup lo más cerca posible de los valores por defecto. Todo lo que añadas ahí se va a comportar distinto en cada máquina y vas a estar depurando algo que no depende de ti.

Si de verdad necesitas control total y no puedes usar el modo nuevo, tienes dos salidas: un componente propio hecho con botones y listas (asumiendo todo el trabajo de accesibilidad), o usar multiple para que las opciones se rendericen en la página y esquives el desplegable nativo.

El modo nuevo: appearance base-select

Aquí está la novedad que cambia el juego. Con appearance: base-select le dices al navegador que deje de tratar el select como un widget del sistema y lo pinte como un elemento normal. El desplegable pasa a ser tuyo: bordes, fondos, filas con iconos, animaciones. Y sigue siendo un select nativo, con su accesibilidad y su navegación por teclado intactas.

Hay dos requisitos y los dos se saltan a menudo.

Uno: el CSS va en dos sitios. Tienes que declarar appearance: base-select tanto en el select como en el pseudoelemento ::picker(select). Puedes activar solo el select y dejar el desplegable con el estilo del sistema, pero no puedes hacer lo contrario.

Dos: el HTML cambia. Ahora un button puede ser el primer hijo del select, y dentro va selectedcontent, que muestra un clon de la opción elegida. Si no pones esa estructura, el navegador crea un botón implícito que no puedes seleccionar con CSS.

<label for="mascota">Elige mascota:</label>
<select id="mascota" name="mascota">
  <button>
    <selectedcontent></selectedcontent>
  </button>

  <option value="">Elige una opción</option>
  <option value="gato">
    <span class="icono" aria-hidden="true">🐱</span>
    <span class="etiqueta">Gato</span>
  </option>
  <option value="perro">
    <span class="icono" aria-hidden="true">🐶</span>
    <span class="etiqueta">Perro</span>
  </option>
</select>
/* 1. Activación: obligatorio en los dos */
select,
::picker(select) {
  appearance: base-select;
}

/* 2. A partir de aquí, se estila como cualquier elemento */
select {
  border: 2px solid #ddd;
  border-radius: 8px;
  padding: 10px 14px;
  background: #fff;
}

::picker(select) {
  border: none;
  border-radius: 8px;
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.12);
}

option {
  display: flex;
  gap: 12px;
  padding: 10px 14px; /* sí, aquí el padding SÍ funciona */
}

option:checked {
  font-weight: 700;
}

/* 3. La flecha del botón, ahora con pseudoelemento propio */
select::picker-icon {
  color: #999;
  transition: rotate .3s;
}

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

/* 4. Ocultar el icono solo en el botón cerrado */
selectedcontent .icono {
  display: none;
}

Fíjate en el padding del option. En el modo clásico era imposible; aquí funciona. Eso es lo que cambia realmente.

Los selectores que tienes disponibles

SelectorPara qué sirve
::picker(select)Todo el desplegable como una sola entidad
::picker-iconLa flecha dentro del botón
::checkmarkLa marca de verificación de la opción elegida
:openEl botón cuando el desplegable está abierto
:checkedLa opción seleccionada dentro del desplegable

Dos regalos que vienen de serie y que no tienes que programar. El botón y el desplegable tienen una relación de popover implícita, así que puedes animar la apertura con transition y allow-discrete más @starting-style. Y hay un anclaje implícito: no necesitas declarar anchor-name, el desplegable ya se posiciona respecto al botón y el navegador trae reglas de reposicionamiento por si se sale de la pantalla.

Pon siempre el atributo value

Esta trampa te va a morder si metes iconos. Cuando un option no tiene atributo value, el valor que se envía se calcula a partir del texto que hay dentro. Con un emoji en un span, ese emoji acaba dentro del valor del formulario. Pon value en todos los option y no tendrás que pensar en ello. El anuncio original con todos los detalles está en el blog de Chrome for Developers.

Fallback con @supports

Y ahora la letra pequeña, que es importante: esto no es Baseline todavía. A día de hoy, agosto de 2026, el soporte está así:

NavegadorEstado
Chrome y EdgeDesde la versión 135, en estable
SafariLlega con Safari 27, presentado en beta. Aún no en estable
FirefoxImplementado pero detrás de flags, desactivado por defecto

Traducido a decisiones: hoy puedes cambiar los estilos de un select así en producción, pero como mejora progresiva, nunca como base. El patrón correcto es escribir primero el select clásico decente y envolver la mejora en una consulta de soporte.

/* Base para todos: el método clásico */
select {
  appearance: none;
  /* … toda la receta clásica de más arriba … */
}

/* Mejora solo donde hay soporte */
@supports (appearance: base-select) {
  select,
  ::picker(select) {
    appearance: base-select;
  }

  /* aquí todo el estilo del modo base */
  option { display: flex; gap: 12px; padding: 10px 14px; }
}

Lo bueno es que el degradado es limpio por diseño. En un navegador sin soporte, la estructura del button y del selectedcontent se ignora por completo, del contenido de los option se conserva solo el texto, y el select sigue funcionando como siempre. No se rompe nada.

Ojo con una cosa si trabajas con frameworks: algunos bloquean estas características, y en otros provocan fallos de hidratación cuando renderizas en servidor. Pruébalo en tu stack antes de darlo por hecho. Para consultar el soporte al día, la ficha de ::picker() en MDN lleva la tabla actualizada.

Accesibilidad: la regla de oro

Si te quedas con una sola frase de todo el artículo, que sea esta: cada option tiene que llevar texto siempre. Los iconos, los cuadros de color y las ilustraciones se añaden al texto, nunca lo sustituyen.

Un option con solo un icono falla en tres frentes a la vez: es más difícil de usar para cualquiera, es imposible con lector de pantalla o línea braille, y en un navegador sin soporte deja un desplegable con opciones vacías. El texto puede estar oculto visualmente si tu diseño lo pide, pero tiene que existir en el HTML, porque sigue en el árbol de accesibilidad.

<!-- Bien: el icono decora, el texto informa -->
<option value="fauna">
  <img src="pajaro.svg" alt="">
  <span>Fauna salvaje</span>
</option>

<!-- Mal: sin texto no hay etiqueta accesible -->
<option value="fauna">
  <img src="pajaro.svg" alt="Fauna">
</option>

Tres notas más que conviene tener presentes:

  • Si el icono es un emoji dentro de un span, ponle aria-hidden="true". Si no, el lector anuncia «gato gato».
  • ::checkmark y ::picker-icon no están en el árbol de accesibilidad. Lo que les pongas con content no lo anuncia nadie.
  • El button dentro del select es inert por defecto, y con él todo lo que contenga. Es a propósito: el conjunto se trata como un botón único. Pero significa que nada interactivo que metas ahí va a recibir foco.

Qué pierdes al activar base-select

No todo son ventajas cuando controlas los estilos de un select al completo, y esto casi nunca se cuenta. Al pasar al modo base renuncias a tres cosas:

  • El select ya no puede dibujarse fuera del panel del navegador. En el modo nativo podía desbordar la ventana; ahora no.
  • Se pierden los componentes nativos del móvil. Adiós a la rueda de selección de iOS y al diálogo de Android. Esto es lo más serio de los tres: son controles que el usuario conoce y que están muy optimizados para el dedo. Piénsalo antes de activarlo en un formulario de móvil.
  • El select deja de coger el ancho de la opción más larga. Tendrás que fijar el width a mano.

Lo que no cambia: tu JavaScript sigue funcionando igual, porque las interfaces del DOM son las mismas. Con un matiz que sí puede sorprenderte: selectedcontent guarda un clon del option elegido, y si modificas el contenido de ese option por JavaScript después del render inicial, el clon no se actualiza solo. Hay que refrescarlo a mano.

Más CSS en el blog

Preguntas frecuentes

¿Por qué no puedo cambiar el color de fondo de un option?

Porque esa parte no depende de los estilos de un select que escribas: el desplegable lo pinta el sistema operativo, no el navegador. En Chromium el background-color del option a veces llega, en Safari nunca. La única forma fiable de controlarlo es el modo appearance: base-select, y ahí sí funciona sin excepciones.

¿Puedo poner padding a un option?

En el modo clásico, no. El espaciado de las filas del desplegable no es accesible desde CSS en ningún navegador. En el modo base sí, y además los option llevan display: flex por defecto, así que puedes montar la fila con icono y texto con gap.

¿Por qué se me ven dos flechas en el select?

Te falta appearance: none. Has añadido tu flecha, pero la nativa del sistema sigue ahí y se pintan las dos. Era exactamente el error de la primera versión de este artículo.

¿Puedo usar ::before o ::after en un select?

No. El contenido del select lo controla el navegador, así que los pseudoelementos no se generan. Si necesitas contenido generado para la flecha, envuelve el select en un contenedor y usa ::after sobre él, con pointer-events: none.

¿Necesito una librería de JavaScript para tener un select bonito?

Cada vez menos, y esa es la buena noticia. Hasta 2025 la respuesta era sí para cualquier diseño exigente. Con base-select consigues el mismo resultado con CSS, manteniendo un select nativo con su teclado y su semántica. Mientras Safari y Firefox no lo tengan en estable, la mejora progresiva es el camino sensato.

¿Funciona con select multiple?

De momento con muchas reservas. El soporte de base-select para listas de selección múltiple llegó más tarde a Chrome y está marcado como experimental, así que no lo pondría en producción todavía.

¿Puedo estilar el desplegable de un datalist?

No, y por el mismo motivo que el select clásico: el panel de autocompletado lo dibuja el navegador y el CSS no llega. Si necesitas control sobre ese panel, toca construir el componente.

Con esto tienes las tres capas de los estilos de un select: lo que funciona en todas partes, lo que nunca va a funcionar y lo que ya puedes empezar a usar con red de seguridad. Si te ha aclarado el asunto, compártelo. Y si te encuentras un comportamiento raro en algún navegador concreto, cuéntamelo en los comentarios como hizo Néstor en su día, que gracias a ese aviso este artículo está hoy corregido.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio