Starter Docs
UI

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

components.json
{
  "registries": {
    "@rabtx": "http://localhost:3002/r/{name}.json"
  }
}

Add the component

npx shadcn@latest add @rabtx/select

Or, without the namespace, by URL:

npx shadcn@latest add http://localhost:3002/r/select.json

The 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.

PartTokens
Triggerfill --card, text --card-foreground, edge --input, icons --muted-foreground
Placeholder and group labels--subtle-foreground
Listfill --popover, text --popover-foreground, shadow --shadow-popover
Rowstext --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

EffectCSS
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 awaya 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 pinchselect:open keyframes square the facing corners while the list is attached, then round them back
Rowsevery 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
Closea 200ms fold keyframe, while transition: display, overlay allow-discrete keeps the list rendered until it ends
Sidedata-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 Field or an aria-label.
  • Reduced motion keeps a 120ms fade; data-motion="off" opens and closes at once.

On this page