Glassmorphism Dropdown Menu: Frosted Select Component
Build a frosted glass dropdown menu with backdrop-filter, rgba backgrounds, and Tailwind v4. Real code, real values — no fluff.
Why Glassmorphism Works So Well for Dropdowns
Honestly, frosted glass is one of those effects that makes a dropdown feel like it actually *belongs* on screen rather than being pasted on top. The translucency lets the background bleed through — gradients, images, other UI elements — so the menu doesn't create a jarring visual cut.
Standard dropdowns with solid backgrounds have a stacking problem. You pop one open and suddenly there's this opaque rectangle blocking a chunk of your UI. With glassmorphism, the backdrop-filter blur keeps visual continuity. Users don't lose context of where they are on the page.
It's not just aesthetics either. The blur effect communicates depth. The dropdown reads as a layer above the content — you get the same spatial signal you'd get from a shadow, but more elegant. That's why you see this pattern everywhere from macOS menus to SaaS dashboards.
The CSS Foundation: backdrop-filter and rgba
Before writing a single line of JSX, you need to understand what's actually happening under the hood. Glassmorphism dropdowns rely on two CSS properties working together: backdrop-filter: blur() and a semi-transparent background-color. Get either one wrong and the effect falls apart.
The background should use rgba(255,255,255,0.12) for light glass on dark backgrounds, or rgba(255,255,255,0.08) if you want something more subtle. For dark glass on lighter backgrounds, flip to rgba(0,0,0,0.25). The blur radius is usually somewhere between 8px and 20px — anything lower looks like a mistake, anything higher starts looking muddy.
One thing that trips people up: backdrop-filter only works if the element has some transparency. If you set background: rgba(255,255,255,1.0) you'll never see the blur because the opaque fill covers everything behind it. Also, don't forget the -webkit-backdrop-filter prefix for Safari — as of 2026 it still needs it on some builds.
If you want to go deeper on the theory behind this visual style, what glassmorphism is and where it came from is worth a read before you start customizing values.
Building the Frosted Dropdown in React + Tailwind v4
Here's the full component. It uses Tailwind v4.0.2 arbitrary value syntax for the backdrop blur and rgba background. The [backdrop-filter:blur(12px)] class is the key piece — without it you've just got a transparent box, not frosted glass.
import { useState, useRef, useEffect } from 'react'
type Option = { label: string; value: string }
interface GlassDropdownProps {
options: Option[]
placeholder?: string
onChange?: (value: string) => void
}
export function GlassDropdown({
options,
placeholder = 'Select an option',
onChange,
}: GlassDropdownProps) {
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState<Option | null>(null)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
function handleSelect(option: Option) {
setSelected(option)
setOpen(false)
onChange?.(option.value)
}
return (
<div ref={ref} className="relative w-64">
{/* Trigger button */}
<button
onClick={() => setOpen(!open)}
className="
w-full px-4 py-3 rounded-xl
flex items-center justify-between
[background:rgba(255,255,255,0.12)]
[backdrop-filter:blur(12px)]
[-webkit-backdrop-filter:blur(12px)]
border border-white/20
text-white text-sm font-medium
transition-all duration-200
hover:[background:rgba(255,255,255,0.18)]
focus:outline-none focus:ring-2 focus:ring-white/30
"
>
<span className={selected ? 'text-white' : 'text-white/50'}>
{selected ? selected.label : placeholder}
</span>
<svg
className={`w-4 h-4 text-white/70 transition-transform duration-200 ${
open ? 'rotate-180' : ''
}`}
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Dropdown panel */}
{open && (
<div
className="
absolute z-50 mt-2 w-full
rounded-xl overflow-hidden
[background:rgba(255,255,255,0.10)]
[backdrop-filter:blur(16px)]
[-webkit-backdrop-filter:blur(16px)]
border border-white/15
shadow-[0_8px_32px_rgba(0,0,0,0.3)]
"
>
<ul className="py-1">
{options.map((opt) => (
<li key={opt.value}>
<button
onClick={() => handleSelect(opt)}
className="
w-full px-4 py-2.5 text-left text-sm
text-white/80 hover:text-white
hover:[background:rgba(255,255,255,0.12)]
transition-colors duration-150
flex items-center gap-3
"
>
{selected?.value === opt.value && (
<span className="w-1.5 h-1.5 rounded-full bg-white inline-block" />
)}
{opt.label}
</button>
</li>
))}
</ul>
</div>
)}
</div>
)
}A few things to notice: the trigger and the panel use different blur values — 12px on the trigger and 16px on the panel. The panel sits further from the background content visually, so it benefits from slightly more blur. Also the shadow-[0_8px_32px_rgba(0,0,0,0.3)] gives the panel its sense of elevation without needing a solid border.
Animating the Dropdown Open/Close State
The component above uses conditional rendering ({open && ...}) which works, but you lose the ability to animate the close transition. If you want a smooth fade+slide, you'll need to keep the panel mounted and toggle visibility with CSS.
The cleanest approach in Tailwind v4 is using data-* attributes on the panel and targeting them with variants. Set data-open={open} on the wrapper div and write data-[open=true]:opacity-100 data-[open=false]:opacity-0 alongside a transition-opacity duration-150. Pair that with translate-y-1 → translate-y-0 for the slide.
Alternatively, if you're already using Framer Motion in your project, wrapping the panel in <AnimatePresence> with a motion.div that has initial={{ opacity: 0, y: -4 }} and animate={{ opacity: 1, y: 0 }} gives you effortless enter/exit animations at roughly duration: 0.15. It's not overkill if you're already importing it elsewhere — just don't add the whole library solely for this.
The animation approach connects directly to how you're handling theme toggling in React — if you've got a dark/light mode system, your rgba values need to shift too. rgba(255,255,255,0.12) for dark mode, something like rgba(0,0,0,0.08) for light mode backgrounds.
Accessibility You Can't Skip
Does your glassmorphism dropdown actually work for keyboard users? This is where a lot of visual-first UI falls down. The frosted glass effect is gorgeous, but if someone's navigating with a keyboard or screen reader, they don't care about backdrop-filter.
At minimum: the trigger button needs aria-expanded={open} and aria-haspopup="listbox". The dropdown panel should be role="listbox" with each option as role="option" and aria-selected. Tab focus should cycle through the options, and pressing Escape should close the panel and return focus to the trigger.
The white text on a translucent background is a contrast trap too. rgba(255,255,255,0.12) on a dark gradient can easily fail WCAG AA (4.5:1 ratio for normal text). Run your color combinations through a contrast checker before shipping. A text-white label reads fine, but text-white/50 placeholder text on rgba(255,255,255,0.12) is going to fail — bump the blur or add a slightly darker backing.
Compare how the glass style handles contrast vs. alternatives in glassmorphism vs neumorphism — the contrast issues are actually different between the two styles in ways that affect component design.
Browser Support and the Safari Situation
As of late 2026, backdrop-filter has solid support across Chrome, Firefox, and Edge. Safari still requires -webkit-backdrop-filter — the standard property alone won't do it on iOS Safari or macOS Safari without the prefix. Both classes are included in the code above for that reason.
Firefox had limited backdrop-filter support for years but it's been stable since v103. The one environment where it still fails completely is Firefox with privacy.resistFingerprinting enabled in about:config. That's an edge case, but if you're building something security-sensitive, have a fallback ready — a solid rgba(20,20,20,0.9) background degrades gracefully without breaking the layout.
If backdrop-filter isn't available, the element still renders — it just won't have the blur effect. The border and semi-transparent background still give you a decent fallback. You can detect support with @supports (backdrop-filter: blur(1px)) in CSS and apply enhanced styles only where it works.
For the full picture of what free glassmorphism components are available and how they handle these compatibility issues, that article covers several libraries side by side.
Multi-Select and Search Variants
The base dropdown component covers single selection. Multi-select changes the data model — selected becomes a Set<string> and each option toggle calls setSelected(prev => new Set(prev.has(v) ? [...prev].filter(x => x !== v) : [...prev, v])). The visual indicator shifts from a dot to a checkbox or a filled pill.
Adding search is straightforward: put a text input at the top of the panel with autoFocus when open becomes true, and filter options against the input value before rendering the list. Keep the input's background: transparent and border-b: 1px solid rgba(255,255,255,0.15) so it integrates into the glass panel without breaking the aesthetic.
For large option lists (50+ items), consider virtualizing the list with @tanstack/virtual to avoid rendering 200 DOM nodes every time the dropdown opens. The glass effect doesn't change the performance characteristics of the list — it's still a bunch of <li> elements. The blur itself is GPU-accelerated and cheap; the DOM is what'll slow you down.
Putting It Together in a Real Layout
The glassmorphism dropdown looks best when the background has some visual complexity — a gradient, an image, or a particles background effect that gives the blur something to work with. On a plain white or solid gray background, the blur effect is invisible and you've just got a low-contrast box.
Position the parent container with position: relative and make sure it has a defined stacking context if you're using transforms or opacity on it. The dropdown panel uses z-50 in the example above — adjust that based on your layout's z-index scale. If you've got modals or toasts at z-100, you're fine. If your nav bar is at z-50, you'll have a collision.
One last practical detail: the 8px gap between the trigger and the dropdown panel (via mt-2) is intentional. It gives the panel room to animate in without appearing to pop out of the button. Too tight and the enter animation looks cramped. Too much space and they feel visually disconnected.
The component fits into Empire UI's glassmorphism style system out of the box. If you want to see how glassmorphism compares to the clay and brutal styles available in the library, claymorphism and neobrutalism take very different approaches to the same depth-and-texture problem.
FAQ
Safari requires the -webkit-backdrop-filter prefix alongside the standard backdrop-filter property. In Tailwind v4 arbitrary value syntax, add both [backdrop-filter:blur(12px)] and [-webkit-backdrop-filter:blur(12px)] as separate classes on the element.
For dark-background layouts, rgba(255,255,255,0.10) to rgba(255,255,255,0.15) works well. For lighter backgrounds, try rgba(0,0,0,0.08) to rgba(0,0,0,0.15). The exact value depends on what's behind the element — test with your actual background content, not a placeholder.
You'll need two sets of rgba values. In dark mode use white-based transparency (rgba(255,255,255,0.12)), in light mode use black-based transparency (rgba(0,0,0,0.06)). With Tailwind you can do this with dark:[background:rgba(255,255,255,0.12)] [background:rgba(0,0,0,0.06)] on the element.
This is usually a GPU compositing issue. Make sure the element's parent doesn't have overflow: hidden or transform: translateZ(0) applied unexpectedly, as these can create stacking context issues. Also check that you're not applying will-change: transform to an ancestor that conflicts with the backdrop-filter compositing layer.
Yes. The GlassDropdown component calls onChange with the string value when a selection is made. Wire it to React Hook Form via Controller: <Controller name="field" control={control} render={({ field }) => <GlassDropdown options={opts} onChange={field.onChange} />} />. Zod schema validation works the same as any other string field.
On modern iOS and Android devices it's GPU-accelerated and performs fine. Older mid-range Android phones (2021 and earlier) can struggle with multiple blurred elements on the same screen. If you're targeting low-end devices, reduce the blur radius to 8px or less, or skip backdrop-filter entirely and use a slightly opaque solid background as fallback.