¿Como personalizar los option de un elemento select?
Ésta es una de las preguntas cuya respuesta es más ansiada en maquetación web. ¿Cómo le doy estilos al listado de opciones de mi menú desplegable? ¿Cómo puedo sobreescribir los estilos de sistema del option? ¿Cómo puedo eliminar el fondo azul del option seleccionado?
Está claro que la cuestión no tiene fácil solución, de otra forma existirían cientos de artículos explicando cómo hacerlo. En lugar de eso, existen cientos de artículos explicando por qué no es posible. Pero los imperios no los crean emperadores que se rinden ante el primer galo que les planta cara ;). Y el cssar no iba a dejar su suerte en manos de la providencia.
Por ello «vini, vidi, vinci», o lo que es lo mismo, llegé, analicé y experimenté para poder daros una victoria a modo de solución. O mejor dicho, de varias soluciones que os presento a continuación.
Menú desplegable con datalist y options personalizado – Experimento 1
Ésta es la más sencilla de las soluciones. Parte de la idea de que los <option>
de un <datalist>
son áltamente personalizables y por ello los perfectos candidados para iniciar los experimentos sustituyendo el select por un input.
Datalist se utiliza junto con multiplicidad de input types (range, date, etc.) y para asociarlo a ellos, se usa el parámetro list del input. Esto permite que el input tome su valor de la selección que se realice sobre el datalist. Sin embargo tiene un inconveniente. Al igual que con <select>
, el listado ofrecido es un elemento de sistema NO PERSONALIZABLE. Por lo tanto…
Paso 1. Deshacernos del atributo list
Ahora tenemos un input y un datalist oculto por el agente de usuario (el navegador), pero que podremos mostrar de forma sencilla aplicando display: block al recibir el input el foco. Y por supuesto, podremos manipular sus estilos CSS a nuestro antojo.
Paso 2. Devolverle la funcionalidad de select
Al perder la asociación proporcionada por list, deberemos usar algo de javascript (jQuery en este caso), para transferir el valor de datalist al input. Nada complicado podéis verlo en el siguiente codepen.
See the Pen
Personalizar Select Option con CSS – Experimento 1 by Daniel Abril (@elcssar)
on CodePen.
CSS Select option – Experimento 2
En este experimento me ceñí a la estructura estándar de un <select>
para salvaguardar la accesibilidad natural del elemento y permitir que los usuarios siguiesen identificándolo como un select por medio de los lectores de pantalla. Los retos eran los siguientes:
- El primer objetivo era poder dar estilo a los
<option>
- Debía conservar la interacción tanto mediante un ratón como mediante el teclado.
- Debía evitar en la medida de lo posible depender de javascript.
Objetivo 1. CSS para html option
Sólo existe una forma de poder dar estilos CSS a un option html: haciendo que deje de ser un menú desplegable. -«¿Cómo? Pero entonces ya no nos sirve…», diréis-. Pues sí, a veces hay que destruir antes de reconstruir. Más tarde nos preocuparemos de volver a hacer que parezca un dropdown menu. Por lo tanto…
Paso 1. Deconstruir el select
Hay dos maneras de lograr esto: mediante el atributo multiple y/o size. Ambas tienen el mismo efecto sobre el select, cambian su ARIA role de combobox (valor por defecto) a listbox. Mientras combobox identifica un input que controla otro elemento (listbox o grid) que aparece dinámicamente para permitir seleccionar el valor que le aplicará, listbox sería esa lista de opciones desde la que tomaría el valor.
Puesto que no pretendemos crear un multiselect, por lógica la primera manera queda descartada, a menos que queramos complicarnos más la vida.
Así pues, aplicaremos el atributo size. El valor mínimo para lograr nuestro objetivo es dos, pero de momento le pondremos 3 para poder ver mejor el siguiente paso. De momento tendremos un select de 3 líneas en vez de una única línea con caret (la flechita ▼). Si añadiésemos más de 3 options aparecería el scroll .
Le he dado un poco de estilo a la caja para que quedase un poco más presentable.
Paso 2. Dar estilos CSS a option
Ahora ya podemos darle estilos a nuestro option:
- Primero le añadimos margenes con padding
- Seguidamente colores para el texto y el fondo en función del estado: focus, hover…
- Finalemente añadimos unos separadores con border
Paso 3. Eliminar el fondo azul del option
El background azul de :hover ya lo hemos solucionado. Vamos a por el del option seleccionado.
Lo primero que tenemos que entender es que el color azul con texto blanco no corresponde con ningún estado controlable mediante CSS estándar o que podamos forzar desde el depurador de código. Se trata de pseudo-clases propias del navegador a las que no tenemos acceso.
Sin embargo, como sabemos por donde van los tiros, porque vemos que el efecto coincide con el momento en el que se selecciona el elemento, para lo cual CSS nos ofrece el pseudo-selector :checked, podemos partir de ahí. Tendremos que resolver dos problemas: el fondo azul y el texto blanco (aunque esto podría variar según el navegador).
¿Cómo cambiamos el fond azul del option?
Aquí vamos a tirar de un truquito: background-image siempre está por encima de background-color. Y CSS nos permite usar como imagen de fondo funciones como linear-gradient()
. Así, crearemos un degradado entre blanco y blanco (o el color de fondo que queramos), que se superpondrá al color azul.
option:checked {
background: linear-gradient(#fff, #fff);
}
Puesto que el fondo es blanco y el texto también, ahora no podremos leer la etiqueta del option.
Solucionemos el problema del texto blanco
El atributo color del texto en este caso tampoco lo podemos cambiar, como no podíamos cambiar el background-color. Aunque sí podemos cambiar el font-size, font-weight, font-style o text-transform.
Entonces de nuevo tendremos que tirar de imaginación y pensar «out of the box». Html option dispone del atributo label y CSS nos permite crear pseudoelementos :before y :after a cuyo atributo content podemos asignarle un valor mediante la función CSS attr().
La clave será sustituir uno por el otro. Por lo tanto eliminamos el texto del interior del html option y se lo añadimos al label para poder rescatar el valor mediante CSS.
HTML
<option label="mi etiqueta"></option>
CSS
option:checked {
font-size: 0; Ocultamos el label controlado por el navegador y le damos estilos para sus hijos
font-weight: bold;
color: #39C;
background: linear-gradient(#fff, #fff);
}
option:checked:after {
content: attr(label);
font-size: 14px; Recuperamos el tamaño del texto
vertical-align: middle;
}
Para poder centrar el texto he usado un pequeño truco que explico en Alinear texto verticalmente con css vertical-align y flexbox.
Y de esta forma, hemos logrado modificar los CSS de un HTML option.
Objetivo 2. Simular la interacción del select tradicional
Bien, esta fue la parte más compleja, y el objeto principal del experimento: cómo lograr simular con CSS la interacción propia de un menú desplegable que ha dejado de serlo.
Tras muchas combinaciones complejas e infructuosas de :checked, :hover :active, :focus, :focus-within… en las que cada vez que parecía que había logrado que todo funcionase como un select nativo, aparecía un caso de uso en el que se me iba la solución al traste; finalmente, decidí simplificar el problema y dar solución a cada caso por separado. Tomé la determinación de centrarme en montar un dropdown basado en :hover. Sabía que no era lo ideal, pero tenía que avanzar. Y funcionó, tenía un selector bastante funcional para usarios «videntes». Luego abordaría las cuestiones de acccesibilidad.
De esta forma, lo primero que necesité fue añadir un opgroup que utilizaría como menú desplegable. Había considerado otras alternativas como cambiar la altura del propio select, pero había una cuestión importante: el dropdown debía poder superponerse a otros contenidos, o incluso ser posicionable en base al espacio disponible alrededor de la caja del select. Por lo tanto, necesitaba un elmento independiente.
Para que fuese intuitivo además, la caja del dropdown debía incluir el caret y una opción por defecto que invite a seleccionar una opción. Para más detalles de cómo hacer esta parte os invito a que leáis HTML Select CSS dropdown. En él se explica pormenorizadamente cómo dar estilos a la caja del select.
Simular el efecto de selección
En un select estándar, cuando marcas un option su valor se traslada a la caja tras cerrarse la lista desplegable de opciones. Ese efecto es el que perdimos al añadir size y el que recuperaremos a continuación.
Para ello, por defecto ocultaremos todos los option del optgroup, dejando sólo visible un option disabled con el valor –Selecciona una opción–. Al pasar por encima el ratón, mostraremos todos los otros option y una vez marquemos uno, éste sustituirá al primero, que nunca podrá volver a ser marcado, y el resto se ocultarán. Sólo otras opciones del optgroup podran activarse.
Para lograr la magia echaremos mano de una función CSS que recientemente ha logrado soporte casi total entre los navegadores: :has(); Con ella comprobaremos si select contiene algún hijo marcado y ocultaremos el resto.
select:has(option:checked) option:not(:checked) {
display: none;
}
Pero permitiremos que puedan volver a visualizarse con :hover para una nueva selección:
select:has(option:checked):hover option {
display: block;
}
Añadir accesibilidad
Llegados a este punto está claro que sólo podrá ser usado por personas con plena capacidad visual. El uso de :hover hace la interacción dependiente del uso ratón. Para solventarlo amplié los selectores :hover para que incluyesen también :focus y :focus-within.
Para ello usé la función CSS :is(), que nos permite simplificar el selector como vemos a continuación:
select:is(:hover, :focus, :focus-within) optgroup {...}
Si queréis saber más sobre la función :is() podéis hacerlo en el artículo Selectores CSS avanzados 4/4 – :is() y :where().
Des esta forma tanto el select como los option podían ser accesibles mediante la tecla Tab y las flechas de dirección. Sin embargo desaconsejo poner tabindex a los options, eso iría en contra de la navegación estándar y generaría algunos problemas adicionales. Por lo tanto nuestro HTML quedaría tal que así:
<select id="mySelect" tabindex="1" value=0 size=2>
<option disabled>-- Selecciona una opción --</option>
<optgroup>
<option label="Opción 1" value="1"></option>
<option label="Opción 2" value="2"></option>
<option label="Opción 3" value="3"></option>
<option label="Opción 4" value="4"></option>
</optgroup>
</select>
Objetivo 3. Independencia de javascript
Aunque toda la interacción era correcta, consideré un problema el hecho de que al navegar mediante el teclado y seleccionar una opción, el menú desplegable no se cerrase. Por lo tanto tuve que tragarme el orgullo y acceder a usar un poco de javascript para que pasados unos segundos del cambio de valor del select, el desplegable se cerrase.
See the Pen
Personalizar Select multiple con CSS – Experimento 2 by Daniel Abril (@elcssar)
on CodePen.
Actualización 07/03/2025: Cuando publiqué el artículo aún quedaban pendientes algunas cuestiones. Por un lado indicar que una vez llegados al punto de usar JS para eliminar el foco, el planteamiento inicial de usar :hover para abrir y cerrar, no sólo carecía de sentido, sino que era contraproducente. Si se lo quitamos funcionará mucho mejor ya que el usuario no tendrá que apartar el cursor para que se cierre. He actualizado el codepen son este cambio.
Por otro lado, no se había resuelto el crossbrowsing, en concreto con Firefox y Safari que presentaban algunas incidencias. Así que aquí os lo dejo.
Workaround para Firefox select peekaboo
Le he llamado peekaboo, tomando el nombre de un antiguo issue de CSS con IE6, pues básicamente los problemas con Firefox eran: 1) por un lado, que no se veía el dropdown cuando se desplegaba, y 2) que una vez conseguido solventar el primero, los textos no eran tampoco visibles.

