La API de selección personalizable ofrece una forma declarativa y controlada por CSS de animar los elementos <select> y sus selectores desplegables. Si combinas appearance: base-select con técnicas modernas de animación de CSS, como @starting-style y el comportamiento de transición allow-discrete, puedes crear transiciones de IU fluidas y de alta calidad para los elementos de la capa superior sin depender de bibliotecas de JavaScript pesadas.
Anteriormente, era imposible animar los menús desplegables de selección nativos porque su IU se renderizaba fuera de las restricciones de viewport accesibles. Con appearance: base-select, el selector se puede diseñar y animar como cualquier otro elemento de página.
Cómo implementarla
Para implementar un selector animado, haz lo siguiente:
- Habilita la personalización: Aplica
appearance: base-selectal elemento<select>y al seudoelemento::picker(select). - Habilita las transiciones de ajuste de tamaño automático (opcional): Define
interpolate-size: allow-keywords(por lo general, en:root) para permitir que el navegador realice la transición entre valores de métricas discretos, comoheight: autoyheight: 0. - Anima el contenedor de la capa superior: Aplica estilos de entrada y salida estándar a
::picker(select). Para asegurarte de que la transición de opacidad funcione cuando se mueva entredisplay: noneydisplay: block, debes usartransition-behavior: allow-discrete(a menudo, se escribe intercalado comotransition: display 0.3s allow-discrete). - Conéctate al estado de apertura con
@starting-style: Usa@starting-stylepara definir los estilos de referencia que el navegador debe calcular antes de que comience la transición. Por ejemplo, si quieres que aparezca gradualmente, establece la opacidad en0dentro del bloque@starting-style. - Rota el ícono: Usa selectores de enfoque o activos de pseudoelementos, como
:open::picker-icon, para aplicar transiciones (como rotación o traslación) al indicador de flecha.
Código de ejemplo: Selección, escalamiento y atenuación suaves
En el siguiente ejemplo, se muestra una selección personalizada con animaciones de página estándar para el contenedor del selector.
<!-- Always use a <label> linked via 'for' to the select for accessibility -->
<label for="theme-select">Visual Theme</label>
<select id="theme-select" class="animated-select" name="theme">
<!-- The <button> inside <select> becomes the visible trigger when appearance: base-select is used -->
<button>
<!-- <selectedcontent> automatically displays the content of the chosen <option> -->
<selectedcontent></selectedcontent>
</button>
<option value="system">
<!-- MANDATORY: Decorative inline SVGs MUST set aria-hidden="true" to prevent redundant screen reader announcement -->
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
System Default
</option>
<option value="light">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
Light UI
</option>
</select>
/* Opt-in to customizable select */
.animated-select,
.animated-select::picker(select) {
appearance: base-select;
}
/* Enable auto-keyword transitions (usually set globally at :root) */
:root {
interpolate-size: allow-keywords;
}
/* Style the visible trigger and icon rotation */
.animated-select {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.875rem 1rem;
font-size: 1rem;
border-radius: 8px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.animated-select::picker-icon {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.animated-select:open::picker-icon {
transform: rotate(180deg);
}
/*
* The Picker Container
* Uses top-layer animations with `allow-discrete` visibility hooks
*/
.animated-select::picker(select) {
background: white;
border-radius: 12px;
box-shadow: 0 10px 25px -3px rgba(0,0,0,0.1);
padding: 0.5rem;
margin-top: 0.25rem;
width: anchor-size(width);
overflow: hidden;
/* The crucial transition setting for popover animations */
transition:
display 0.4s allow-discrete,
overlay 0.4s allow-discrete,
opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
height 0.4s cubic-bezier(0.16, 1, 0.3, 1);
opacity: 0;
height: 0;
}
/* Open State */
.animated-select:open::picker(select) {
opacity: 1;
height: auto;
}
/* @starting-style to hook the transition on initial popover open */
@starting-style {
.animated-select:open::picker(select) {
opacity: 0;
height: 0;
}
}
/* Support for SVG inside Options and Selected Content */
.animated-select option svg,
.animated-select selectedcontent svg {
flex-shrink: 0; /* Prevent icons from shrinking */
width: 1.25rem;
height: 1.25rem;
}
/* MANDATORY: Provide multiple indicators (e.g. bold font and distinct background) for the checked state to avoid color-only state communication */
.animated-select option:checked {
font-weight: 700;
background-color: #f1f5f9;
}
/* Ensure copy-paste safety for users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
.animated-select::picker(select),
.animated-select::picker-icon {
transition: none !important;
}
}
Implementación estratégica y prácticas recomendadas
- SÍ usa
@starting-stylecuando necesites que las animaciones se activen exactamente cuando un elemento pase dedisplay: nonea visible. - NO uses el bloqueo de desplazamiento ad hoc. Los elementos de la capa superior administrados por "base-select" deben permitir comportamientos naturales de descarte del fondo.
- SÍ verifiques las preferencias de movimiento reducido. Siempre une las restricciones de animación en una consulta de medios
prefers-reduced-motionpara garantizar entornos accesibles para las personas afectadas por el mareo por movimiento. - 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 atributonamey 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
valueestá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.
}
});