select en HTML: selected, optgroup, multiple y cómo estilarlo sin romperlo
El select nativo trae gratis el teclado, el lector de pantalla y el desplegable del móvil, y casi nadie lo aprovecha. Cómo se construye, cómo se marca la opción por defecto con selected, qué llega al servidor con multiple y hasta dónde se puede estilar de verdad en 2026.
Tabla de contenidos
Pones un <select> en un formulario de alta. La primera opción dice "Selecciona una provincia". El usuario ni la toca, pulsa enviar, y en tu base de datos acaba una fila con provincia = "Selecciona una provincia". Nadie eligió nada y aun así tienes un dato basura que parece válido. El select en HTML es uno de los controles más antiguos de la plataforma y, a la vez, uno de los peor entendidos: se usa mal como etiqueta, se estila a martillazos y, cuando el diseño aprieta, se tira a la basura y se reconstruye con <div> y JavaScript perdiendo por el camino el teclado, el lector de pantalla y el desplegable nativo del móvil.
Voy a desmontar el control entero: cómo se construye, cómo se marca la opción por defecto con selected, cómo se agrupan opciones con optgroup, cómo se permite elegir varias con multiple, qué llega de verdad al servidor cuando lo haces, y hasta dónde se puede estilar hoy sin romper nada. Empiezo por lo básico, porque es exactamente donde están casi todos los errores que veo en producción.
Qué es la etiqueta select y cómo se construye
Un <select> es un control de formulario que ofrece una lista de opciones para elegir. Se compone de tres piezas: el propio <select>, que es el contenedor y el que lleva el name que viaja al servidor; los <option>, cada uno con su value; y una <label> asociada que le da nombre accesible.
Dos detalles que la mayoría pasa por alto. El primero: lo que se envía al servidor es el value, no el texto visible. Si un <option> no tiene value, el navegador manda el texto interior como valor; por eso conviene poner value siempre, para controlar exactamente qué recibes. El segundo: el for de la <label> tiene que coincidir con el id del <select>, no con su name. Sin esa asociación, un lector de pantalla anuncia el control como "lista" sin decir de qué, y un clic en el texto no enfoca el campo.
Fíjate en la primera opción del ejemplo: value="". Es una opción vacía que actúa de marcador de posición. Vuelvo sobre ella enseguida, porque ahí se cuece el primer error grave.
Marcar una opción por defecto: el atributo selected
Por defecto, un <select> cerrado muestra su primer<option>. Si quieres que arranque con otra opción marcada, el atributo selected en ese <option> lo fija al cargar la página:
selected es un atributo booleano: está o no está, selected="selected" y selected son equivalentes. Si lo pones en más de un <option> de un select normal (sin multiple), gana el último marcado y el resto se ignora.
Aquí llega el error que abría el artículo. Mucha gente usa la primera opción como si fuera la etiqueta del campo:
<!-- MAL: la primera opción hace de etiqueta -->
<select name="provincia">
<option selected>Selecciona una provincia</option>
<option>Madrid</option>
</select>
Ese <option> no tiene value, así que su valor enviado es el texto "Selecciona una provincia", un dato perfectamente inútil que además supera cualquier validación de "campo no vacío". Y como sustituye a una <label> de verdad, el lector de pantalla no tiene nombre estable que anunciar. La forma correcta usa una <label> aparte y deja la primera opción con value="" para que un required la rechace:
Con value="" y required, el navegador bloquea el envío si el usuario no cambia la opción. Con disabled sobre esa primera opción evitas además que la vuelva a elegir a propósito. No confíes solo en eso: valida siempre en el servidor, porque el atributo required es una comodidad de la interfaz, no una garantía. Lo aprendí por las malas confiando en lo que llega del cliente, un patrón que ya conté a propósito de cómo NULL rompe silenciosamente una comparación en SQL.
Agrupar opciones con optgroup
Cuando la lista crece, <optgroup> divide las opciones en categorías con un encabezado no seleccionable. El texto del grupo va en su atributo label:
<label for="plato">Plato</label>
<select name="plato" id="plato">
<optgroup label="Entrantes">
<option value="ensalada">Ensalada de la casa</option>
<option value="croquetas">Croquetas</option>
</optgroup>
<optgroup label="Principales">
<option value="lubina">Lubina al horno</option>
<option value="solomillo">Solomillo</option>
</optgroup>
</select>
El label del optgroup es puramente visual y de agrupación: no es una opción, no se puede seleccionar y no se envía nada por él. Un detalle práctico: los <optgroup> no se pueden anidar. Si necesitas jerarquías de varios niveles, el <select> no es tu control; probablemente quieras un árbol o una búsqueda con autocompletado.
Selección múltiple: los atributos multiple y size
Por defecto solo se elige una opción. El atributo booleano multiple permite elegir varias. Al ponerlo, el control deja de ser un desplegable y se convierte en una lista con scroll; el usuario selecciona con Ctrl (o Cmd en Mac) para sumar opciones sueltas y con Shift para un rango.
El atributo size fija cuántas filas se ven a la vez. Según MDN, la especificación fija el valor por defecto en 1, pero Firefox devuelve 0 para no romper webs antiguas; en la práctica, si no pones size en un multiple, el alto que ves depende del navegador. Ponlo tú y no dependas del azar. size también funciona en un <select> sin multiple: con size="5" y una sola selección obtienes una lista abierta de cinco filas en lugar del desplegable clásico.
Ese gesto de Ctrl/Cmd es poco descubrible. Mucha gente no sabe que puede elegir más de uno, así que, si usas multiple, ayúdalo con un texto de apoyo cercano. Y ojo con el móvil: el selector múltiple nativo en pantallas táctiles es incómodo. Para listas largas de selección múltiple, plantéate si un grupo de checkboxes no comunica mejor que el usuario puede marcar varias.
El desplegable que el navegador te da gratis ya sabe funcionar con teclado, con lector de pantalla y con el dedo. Reconstruirlo cuesta más de lo que parece.
Qué llega al servidor con multiple: el POST y el porqué de name="campo[]"
Aquí está la laguna que casi ningún tutorial cubre y que provoca bugs difíciles de ver. Cuando envías un <select multiple>, el navegador no manda "un array": manda un par name=value por cada opción marcada. Si el usuario elige rojo y azul, en el cuerpo de la petición viajan dos pares con el mismo nombre:
colores=rojo&colores=azul
Qué hace tu backend con dos claves repetidas depende del lenguaje, y ahí está la trampa. PHP, si el control se llama colores, procesa los pares en orden y cada uno sobrescribe al anterior: te quedas solo con azul, el último, y el rojo desaparece sin error ni aviso. La solución es la convención de PHP de añadir corchetes al name:
Los corchetes son sintaxis de PHP, no de HTML ni de HTTP: el navegador los trata como parte literal del nombre y envía colores[]=rojo&colores[]=azul. Otros entornos resuelven las claves repetidas de otra forma, hay frameworks que te devuelven directamente un array sin corchetes, así que comprueba el tuyo antes de dar nada por hecho.
Y el caso límite que se olvida siempre: si el usuario no marca ninguna opción de un multiple, el control no envía nada en absoluto. La clave ni aparece en el cuerpo de la petición. Por eso arriba escribo $_POST['colores'] ?? []: si esperas una clave que no llega, revientas con un aviso de índice indefinido. Un <select multiple> sin selección no manda una cadena vacía; manda silencio.
Por qué durante veinte años no se pudo estilar un select
Si alguna vez has intentado cambiar la flecha de un <select> o el aspecto del desplegable abierto y te has estrellado, no era culpa tuya. Durante casi dos décadas fue imposible de verdad, y la razón es arquitectónica.
El desplegable de un <select>, el panel que aparece con la lista de opciones, no lo pintaba el motor del navegador con CSS como el resto de la página. Lo dibujaba el propio sistema operativo, con sus widgets nativos, fuera del árbol de renderizado normal. Por eso el desplegable de un <select> en Windows se veía distinto que en macOS o en Android: no era HTML pintado, era un control del sistema invocado por el navegador. Y a lo que dibuja el sistema operativo, tu CSS no llega.
Podías tocar la caja cerrada, bordes, fondo, tipografía, con suerte variable, pero el panel abierto era territorio prohibido. Esa frontera es la que explica tanto la frustración histórica como el auge de las librerías que reemplazaban el control entero. Y también por qué lo que ha cambiado por fin en 2026 es una novedad de calado y no un truco más.
Estilar el select nativo hoy: appearance: none y tu propia flecha
La técnica clásica, soportada de forma amplia y la que debes usar como base, es appearance: none. Le dice al navegador que no pinte su estilo nativo del control, incluida la flecha. A partir de ahí dibujas la tuya, normalmente con una imagen de fondo:
La flecha es un SVG embebido como data URI, un triángulo apuntando hacia abajo. El padding-right grande reserva el hueco para que el texto de la opción no se solape con ella. Con esto tienes control total sobre la caja cerrada y funciona prácticamente en todo lo que hay ahí fuera.
Lo que appearance: noneno te da es el desplegable abierto. El panel con las opciones sigue siendo el nativo del sistema. Durante años eso fue el techo. Ya no lo es del todo, pero lo que viene ahora hay que servirlo con cabeza.
Personalizar el desplegable en 2026: appearance: base-select y sus pseudo-elementos
La novedad real es que ahora se puede estilar también el desplegable abierto: los bordes del panel, cada opción, la marca de la seleccionada y la flecha, todo con CSS normal. Se activa con un valor nuevo, appearance: base-select, que hay que poner tanto en el <select> como en su picker:
Al optar por este modo, el marcado cambia: dentro del <select> metes un <button> con un <selectedcontent>, que es un clon de la opción elegida y hace de cara visible del control cerrado. El resto siguen siendo <option>:
Con eso desbloqueas pseudo-elementos que antes no existían. ::picker(select) apunta al panel desplegable entero; ::picker-icon, a la flecha; y ::checkmark, a la marca de la opción activa. Ya puedes hacer cosas que durante veinte años fueron imposibles, como girar la flecha al abrir:
Y ahora la parte honesta, la que el marketing de los navegadores se salta. Esto no está soportado en todas partes. MDN clasifica los elementos select personalizables como de "disponibilidad limitada". Según caniuse, consultado el 10 de julio de 2026, el soporte global ronda el 68 %: lo tiene el motor de Chromium (Chrome, Edge y Opera) desde la versión 135 y Safari de escritorio desde la 27; Firefox lo trae detrás de una bandera desactivada por defecto, y Safari en iOS todavía no lo tiene. Comprueba ese enlace el día que vayas a usarlo, porque estas cifras se mueven rápido.
La buena noticia es que degrada de forma natural. Un navegador que no entiende appearance: base-select ignora ese valor y sigue mostrando el <select> nativo, que funciona perfectamente. Es decir: puedes usarlo hoy como mejora progresiva. Hoy la mayoría de tus visitantes vería el control personalizado y una parte todavía importante, Firefox y todo iPhone, vería el nativo. Ninguno vería nada roto. Lo que no debes hacer es construir tu diseño dependiendo de estos estilos, ni dar por sentado que el usuario los verá. Trátalos como un extra, pruébalos en Chromium y en un navegador sin soporte, y no los conviertas en requisito. Si necesitas hoy un desplegable idéntico en todos los navegadores, esta no es todavía tu herramienta.
El error grave: reemplazar el select por un div con JavaScript
Aquí está el fondo del asunto. Cuando el diseño exige un desplegable que la técnica clásica no permitía, la tentación clásica ha sido tirar el <select> y reconstruirlo con un <div>, una lista y JavaScript, o instalar una librería que hace justo eso. Es, casi siempre, un error.
Un <select> nativo trae gratis, de fábrica, un montón de comportamiento que cuesta muchísimo replicar bien:
Navegación completa por teclado: flechas para moverse, Home y End, teclear una letra para saltar a la opción que empieza por ella, Escape para cerrar.
Anuncio correcto en lectores de pantalla: se presenta como lo que es, con su estado y su opción activa.
El selector nativo del móvil, esa rueda o lista a pantalla completa que el sistema operativo ofrece y que los usuarios de móvil ya conocen.
Gestión del foco, cierre al hacer clic fuera, y comportamiento coherente con el resto del sistema.
Cuando lo reemplazas por un <div>, empiezas de cero con todo eso. Tienes que reconstruir a mano los roles ARIA (listbox, option), la gestión del foco, cada tecla, el estado seleccionado, el cierre por Escape y el comportamiento táctil. La inmensa mayoría de esas reimplementaciones, incluidas librerías muy populares, quedan a medias: se manejan con ratón pero cojean con teclado, confunden al lector de pantalla o pierden el desplegable nativo del móvil. Has cambiado un control accesible por defecto por uno que hay que auditar entero, y encima has metido dependencias.
La regla que sigo, y que MDN recomienda explícitamente ahora, es partir siempre del <select> nativo. Con appearance: none estilas la caja; con appearance: base-select, cuando el soporte te lo permita y como mejora progresiva, estilas también el desplegable. Reescribir el control con <div> es la última opción, no la primera, y casi nunca hace falta. Este es exactamente el tipo de decisión que separa una interfaz que funciona para todos de una que solo funciona para quien la programó; forma parte de cómo enfoco el desarrollo frontend.
Accesibilidad del select: label asociado y buenas prácticas
Resumo lo accionable, porque la accesibilidad de un <select> es sencilla si respetas cuatro cosas.
Asocia siempre una <label> con for apuntando al id del control. Es lo que da nombre al campo, permite enfocarlo al pulsar el texto y lo hace usable con lector de pantalla. No uses la primera <option> como etiqueta: rompe el nombre accesible y, si no tiene value, ensucia tus datos. Si necesitas un marcador de posición, usa una opción con value="" combinada con required, y añade disabled para que no se pueda reelegir, tal como mostré en la sección de selected.
No conviertas un <select> en menú de navegación que dispara la acción al cambiar de opción. Un <select> es para elegir un valor de un formulario, no para navegar; hacer que un change te lleve a otra página sorprende a quien usa teclado y salta de opción con las flechas. Y recuerda lo de siempre: el <select> nativo ya es accesible; cada vez que lo sustituyes por otra cosa, la accesibilidad pasa a ser tu responsabilidad, no la del navegador.
Tabla de referencia: atributos de select y option
Elemento
Atributo
Qué hace
<select>
name
Nombre del control que viaja al servidor. Con multiple, en PHP añade [] para recibir un array.
<select>
multiple
Booleano. Permite elegir varias opciones; el control pasa a lista con scroll.
<select>
size
Número de filas visibles. Por defecto 1 según la especificación (0 en Firefox).
<select>
required
Booleano. Exige una opción con value no vacío antes de enviar.
<select>
disabled
Booleano. Desactiva el control; no se envía su valor.
<select>
autofocus
Booleano. Enfoca el control al cargar la página.
<select>
form
Asocia el control a un <form> por su id, aunque esté fuera de él.
<option>
value
Valor que se envía. Si falta, se envía el texto interior.
<option>
selected
Booleano. Marca la opción por defecto al cargar.
<option>
disabled
Booleano. Opción no seleccionable (útil en el marcador de posición).
<optgroup>
label
Encabezado de grupo, no seleccionable y sin valor propio.
Ese es el select en HTML entero: un control viejo, robusto y accesible por defecto que casi nadie exprime. La mayoría de los problemas no vienen de que el <select> sea limitado, sino de pelearse con él en vez de usarlo como está pensado. Estilas la caja con appearance: none, sumas el desplegable personalizado con appearance: base-select como mejora progresiva cuando el soporte acompañe, nombras bien el control para el servidor y dejas que el navegador siga haciendo el trabajo duro del teclado, el foco y el móvil. Si en algún momento sientes que necesitas reemplazarlo por completo, para y pregúntate qué estás ganando de verdad y qué vas a tener que reconstruir a cambio.
Preguntas frecuentes
¿Cómo marco una opción como seleccionada por defecto en un select?
Se pone el atributo booleano selected en el option que quieras que aparezca marcado al cargar la página, por ejemplo . Si no pones selected en ninguno, el navegador muestra por defecto el primer option de la lista. En un select normal (sin multiple), si marcas selected en varios options gana el último; solo en un select con multiple tiene sentido preseleccionar varios.
¿Cómo pongo selección múltiple en un select de HTML?
Se añade el atributo booleano multiple al select. El control deja de ser un desplegable y se convierte en una lista con scroll: el usuario suma opciones sueltas con Ctrl (Cmd en Mac) y selecciona un rango con Shift. Conviene fijar también size para controlar cuántas filas se ven. Como el gesto de Ctrl es poco descubrible y en móvil resulta incómodo, para listas largas suele comunicar mejor un grupo de checkboxes.
¿Por qué con multiple solo me llega un valor al servidor?
Porque un select multiple no envía un array: manda un par name=value por cada opción marcada (por ejemplo colores=rojo&colores=azul). PHP procesa los pares repetidos sobrescribiendo, así que te quedas con el último. La solución es nombrar el control con corchetes, name="colores[]", y entonces PHP construye un array de verdad. Ten en cuenta además que si el usuario no marca ninguna opción, la clave no llega en absoluto: recoge el valor con algo como $_POST['colores'] ?? [].
¿Cómo cambio la flecha de un select con CSS?
Se aplica appearance: none (con el prefijo -webkit-appearance: none) al select para que el navegador no pinte su flecha nativa, y luego se dibuja la propia como imagen de fondo, normalmente un SVG embebido como data URI colocado a la derecha con background-position. Hay que reservar hueco con padding-right para que el texto no se solape con la flecha. Esta técnica está soportada de forma amplia y controla la caja cerrada del select, pero no el panel desplegable abierto.
¿Ya se puede personalizar el desplegable del select con CSS en 2026?
En parte. Existe appearance: base-select, que activado en el select y en su ::picker(select) permite estilar también el panel abierto, cada option, la flecha (::picker-icon) y la marca de la opción activa (::checkmark), usando un
¿Debo reemplazar el select por un div con JavaScript para estilizarlo?
Casi nunca. El select nativo trae gratis navegación por teclado, anuncio correcto en lectores de pantalla, gestión de foco y el selector nativo del móvil. Al reconstruirlo con un div tienes que reimplementar todo eso a mano (roles ARIA, teclas, foco, cierre por Escape, comportamiento táctil), y la mayoría de esas versiones quedan a medias. Lo recomendable es partir siempre del select nativo, estilar la caja con appearance: none y, cuando el soporte acompañe, el desplegable con appearance: base-select.
Casillas y botones de radio parecen triviales y casi todo el mundo los hace mal. Qué llega de verdad al servidor, por qué el label no es decorativo y cómo estilarlos en 2026 sin romper el teclado.
Un salto de línea en HTML se hace con <br>, pero casi siempre que lo ves está mal usado. Te explico cuándo es contenido y cuándo son dos párrafos, más wbr, ­, y el CSS que controla el corte.
Un iframe no incrusta un vídeo: incrusta un documento entero con su propio código dentro del tuyo. Qué atributos importan de verdad y cómo cerrar los agujeros de seguridad que casi ningún tutorial menciona.
¿Te ha servido?
Si tienes un problema parecido y quieres una opinión técnica, cuéntamelo. Te respondo en menos de 24 h laborables.
Uso cookies propias necesarias para que la web funcione. Las demás categorías
están desactivadas hasta que tú decidas. Puedes aceptarlas todas, rechazarlas
todas o elegir una a una. Más detalle en la
política de cookies.