[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-customizable-select-appearance-base-select":44,"search-suggestions":60,"quiz-article-customizable-select-appearance-base-select":107},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a0a9ea-93f0-711f-a04e-b83d7e173ebe","customizable-select-appearance-base-select","PRACTICE_QUIZ","Customizable select — test yourself","Eight questions on appearance: base-select — what it actually unlocks, why it fixes the popup-clipping bug hand-built dropdowns hit, and what stays exactly the same underneath.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",107,{"slug":66,"name":67,"articles":68},"javascript","Javascript",89,{"slug":70,"name":71,"articles":72},"frontend","Frontend",72,{"slug":74,"name":75,"articles":76},"css","Css",36,{"slug":78,"name":79,"articles":80},"tutorial","Tutorial",35,{"slug":82,"name":83,"articles":84},"typescript","Typescript",15,{"slug":86,"name":87,"articles":88},"react","React",13,{"slug":90,"name":91,"articles":92},"performance","Performance",12,{"slug":94,"name":95,"articles":96},"browser","Browser",9,{"slug":98,"name":99,"articles":96},"node","Node",{"slug":101,"name":102,"articles":103},"html","Html",7,{"slug":105,"name":106,"articles":103},"accessibility","Accessibility",{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":112,"publishedAt":113,"viewCount":114,"likeCount":19,"commentCount":19,"author":115,"vertical":120,"topic":121,"tags":123,"_count":128,"playground":130,"body":132,"bodyMd":256,"seo":257,"translationGroupId":260,"series":52,"podcastUrl":52,"verticalId":5,"thread":261,"assessments":263,"translations":266,"quiz":268},"01a0a9ea-93a3-7734-b359-6d7cc4df502b","The Dropdown You've Been Faking for a Decade","Every hand-built 'custom select' with icons and a styled popup exists because the real \u003Cselect> couldn't do that. appearance: base-select just closed the gap — without you losing the native picker's keyboard nav and mobile UI.","\u002Fmedia\u002Fcovers\u002Fcustomizable-select-appearance-base-select.png",4,"2026-09-22T19:36:39.450Z",32,{"id":116,"name":117,"username":118,"avatarUrl":52,"headline":119},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":74,"name":122},"CSS",[124,125,126,127],{"slug":74,"name":75,"color":52},{"slug":101,"name":102,"color":52},{"slug":62,"name":63,"color":52},{"slug":105,"name":106,"color":52},{"assessments":129},1,{"slug":46,"title":131},"The clipped dropdown — interactive playground",{"blocks":133,"version":129},[134,138,141,145,148,153,156,159,162,165,168,171,174,177,180,183,186,190,193,197,200,203,206,209,213,216,219,222,225,228,231,234,237,240,243,246,249],{"id":135,"html":136,"type":137},"b1","\u003Cp>Design wants a country picker. Nothing fancy — a dropdown, a small flag icon next to each name. You reach for \u003Ccode>&lt;select&gt;\u003C\u002Fcode>, type the first \u003Ccode>&lt;option&gt;\u003C\u002Fcode>, and hit the wall every frontend dev hits eventually: an \u003Ccode>&lt;option&gt;\u003C\u002Fcode> can only ever show text. No \u003Ccode>&lt;img&gt;\u003C\u002Fcode>, no icon span, nothing. The browser renders whatever markup you put inside it as a plain string.\u003C\u002Fp>","paragraph",{"id":139,"html":140,"type":137},"b2","\u003Cp>So you do what everyone does. You build a fake one.\u003C\u002Fp>",{"id":142,"html":143,"text":143,"type":144,"level":31},"b3","The dropdown you build instead","heading",{"id":146,"html":147,"type":137},"b4","\u003Cp>A \u003Ccode>&lt;div role=&quot;listbox&quot;&gt;\u003C\u002Fcode>, a button that toggles it, \u003Ccode>&lt;div role=&quot;option&quot;&gt;\u003C\u002Fcode> children with real icons inside, arrow-key handlers, a click-outside listener to close it. It&#39;s maybe 150 lines. It works in the demo. Ship it.\u003C\u002Fp>",{"id":149,"code":150,"type":151,"language":101,"highlight":152},"b5","\u003Cdiv class=\"select\" id=\"country-select\">\n  \u003Cbutton aria-haspopup=\"listbox\" aria-expanded=\"false\">🇨🇦 Canada\u003C\u002Fbutton>\n  \u003Cdiv role=\"listbox\" hidden>\n    \u003Cdiv role=\"option\" data-value=\"ca\">🇨🇦 Canada\u003C\u002Fdiv>\n    \u003Cdiv role=\"option\" data-value=\"jp\">🇯🇵 Japan\u003C\u002Fdiv>\n    \u003Cdiv role=\"option\" data-value=\"br\">🇧🇷 Brazil\u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>","code",[],{"id":154,"html":155,"type":137},"b6","\u003Cp>Then someone drops this select inside a card component — rounded corners, \u003Ccode>overflow: hidden\u003C\u002Fcode> so a stray image never bleeds past the border radius. Totally normal CSS. And now the popup, which is just a child \u003Ccode>&lt;div&gt;\u003C\u002Fcode> positioned \u003Ccode>absolute\u003C\u002Fcode>, gets sliced off at the card&#39;s edge the moment it&#39;s taller than the space below the trigger.\u003C\u002Fp>",{"id":157,"html":158,"text":158,"type":144,"level":31},"b7","Try it — watch the fix disappear back into the bug",{"id":160,"html":161,"type":137},"b8","\u003C!-- playground:start -->",{"id":163,"html":164,"text":164,"type":144,"level":31},"b9","🎮 Try it yourself",{"id":166,"html":167,"type":137},"b10","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcustomizable-select-appearance-base-select\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":169,"html":170,"type":137},"b11","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":172,"html":173,"type":137},"b12","\u003C!-- playground:end -->",{"id":175,"html":176,"type":137},"b13","\u003Cp>You didn&#39;t do anything wrong. \u003Ccode>overflow: hidden\u003C\u002Fcode> clips \u003Cem>any\u003C\u002Fem> descendant, and a hand-built popup is, structurally, just a descendant. The standard escape hatch is \u003Ccode>position: fixed\u003C\u002Fcode> plus \u003Ccode>getBoundingClientRect()\u003C\u002Fcode> to glue the popup under the trigger by hand — and now you&#39;re also re-running that math on every \u003Ccode>scroll\u003C\u002Fcode> and \u003Ccode>resize\u003C\u002Fcode> event, and remembering to tear the listeners down when the select unmounts. None of this is exotic. It&#39;s just work the native \u003Ccode>&lt;select&gt;\u003C\u002Fcode> used to do for you, that you signed back up for the moment you needed one icon.\u003C\u002Fp>",{"id":178,"html":179,"type":137},"b14","\u003Cp>And the part that doesn&#39;t show up in a demo video: keyboard behavior. Type-ahead (press &quot;j&quot; and jump to &quot;Japan&quot;), Home\u002FEnd, wraparound at the list&#39;s edges, closing on Escape and returning focus to the trigger, announcing the right state to a screen reader as options are highlighted — a real \u003Ccode>&lt;select&gt;\u003C\u002Fcode> gets every one of those for free, in every browser, on every OS. The \u003Ca href=\"https:\u002F\u002Fwww.w3.org\u002FWAI\u002FARIA\u002Fapg\u002Fpatterns\u002Flistbox\u002F\">WAI-ARIA Authoring Practices listbox pattern\u003C\u002Fa> exists as a reference implementation precisely because getting all of that right by hand, consistently, is genuinely hard. Most homegrown dropdowns implement a fraction of it and call it done.\u003C\u002Fp>",{"id":181,"html":182,"text":182,"type":144,"level":31},"b15","What actually changed",{"id":184,"html":185,"type":137},"b16","\u003Cp>\u003Ccode>&lt;select&gt;\u003C\u002Fcode> now has an opt-in that unlocks styling without giving up any of that native behavior:\u003C\u002Fp>",{"id":187,"code":188,"type":151,"language":74,"highlight":189},"b17","select {\n  appearance: base-select;\n}\n\n\u002F* the popup is now a real, styleable target *\u002F\nselect::picker(select) {\n  border-radius: 12px;\n  border: 1px solid #2a2f3a;\n}",[],{"id":191,"html":192,"type":137},"b18","\u003Cp>Two things happen the moment you set \u003Ccode>appearance: base-select\u003C\u002Fcode>. First, \u003Ccode>&lt;option&gt;\u003C\u002Fcode> stops being text-only — it can hold real markup, so the flag icon goes right back where you originally wanted it:\u003C\u002Fp>",{"id":194,"code":195,"type":151,"language":101,"highlight":196},"b19","\u003Cselect>\n  \u003Cbutton>\n    \u003Cselectedcontent>\u003C\u002Fselectedcontent>\n  \u003C\u002Fbutton>\n  \u003Coption value=\"ca\">\u003Cimg src=\"\u002Fflags\u002Fca.svg\" alt=\"\" \u002F> Canada\u003C\u002Foption>\n  \u003Coption value=\"jp\">\u003Cimg src=\"\u002Fflags\u002Fjp.svg\" alt=\"\" \u002F> Japan\u003C\u002Foption>\n  \u003Coption value=\"br\">\u003Cimg src=\"\u002Fflags\u002Fbr.svg\" alt=\"\" \u002F> Brazil\u003C\u002Foption>\n\u003C\u002Fselect>",[],{"id":198,"html":199,"type":137},"b20","\u003Cp>That \u003Ccode>&lt;selectedcontent&gt;\u003C\u002Fcode> element is new too — it mirrors whatever content the chosen \u003Ccode>&lt;option&gt;\u003C\u002Fcode> holds into the closed button, so the flag shows up on the trigger, not just inside the open popup.\u003C\u002Fp>",{"id":201,"html":202,"type":137},"b21","\u003Cp>Second, and this is the part that actually kills the clipping bug from the wrong-way section: \u003Ccode>::picker(select)\u003C\u002Fcode> doesn&#39;t render as a descendant \u003Ccode>&lt;div&gt;\u003C\u002Fcode> at all. It&#39;s promoted to the \u003Cstrong>top layer\u003C\u002Fstrong> — the same rendering layer \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode> and the Popover API use — so it paints above the entire page, unclipped by any ancestor&#39;s \u003Ccode>overflow: hidden\u003C\u002Fcode>, with no \u003Ccode>z-index\u003C\u002Fcode> or \u003Ccode>position: fixed\u003C\u002Fcode> math required. The exact bug in the playground above simply can&#39;t happen to it, because it was never inside the clipping container to begin with.\u003C\u002Fp>",{"id":204,"html":205,"text":205,"type":144,"level":31},"b22","The honest caveat",{"id":207,"html":208,"type":137},"b23","\u003Cp>This is a real, working CSS feature — not a proposal — but support isn&#39;t universal yet. It shipped first in Chromium browsers; Firefox and Safari have it in progress at various stages. Check \u003Ca href=\"https:\u002F\u002Fcaniuse.com\u002Fmdn-css_properties_appearance_base-select\">caniuse\u003C\u002Fa> before you rely on it, and reach for it as \u003Cstrong>progressive enhancement\u003C\u002Fstrong>: wrap the opt-in behind \u003Ccode>@supports (appearance: base-select)\u003C\u002Fcode> and let browsers without support fall back to a plain \u003Ccode>&lt;select&gt;\u003C\u002Fcode>, which still works, still submits, still has icon-free but fully accessible options. You lose the flags, not the form.\u003C\u002Fp>",{"id":210,"code":211,"type":151,"language":74,"highlight":212},"b24","@supports (appearance: base-select) {\n  select { appearance: base-select; }\n}",[],{"id":214,"html":215,"type":137},"b25","\u003Cp>That&#39;s the whole migration. No polyfill, no JS fallback path to maintain — the feature detection \u003Cem>is\u003C\u002Fem> the fallback.\u003C\u002Fp>",{"id":217,"html":218,"type":137},"b26","\u003C!-- quiz:start -->",{"id":220,"html":221,"text":221,"type":144,"level":31},"b27","🧠 Test yourself",{"id":223,"html":224,"type":137},"b28","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcustomizable-select-appearance-base-select\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":226,"html":227,"type":137},"b29","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":229,"html":230,"type":137},"b30","\u003C!-- quiz:end -->",{"id":232,"html":233,"text":233,"type":144,"level":31},"b31","The payoff",{"id":235,"html":236,"type":137},"b32","\u003Cp>The custom \u003Ccode>&lt;div role=&quot;listbox&quot;&gt;\u003C\u002Fcode> component your team maintains — the one with the scroll listener and the \u003Ccode>getBoundingClientRect()\u003C\u002Fcode> call and the open GitHub issue about it not closing right on iOS — was never really a design requirement. It was a workaround for \u003Ccode>&lt;option&gt;\u003C\u002Fcode> being text-only and the popup being an ordinary, clippable \u003Ccode>&lt;div&gt;\u003C\u002Fcode>. Both of those are gone now, behind one CSS property and one new element.\u003C\u002Fp>",{"id":238,"html":239,"type":137},"b33","\u003Cp>Are you still shipping a hand-rolled dropdown for something this small, or are you already behind \u003Ccode>@supports\u003C\u002Fcode> on the real thing? I&#39;d genuinely like to know which.\u003C\u002Fp>",{"id":241,"type":242},"b34","divider",{"id":244,"html":245,"type":137},"b35","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":247,"html":248,"type":137},"b36","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":250,"type":251,"items":252,"ordered":18},"b37","list",[253,254,255],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Design wants a country picker. Nothing fancy — a dropdown, a small flag icon next to each name. You reach for `\u003Cselect>`, type the first `\u003Coption>`, and hit the wall every frontend dev hits eventually: an `\u003Coption>` can only ever show text. No `\u003Cimg>`, no icon span, nothing. The browser renders whatever markup you put inside it as a plain string.\n\nSo you do what everyone does. You build a fake one.\n\n## The dropdown you build instead\n\nA `\u003Cdiv role=\"listbox\">`, a button that toggles it, `\u003Cdiv role=\"option\">` children with real icons inside, arrow-key handlers, a click-outside listener to close it. It's maybe 150 lines. It works in the demo. Ship it.\n\n```html\n\u003Cdiv class=\"select\" id=\"country-select\">\n  \u003Cbutton aria-haspopup=\"listbox\" aria-expanded=\"false\">🇨🇦 Canada\u003C\u002Fbutton>\n  \u003Cdiv role=\"listbox\" hidden>\n    \u003Cdiv role=\"option\" data-value=\"ca\">🇨🇦 Canada\u003C\u002Fdiv>\n    \u003Cdiv role=\"option\" data-value=\"jp\">🇯🇵 Japan\u003C\u002Fdiv>\n    \u003Cdiv role=\"option\" data-value=\"br\">🇧🇷 Brazil\u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n```\n\nThen someone drops this select inside a card component — rounded corners, `overflow: hidden` so a stray image never bleeds past the border radius. Totally normal CSS. And now the popup, which is just a child `\u003Cdiv>` positioned `absolute`, gets sliced off at the card's edge the moment it's taller than the space below the trigger.\n\n## Try it — watch the fix disappear back into the bug\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcustomizable-select-appearance-base-select\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\nYou didn't do anything wrong. `overflow: hidden` clips *any* descendant, and a hand-built popup is, structurally, just a descendant. The standard escape hatch is `position: fixed` plus `getBoundingClientRect()` to glue the popup under the trigger by hand — and now you're also re-running that math on every `scroll` and `resize` event, and remembering to tear the listeners down when the select unmounts. None of this is exotic. It's just work the native `\u003Cselect>` used to do for you, that you signed back up for the moment you needed one icon.\n\nAnd the part that doesn't show up in a demo video: keyboard behavior. Type-ahead (press \"j\" and jump to \"Japan\"), Home\u002FEnd, wraparound at the list's edges, closing on Escape and returning focus to the trigger, announcing the right state to a screen reader as options are highlighted — a real `\u003Cselect>` gets every one of those for free, in every browser, on every OS. The [WAI-ARIA Authoring Practices listbox pattern](https:\u002F\u002Fwww.w3.org\u002FWAI\u002FARIA\u002Fapg\u002Fpatterns\u002Flistbox\u002F) exists as a reference implementation precisely because getting all of that right by hand, consistently, is genuinely hard. Most homegrown dropdowns implement a fraction of it and call it done.\n\n## What actually changed\n\n`\u003Cselect>` now has an opt-in that unlocks styling without giving up any of that native behavior:\n\n```css\nselect {\n  appearance: base-select;\n}\n\n\u002F* the popup is now a real, styleable target *\u002F\nselect::picker(select) {\n  border-radius: 12px;\n  border: 1px solid #2a2f3a;\n}\n```\n\nTwo things happen the moment you set `appearance: base-select`. First, `\u003Coption>` stops being text-only — it can hold real markup, so the flag icon goes right back where you originally wanted it:\n\n```html\n\u003Cselect>\n  \u003Cbutton>\n    \u003Cselectedcontent>\u003C\u002Fselectedcontent>\n  \u003C\u002Fbutton>\n  \u003Coption value=\"ca\">\u003Cimg src=\"\u002Fflags\u002Fca.svg\" alt=\"\" \u002F> Canada\u003C\u002Foption>\n  \u003Coption value=\"jp\">\u003Cimg src=\"\u002Fflags\u002Fjp.svg\" alt=\"\" \u002F> Japan\u003C\u002Foption>\n  \u003Coption value=\"br\">\u003Cimg src=\"\u002Fflags\u002Fbr.svg\" alt=\"\" \u002F> Brazil\u003C\u002Foption>\n\u003C\u002Fselect>\n```\n\nThat `\u003Cselectedcontent>` element is new too — it mirrors whatever content the chosen `\u003Coption>` holds into the closed button, so the flag shows up on the trigger, not just inside the open popup.\n\nSecond, and this is the part that actually kills the clipping bug from the wrong-way section: `::picker(select)` doesn't render as a descendant `\u003Cdiv>` at all. It's promoted to the **top layer** — the same rendering layer `\u003Cdialog>` and the Popover API use — so it paints above the entire page, unclipped by any ancestor's `overflow: hidden`, with no `z-index` or `position: fixed` math required. The exact bug in the playground above simply can't happen to it, because it was never inside the clipping container to begin with.\n\n## The honest caveat\n\nThis is a real, working CSS feature — not a proposal — but support isn't universal yet. It shipped first in Chromium browsers; Firefox and Safari have it in progress at various stages. Check [caniuse](https:\u002F\u002Fcaniuse.com\u002Fmdn-css_properties_appearance_base-select) before you rely on it, and reach for it as **progressive enhancement**: wrap the opt-in behind `@supports (appearance: base-select)` and let browsers without support fall back to a plain `\u003Cselect>`, which still works, still submits, still has icon-free but fully accessible options. You lose the flags, not the form.\n\n```css\n@supports (appearance: base-select) {\n  select { appearance: base-select; }\n}\n```\n\nThat's the whole migration. No polyfill, no JS fallback path to maintain — the feature detection *is* the fallback.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcustomizable-select-appearance-base-select\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## The payoff\n\nThe custom `\u003Cdiv role=\"listbox\">` component your team maintains — the one with the scroll listener and the `getBoundingClientRect()` call and the open GitHub issue about it not closing right on iOS — was never really a design requirement. It was a workaround for `\u003Coption>` being text-only and the popup being an ordinary, clippable `\u003Cdiv>`. Both of those are gone now, behind one CSS property and one new element.\n\nAre you still shipping a hand-rolled dropdown for something this small, or are you already behind `@supports` on the real thing? I'd genuinely like to know which.\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":109,"canonical":258,"description":259},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcustomizable-select-appearance-base-select","Every hand-built 'custom select' with icons and a styled popup exists because the real \u003Cselect> couldn't do that. appearance: base-select just closed the gap — without you losing t","01a0a9ea-93a3-7734-b359-71ca4f33b00a",{"id":262,"locked":18},"01a0a9ea-93d2-77a9-8c0a-f4532dff8d71",[264],{"id":45,"slug":46,"title":48,"_count":265},{"questions":51},[267],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":269,"questionCount":51},{"questions":51}]