Select Peekaboo Bug en Firefox
Mostrar dropdown y eliminar scroll del select
Para hacer frente a ambas incidencias, después de analizar minuciosamente el problema, he decidido hacer una excepción para Firefox.
@-moz-document url-prefix() {Códiog para FF}
El problema principal radicaba en que nuestro select medía el espacio justo para mostrar un único elemento y Firefox no respetaba el overflow: visible que se pretendía forzar y por lo tanto el contentido quedaba oculto. La solución vino de la mano de uno de los primeros planteamientos del experimento: aumentar la altura del select al recibir el foco para mostrar el dropdown. Puesto que teníamos controlada la altura por CSS lo lógico parecía cambiarla por un height: auto; sin embargo, el size=2 que estábamos usando nos limitaba su crecimiento, de modo que hubo que cambiar el valor a 4.
Pero esto generó un problema adicional, que al crecer el select, empujaba el contenido. Añadimos un contenedor con la misma altura que del select y trasferimos los valores del margin a ese contenedor, sólo para FF.
Además, eliminamos el scroll con el estándard scrollbar-width: none;
@-moz-document url-prefix() {
.contenedor{
position: relative;
height: 50px;
margin: 12px;
}
select{
scrollbar-width:none;
margin: 0;
}
select:is(:focus, :focus-within) {
position: absolute;
height: auto;
}
}
Options personalizados en Firefox
Al mostrar los options me encontré con que sólo se veían cuando seleccionaba alguno. El problema al parecer es que Firefox no pinta el valor del atributo label como hacen los -webkit-. Lo solucioné igual que había solucionado el problema del fondo azul: pintando su valor mediante un :after, sólo para FF, pues si no, se duplicaría en el resto de navegadores.
option:after {
content: attr(label);
}
Pero no era el único problema, el padding del select destinado a dejar espacio para el caret, afectaba al optgroup, dejando un hueco extraño a la derecha.

Para solventarlo sólo sólo tuve que eliminar el padding en el momento de recibir el foco, conservándolo cuando el select se cierra.
select:is(:focus, :focus-within) {
position: absolute;
height: auto;
padding: 0;
}
Aquí os dejo el codepen con las correcciones.
See the Pen
Personalizar Select multiple con CSS – Experimento 3 by Daniel Abril (@elcssar)
on CodePen.
Select option en Safari
Aquí topamos con un muro de hormigón. De momento lo he capado por JS cambiando el valor de size a 1 sólo para Safari. De esa forma, su rol no cambia a listbox y el select conserva su comportamiento por defecto, con los estilos de sistema. Digamos que es un «Graceful Degradation» de una solución que Safari no soporta. Aunque la solución para FF sí nos podría ser de utilidad para Safari, se añaden algunos problemas como el hecho de que seguimo sin poder usar paddings ni la pseudoclase :checked.
No olvidéis suscribiros para recibir actualizaciones de este post y otros muchos nuevos que están por venir. Saludos y como siempre, espero vuestros comentarios.