Select
A select with a glass trigger and a glass popup, kept from overlapping each other.
Installation
npx shadcn@latest add https://liqui.design/r/select.jsonInstalling more than one? Register the namespace once in components.json and
drop the URLs:
{
"registries": {
"@liqui-design": "https://liqui.design/r/{name}.json"
}
}npx shadcn@latest add @liqui-design/selectUsage
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';<Select defaultValue="squircle">
<SelectTrigger>
<SelectValue placeholder="Choose a profile" />
</SelectTrigger>
<SelectContent>
<SelectItem value="squircle">Squircle</SelectItem>
<SelectItem value="convex">Convex</SelectItem>
</SelectContent>
</Select>The full anatomy also ships SelectGroup, SelectGroupLabel and
SelectSeparator. SelectContent renders the portal, the positioner, the glass
popup and Select.List — pass positioner props (side, align, sideOffset)
straight to it.
Notes
The popup does not overlap the trigger
Base UI's select positions the popup over the trigger by default
(alignItemWithTrigger), lining the selected row up with the value text. That
is correct macOS behaviour and the wrong thing for glass: the popup's backdrop
would then include the trigger's own tint and specular rim, so the top of the
list refracts a piece of UI instead of the page — and the trigger vanishes under
a surface built to look exactly like it.
SelectContent defaults the prop off and drops the list below the trigger
instead. Pass it back if you want the native behaviour:
<SelectContent alignItemWithTrigger />Two glass surfaces stacking is not always wrong — the alert dialog does it deliberately over its scrim. What makes it wrong here is that the lower surface is also a control, and the upper one hides it while refracting it.
This popup cannot be kept mounted
Context Menu keeps its
portal in the DOM so the decoded displacement map survives close. Select.Portal
returns null while closed, so that trick is unavailable here.
It matters less than it sounds. The map is cached module-wide by size and optics, and the SVG filter lives in a registry that outlives any popup, so only the very first open pays for generation and decode. Reopens reference an already-decoded filter and arrive refracted on the first frame.
The trigger is not a native button
<BaseSelect.Trigger nativeButton={false} render={<LiquiGlass … />} />Same constraint as Button: the glass anatomy is a
stack of divs, and a native <button> only accepts phrasing content. Base UI
supplies role, tabIndex and the key handling once the native element is
opted out. Base UI's own default for Select.Trigger is nativeButton: true,
so this has to be set explicitly — leaving it out produces invalid HTML and a
console warning.
Scrolling clips to the radius
Select.List carries max-h-[var(--available-height)] and scrolls. The glass
content wrapper therefore gets overflow-hidden rounded-[inherit], otherwise a
row scrolled to the top square off the popup's corners while the surface behind
it stays round.