{"id":6293,"date":"2025-07-01T14:37:38","date_gmt":"2025-07-01T19:37:38","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=6293"},"modified":"2025-07-01T14:37:39","modified_gmt":"2025-07-01T19:37:39","slug":"custom-select-that-comes-up-from-the-bottom-on-mobile","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/custom-select-that-comes-up-from-the-bottom-on-mobile\/","title":{"rendered":"Custom Select (that comes up from the bottom on mobile)"},"content":{"rendered":"\n<p><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Learn_web_development\/Extensions\/Forms\/Customizable_select\">Custom <code>&lt;select&gt;<\/code> menus are a thing now<\/a>, especially because they can be progressively enhanced into. Una has <a href=\"https:\/\/codepen.io\/collection\/BNZjPe\">some great examples<\/a>. <\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"669\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-20-at-6.58.11%E2%80%AFAM.png?resize=1024%2C669&#038;ssl=1\" alt=\"Screenshot of a customizable select menu with flags and country names in a dropdown format.\" class=\"wp-image-6304\" style=\"width:482px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-20-at-6.58.11%E2%80%AFAM.png?resize=1024%2C669&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-20-at-6.58.11%E2%80%AFAM.png?resize=300%2C196&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-20-at-6.58.11%E2%80%AFAM.png?resize=768%2C502&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-20-at-6.58.11%E2%80%AFAM.png?w=1090&amp;ssl=1 1090w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><figcaption class=\"wp-element-caption\"><a href=\"https:\/\/codepen.io\/una\/pen\/eYojgZw\">Demo<\/a>, which falls back to entirely default styling.<\/figcaption><\/figure>\n\n\n\n<p>I was recently at <a href=\"https:\/\/www.cssday.nl\/\">CSS Day<\/a> and got to see <a href=\"https:\/\/utilitybend.com\/\">Brecht De Ruyte<\/a> do a whole talk on it. He&#8217;s also go a <s>three<\/s>four-part series on it (<a href=\"https:\/\/utilitybend.com\/blog\/the-customizable-select-part-one-history-trickery-and-styling-the-select-with-css\">starting here<\/a>). My brain was full of CSS stuff while there, I had a weird hankering to work on a custom select that combined a bunch of it. I roped Brecht into collabing on my idea.<\/p>\n\n\n\n<p>See, we were on the heals of the whole <a href=\"https:\/\/www.apple.com\/newsroom\/2025\/06\/apple-introduces-a-delightful-and-elegant-new-software-design\/\">liquid glass thing from Apple<\/a> and it seemed fun to make the selects kinda glassy with borders and blur. I also wanted to if <em>animating the select in<\/em> was possible (and maybe stagger them in?!). Plus, I was reminiscing about the <a href=\"https:\/\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/iphoneselect.png\">original weird iOS select UI<\/a> where it had a special UI that came up from the bottom. Is that maybe&#8230; <em>better?<\/em> for thumb-reach? So let&#8217;s try that.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Base<\/h2>\n\n\n\n<p>I like Brecht&#8217;s snippet that sets the stage nicely:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">select<\/span> {\n  <span class=\"hljs-attribute\">appearance<\/span>: none;\n  @supports (<span class=\"hljs-attribute\">appearance<\/span>: base-select) {\n    &amp;,\n    &amp;::<span class=\"hljs-built_in\">picker<\/span>(select) {\n      appearance: base-select;\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That&#8217;s saying:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>We&#8217;re going to wipe out the base styling anyway. Even browsers that don&#8217;t support the entire suite of custom styles for selects support styling the basic element itself, just not the &#8220;picker&#8221; part.<\/li>\n\n\n\n<li>In browsers that support it, we need to set <code>appearance: base-select;<\/code> to opt-in to the custom styleabtlity, and we need to do it both on the select itself and the picker, which uses this newfangled pseudo element.<\/li>\n<\/ol>\n\n\n\n<p>Minor aside: it&#8217;s interesting that the <code>appearance<\/code> value is <code>base-select<\/code> for now. In the hopefully-not-too-distant future, we&#8217;ll be opt-in &#8220;resetting&#8221; not just selects but <a href=\"https:\/\/www.youtube.com\/watch?v=WgSiqSqxTxw\">all the form elements with <code>appearance: base<\/code>.<\/a> But I guess that isn&#8217;t far enough along and may have been a slightly dangerous breaking change scenario, so it&#8217;s isolated to <code>base-select<\/code> for now. So be it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Glassy Look<\/h2>\n\n\n\n<p>We&#8217;ve got the ability now to style the <code>select<\/code> directly and a good amount of lienency to style it however we want. Here, a blurry background is applied and the dropdown arrow is applied with a background SVG. (This is Brecht&#8217;s cool idea and implementation, as a reminder.)<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">select<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: flex;\n  <span class=\"hljs-attribute\">justify-content<\/span>: space-between;\n  <span class=\"hljs-attribute\">min-width<\/span>: <span class=\"hljs-number\">300px<\/span>;\n  <span class=\"hljs-attribute\">align-items<\/span>: center;\n  <span class=\"hljs-attribute\">color<\/span>: white;\n  <span class=\"hljs-attribute\">padding-block<\/span>: <span class=\"hljs-number\">10px<\/span>;\n  <span class=\"hljs-attribute\">padding-inline<\/span>: <span class=\"hljs-number\">10px<\/span> <span class=\"hljs-number\">30px<\/span>;\n  <span class=\"hljs-attribute\">border<\/span>: <span class=\"hljs-number\">0<\/span>;\n  <span class=\"hljs-attribute\">border-radius<\/span>: <span class=\"hljs-number\">5px<\/span>;\n  <span class=\"hljs-attribute\">cursor<\/span>: pointer;\n  <span class=\"hljs-attribute\">font-weight<\/span>: <span class=\"hljs-number\">700<\/span>;\n  <span class=\"hljs-attribute\">backdrop-filter<\/span>: <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">5px<\/span>);\n  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">oklch<\/span>(<span class=\"hljs-number\">0.4764<\/span> <span class=\"hljs-number\">0.2094<\/span> <span class=\"hljs-number\">259.13<\/span> \/ <span class=\"hljs-number\">0.3<\/span>)\n    <span class=\"hljs-built_in\">url<\/span>(<span class=\"hljs-string\">\"data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23FFF' class='size-6'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='m19.5 8.25-7.5 7.5-7.5-7.5' \/%3E%3C\/svg%3E%0A\"<\/span>)\n    right <span class=\"hljs-number\">10px<\/span> center \/ <span class=\"hljs-number\">20px<\/span> no-repeat;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Even in Firefox, which doesn&#8217;t support <code>appearance: base-select<\/code>, we&#8217;ve got the look we&#8217;re after:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"832\" height=\"338\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.09.44%E2%80%AFAM.png?resize=832%2C338&#038;ssl=1\" alt=\"Custom select menu with a blurred background, showing the option 'One' and a dropdown arrow.\" class=\"wp-image-6405\" style=\"width:450px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.09.44%E2%80%AFAM.png?w=832&amp;ssl=1 832w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.09.44%E2%80%AFAM.png?resize=300%2C122&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.09.44%E2%80%AFAM.png?resize=768%2C312&amp;ssl=1 768w\" sizes=\"auto, (max-width: 832px) 100vw, 832px\" \/><\/figure>\n<\/div>\n\n\n<p>We have no ability to style the picker in Firefox or Safari (yet!) but <em>that&#8217;s totally fine. <\/em>We just get the default experience:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"878\" height=\"426\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.10.46%E2%80%AFAM.png?resize=878%2C426&#038;ssl=1\" alt=\"A custom select menu with a blurred glassy background featuring options: 'One' (selected), 'Two', 'Three', and 'Four'.\" class=\"wp-image-6406\" style=\"width:543px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.10.46%E2%80%AFAM.png?w=878&amp;ssl=1 878w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.10.46%E2%80%AFAM.png?resize=300%2C146&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.10.46%E2%80%AFAM.png?resize=768%2C373&amp;ssl=1 768w\" sizes=\"auto, (max-width: 878px) 100vw, 878px\" \/><\/figure>\n<\/div>\n\n\n<p>Our goal is to change up this experience on small screens, so it&#8217;s a little unfortunate this stuff isn&#8217;t in iOS yet (it is in Android!) but again, we just get the default experience which is fine:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"542\" height=\"1024\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.13.43%E2%80%AFAM.png?resize=542%2C1024&#038;ssl=1\" alt=\"A close-up view of a customizable select dropdown menu on an iPhone 15, displaying options 'One', 'Two', 'Three', and 'Four' with a blue gradient background.\" class=\"wp-image-6407\" style=\"width:292px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.13.43%E2%80%AFAM.png?resize=542%2C1024&amp;ssl=1 542w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.13.43%E2%80%AFAM.png?resize=159%2C300&amp;ssl=1 159w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.13.43%E2%80%AFAM.png?resize=768%2C1452&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.13.43%E2%80%AFAM.png?resize=813%2C1536&amp;ssl=1 813w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.13.43%E2%80%AFAM.png?resize=1083%2C2048&amp;ssl=1 1083w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-30-at-11.13.43%E2%80%AFAM.png?w=1130&amp;ssl=1 1130w\" sizes=\"auto, (max-width: 542px) 100vw, 542px\" \/><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\">The Picker Icon<\/h2>\n\n\n\n<p>We can start playing with, in a progressive enhancement friendly way, styling the custom &#8220;picker&#8221; now. Let&#8217;s do the icon first.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">select<\/span> {\n  ...\n\n  @supports (<span class=\"hljs-attribute\">appearance<\/span>: base-select) {\n    background: <span class=\"hljs-built_in\">oklch<\/span>(<span class=\"hljs-number\">0.4764<\/span> <span class=\"hljs-number\">0.2094<\/span> <span class=\"hljs-number\">259.13<\/span> \/ <span class=\"hljs-number\">0.3<\/span>);\n\n    &amp;:focus,\n    &amp;:hover {\n      <span class=\"hljs-attribute\">background-color<\/span>: <span class=\"hljs-built_in\">oklch<\/span>(<span class=\"hljs-number\">0.4764<\/span> <span class=\"hljs-number\">0.2094<\/span> <span class=\"hljs-number\">259.13<\/span> \/ <span class=\"hljs-number\">0.6<\/span>);\n    }\n    \n    &amp;<span class=\"hljs-selector-pseudo\">::picker-icon<\/span> {\n      <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\"<\/span>;\n      <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">20px<\/span>;\n      <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">20px<\/span>;\n      <span class=\"hljs-attribute\">background-image<\/span>: <span class=\"hljs-built_in\">url<\/span>(<span class=\"hljs-string\">\"data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23FFF' class='size-6'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='m19.5 8.25-7.5 7.5-7.5-7.5' \/%3E%3C\/svg%3E%0A\"<\/span>);\n      <span class=\"hljs-attribute\">transition<\/span>: rotate <span class=\"hljs-number\">0.2s<\/span> ease-out;\n    }\n\n    &amp;<span class=\"hljs-selector-pseudo\">:open<\/span><span class=\"hljs-selector-pseudo\">::picker-icon<\/span> {\n      <span class=\"hljs-attribute\">rotate<\/span>: <span class=\"hljs-number\">180deg<\/span>;\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>When the browser supports it, we&#8217;ll rip off the SVG background we were using for the dropdown arrow and apply it as the <code>::picker-icon<\/code> instead. That alone isn&#8217;t terribly useful, but now because we can target it individually, we can animate a rotation on it. That&#8217;s nice.<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player aligncenter wp-block-jetpack-videopress--has-max-width\" style=\"max-width: 258px;\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='529' src='https:\/\/videopress.com\/embed\/8d0oRuyB?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1739540970'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<h2 class=\"wp-block-heading\">The Picker<\/h2>\n\n\n\n<p>Styling the part that opens up when you active a select we&#8217;re calling the &#8220;picker&#8221;, and this is the part that&#8217;s completely new to be able to style. You get your hands on it with the somewhat unusual <code>select::picker(select)<\/code> selector. You have to put <code>select<\/code> in the pseudo function thing \u2014 it&#8217;s the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/::picker#parameters\">only valid value<\/a>. For now? Maybe it&#8217;s because in the future they&#8217;ll want to use <code>::picker<\/code> for date inputs or the like? Not sure but whatever.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">select<\/span> {\n  ... \n \n  @supports (<span class=\"hljs-attribute\">appearance<\/span>: base-select) {\n    ...\n\n    &amp;::<span class=\"hljs-built_in\">picker<\/span>(select) {\n\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We don&#8217;t really need much styling of the picker itself. That is, we want to <em>remove<\/em> the base styling by making the background transparent. The <code>option<\/code> elements themselves will have the <em>look<\/em>.<\/p>\n\n\n\n<p>This is where we&#8217;re going to do some interesting positioning, though. The way the <code>::picker<\/code> positions itself next to the select is: anchor positioning! Of course it is, might as well use the layout primitives baked into the browser. It does feel weird\/interesting to see at work though, as <strong>we need to be aware of it to change it.<\/strong> We&#8217;re going to wait for small screens, then attach the picker to the bottom of the screen. <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">select<\/span> {\n  ... \n \n  @supports (<span class=\"hljs-attribute\">appearance<\/span>: base-select) {\n    ...\n\n    &amp;::<span class=\"hljs-built_in\">picker<\/span>(select) {\n      background: transparent;\n\n      @media (width &lt; 400px) {\n        <span class=\"hljs-attribute\">position-anchor<\/span>: --html;\n        <span class=\"hljs-attribute\">bottom<\/span>: <span class=\"hljs-number\">0<\/span>;\n        <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n      }\n    }\n\n    <span class=\"hljs-selector-tag\">option<\/span> {\n      <span class=\"hljs-attribute\">backdrop-filter<\/span>: <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">12px<\/span>);\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player wp-block-jetpack-videopress--has-max-width\" style=\"max-width: 684px;\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='350' src='https:\/\/videopress.com\/embed\/5QLpeHjn?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1739540970'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>Again the theory there is small screens are often phones and we&#8217;re moving the picker down to make it more thumb-reachable. It&#8217;s an assumption. Maybe we should be thinking in terms of <code>@media (pointer: coarse)<\/code> or something, but I&#8217;ll leave that to you, we&#8217;re just playing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Animating<\/h2>\n\n\n\n<p>I&#8217;d rate this stuff as decently complicated to animate. Here&#8217;s some reasons:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The Shadow Root is at play here, making using DevTools to poke around in there while you&#8217;re working is a little extra cumbersome.<\/li>\n\n\n\n<li>The <code>::picker<\/code> is a display <code>none<\/code> to <code>block<\/code> change when it becomes visible, which means to animate it you need to remember <code>transition-behavior: allow-discrete<\/code> and how all that works.<\/li>\n\n\n\n<li>We&#8217;re also going to need <code>@starting-style<\/code> to get incoming animations, which can be repetitive. Plus some bonus staggering.<\/li>\n\n\n\n<li>We&#8217;ve got an <code>:open<\/code> state to mix in, <code>@media<\/code> queries to mix in, a <code>:checked<\/code> state for the options with a <code>::checkmark<\/code>, and other pseudos.<\/li>\n<\/ul>\n\n\n\n<p>All together, it just feels like <em>a lot<\/em>. It&#8217;s a lot of different nested state spread out. Even trying to organize it as nicely as possible, it&#8217;s hard to keep straight. The nesting is handy, but you can&#8217;t nest quite everything. Like the <code>:open<\/code> state is on the <code>select<\/code>, so you can&#8217;t style the <code>::picker<\/code> and then the open state within it, which would be handy for <code>@starting-style<\/code>, because you really need to write <code>select:open::picker(select)<\/code> not <code>select::picker(select):open<\/code> It&#8217;s fine it&#8217;s just a little bah humbug. <\/p>\n\n\n\n<p>Lemme just put the basics for the stagged in\/out animations for the <code>option<\/code> elements here for a taste:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">select<\/span> {\n  ... \n \n  @supports (<span class=\"hljs-attribute\">appearance<\/span>: base-select) {\n    ...\n\n    option {\n      ... \n\n      transition-property: opacity, scale;\n      <span class=\"hljs-attribute\">transition-duration<\/span>: <span class=\"hljs-number\">0.2s<\/span>;\n      <span class=\"hljs-attribute\">transition-delay<\/span>: <span class=\"hljs-built_in\">calc<\/span>((sibling-count() - <span class=\"hljs-built_in\">sibling-index<\/span>()) * <span class=\"hljs-number\">100ms<\/span>);\n      <span class=\"hljs-attribute\">scale<\/span>: <span class=\"hljs-number\">0.25<\/span>;\n      <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n    }\n\n    &amp;<span class=\"hljs-selector-pseudo\">:open<\/span> {\n      option {\n        <span class=\"hljs-attribute\">scale<\/span>: <span class=\"hljs-number\">1<\/span>;\n        <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">1<\/span>;\n        <span class=\"hljs-attribute\">transition-delay<\/span>: <span class=\"hljs-built_in\">calc<\/span>(sibling-index() * <span class=\"hljs-number\">100ms<\/span>);\n\n        @starting-style {\n          <span class=\"hljs-attribute\">scale<\/span>: <span class=\"hljs-number\">0.25<\/span>;\n          <span class=\"hljs-attribute\">opacity<\/span>: <span class=\"hljs-number\">0<\/span>;\n        }\n      }\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>See above it was necessary to repeat the <code>option<\/code> selector. Not a huge deal, but you usually expect to avoid that with nesting. Plus the <code>@starting-style<\/code> thing can feel repetitive, but that&#8217;s offering the possibility of different in-and-out styling so it&#8217;s ultimately a good thing.<\/p>\n\n\n\n<p>The staggered \/ scale \/ fade-in thing feels nice to me, and particularly nice when they skoosh up from the bottom anchored position.<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player wp-block-jetpack-videopress--has-max-width\" style=\"max-width: 674px;\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='435' src='https:\/\/videopress.com\/embed\/vZrYxEv1?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=1&amp;persistVolume=0&amp;playsinline=1&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1739540970'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<h2 class=\"wp-block-heading\">Demo<\/h2>\n\n\n\n<p>There&#8217;s a bunch more CSS tucked in there to make it all happen, so you might as well have the whole thing here:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_dPovqzz\" src=\"\/\/codepen.io\/editor\/anon\/embed\/dPovqzz?height=450&amp;theme-id=1&amp;slug-hash=dPovqzz&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed dPovqzz\" title=\"CodePen Embed dPovqzz\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n","protected":false},"excerpt":{"rendered":"<p>You&#8217;ve got A LOT of control over the design of select menus now, and it can be done as a progressive enhancement.<\/p>\n","protected":false},"author":1,"featured_media":6429,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[100,7,50,361],"class_list":["post-6293","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-animation","tag-css","tag-select","tag-stagger"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/Custom-Select-that-comes-up-from-the-bottom-on-mobile.jpg?fit=1140%2C676&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6293","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=6293"}],"version-history":[{"count":13,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6293\/revisions"}],"predecessor-version":[{"id":6424,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6293\/revisions\/6424"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/6429"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=6293"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=6293"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=6293"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}