Select
A native select whose list unfolds from the trigger, pulls away into its own panel and fades its rows in. Every animation is CSS.
What the project is built with.
Installation
Add the registry to components.json, once
{
"registries": {
"@rabtx": "http://localhost:3002/r/{name}.json"
}
}Add the component
npx shadcn@latest add @rabtx/selectOr, without the namespace, by URL:
npx shadcn@latest add http://localhost:3002/r/select.jsonThe files land in your components/ui and are styled by your own theme. Add --overwrite to replace a shadcn component with the same name.
Usage
Installed from the registry, import from @/components/ui/select; inside this monorepo, from
@rabtx/ui/select.
import { Select, SelectItem } from "@rabtx/ui/select";
export function FrameworkPicker() {
const [framework, setFramework] = useState("");
return (
<Select
aria-label="Framework"
placeholder="Pick a framework"
value={framework}
onValueChange={setFramework}
>
<SelectItem value="next">Next.js</SelectItem>
<SelectItem value="astro">Astro</SelectItem>
</Select>
);
}It is a real <select>, so the browser provides the keyboard (arrows, Home, End, typeahead,
Escape), the form value, screen reader semantics and the picker on phones. Where the browser has
the customizable select (appearance: base-select), the list is drawn and animated by the
package's CSS. Everywhere else the system list opens, and the trigger looks the same.
Icons
leadingIcon sits before the value. An item's icon shows in the styled list; a system list
shows the label alone.
Groups and separators
SelectGroup is an <optgroup> with a label; SelectSeparator is an <hr>.
Error and disabled
Inside a Field, error turns the edge red and links the message. Items and the whole select
can be disabled.
Choose a plan to continue.
Controlled
Value: todo
Plain options
SelectItem renders an <option>, so plain options work too, and name puts the value in a form.
Placement
Just before the list opens, the select checks whether it fits below. If it does not and there is more room above, it opens above and every motion mirrors: it unfolds upward and the top corners pinch. Scroll this example to the bottom of the window and open it.
API
Select takes everything a native <select> takes, except size and multiple, plus:
Prop
Type
SelectItem takes value, disabled and icon. SelectGroup takes a label.
Outside React, the class names are exported from @rabtx/ui/select/styles:
<div class="rx-select-field rx-select-md">
<select class="rx-select" aria-label="Team">
<option>Design</option>
</select>
<span class="rx-select-chevron" aria-hidden="true"></span>
</div>Theming
The Select reads only the theme tokens.
| Part | Tokens |
|---|---|
| Trigger | fill --card, text --card-foreground, edge --input, icons --muted-foreground |
| Placeholder and group labels | --subtle-foreground |
| List | fill --popover, text --popover-foreground, shadow --shadow-popover |
| Rows | text --muted-foreground, hover and checked --accent / --accent-foreground |
| Focus and error | --ring, --destructive |
| Corners | --radius-box (trigger and list), --radius-item (rows) |
| Font and motion | --font-sans, --spring-unfold |
How the motion works
| Effect | CSS |
|---|---|
| Unfold | ::picker(select) runs a clip-path: inset() keyframe from an 8px lip to the full list on the unfold spring (a linear() easing). No clip is left at rest, so the shadow is whole. |
| Pull away | a second keyframe, delayed to the last 60%, moves the list from flush on the trigger into its 8px gap and rounds its near corners |
| Trigger pinch | select:open keyframes square the facing corners while the list is attached, then round them back |
| Rows | every option fades down 4px in 120ms when the list opens, with no stagger, so long lists have no late rows |
| Chevron | :has(> select:open) turns it 180° on the same spring |
| Close | a 200ms fold keyframe, while transition: display, overlay allow-discrete keeps the list rendered until it ends |
| Side | data-side="top" mirrors position-area, the margin, the lip and the corners |
Browser support
- Customizable select (Chromium 135+): the styled, animated list, item icons, ticks and the side-aware unfold.
- Everywhere else: the system list. The trigger, sizes, placeholder color, focus, error, disabled, leading icon and keyboard are the same. Item icons are left out, and so is the motion.
- Item icons render only after hydration, so server HTML is identical in every browser.
Accessibility
- A native select: its role, its options and their states are exposed by the browser.
- Focus shows a blue edge and a soft ring. Label it with
Fieldor anaria-label. - Reduced motion keeps a 120ms fade;
data-motion="off"opens and closes at once.