Animated Select Picker
The customizable select API offers a declarative, CSS-driven way to animate <select> elements and their dropdown pickers. By combining appearance: base-select with modern CSS animation techniques—such as @starting-style and the allow-discrete transition behavior—you can create fluid, premium UI transitions for top-layer elements without relying on heavy JavaScript libraries.
Previously, animating native select dropdowns was impossible because their UI was rendered outside the accessible viewport constraints. With appearance: base-select, the picker becomes styleable and animatable like any other page element.
How to Implement
Section titled “How to Implement”To implement an animated select picker:
- Opt-in to customization: Apply
appearance: base-selectto both the<select>element and the::picker(select)pseudo-element. - Enable auto-sizing transitions (Optional): Define
interpolate-size: allow-keywords(usually on:root) to allow the browser to transition between discrete metric values likeheight: autoandheight: 0. - Animate the top-layer container: Apply standard entry/exit styles to
::picker(select). Includedisplayandoverlayin thetransitionproperty and settransition-behavior: allow-discreteso the picker stays rendered in the top layer when transitioning betweendisplay: noneanddisplay: block. - Hook into the opening state with
@starting-style: Use@starting-styleto define the baseline styles the browser should compute before the transition begins. For example, if you want it to fade in, set the opacity to0inside the@starting-styleblock. - Rotate the icon: Use pseudo-element focus or active selectors like
:open::picker-iconto apply transitions (such as rotation or translation) to the arrow indicator.
Example Code: Smooth Select Scale and Fade
Section titled “Example Code: Smooth Select Scale and Fade”The following example demonstrates a custom select styled with standard page animations for the picker container.
<!-- 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) { /* Set a text color with the background so options stay readable on pages with a dark color scheme */ background: white; color: black; 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);
transition: display, overlay, opacity cubic-bezier(0.16, 1, 0.3, 1), height cubic-bezier(0.16, 1, 0.3, 1); transition-duration: 0.4s; /* Separate after transition so the shorthand doesn't reset it or fail in older browsers */ transition-behavior: allow-discrete; 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; color: black;}
/* 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; }}Strategic Implementation & Best Practices
Section titled “Strategic Implementation & Best Practices”- DO use
@starting-stylewhen you need animations to trigger exactly when an element transitions fromdisplay: noneto visible. - DO NOT use
allow-discretein thetransitionshorthand — it will make older browsers ignore the entiretransitiondeclaration. Except in use cases where that is desirable, use a separatetransition-behavior: allow-discretedeclaration. - DO NOT use ad-hoc scroll locking. Top-layer elements managed by ‘base-select’ should allow natural backdrop dismiss behaviors.
- DO NOT set
overflow: hiddenon::picker(select). The picker must stay scrollable so that options that don’t fit in the available space, for example at high zoom or in a short viewport, can still be reached. - DO verify reduced motion preferences. Always wrap animation constraints in a
prefers-reduced-motionmedia query to ensure accessible environments for those affected by motion sickness. Baseline: customizable-select / usage
Fallback strategies
Section titled “Fallback strategies”Baseline: customizable-select
Compatibility notes: Badges like Baseline: … mark web features referenced by this guide. Baseline status describes availability across the core browser set — it does not mean every browser or older version supports the feature. Feature data as published upstream, synced 2026-11-12.
Live demo
Section titled “Live demo”Open the demo standalone · Demo source on GitHub
Counter-example (negative demo) — what this guide tells you to avoid.
Source: GoogleChrome/modern-web-guidance-src (Apache-2.0), upstream commit 179dbc6, synced 2026-11-12. Local changes: macros expanded to links/badges, first heading de-duplicated, demo and this footer added. This is an independent, unofficial site — not a Google publication.