The Select shadcn/ui is missing.
A single-select, multi-select, and tags combobox for shadcn/ui — built-in search, virtualized lists, full keyboard & screen-reader support, and Ant Design Select parity in one copy-paste component.
Try it
npx shadcn@latest add https://gjs-select.gokhanyildiz.dev/r/gjs-select.jsonGet started
Installation
Add the component with the shadcn CLI — it drops gjs-select.tsx straight into your components/ui. No package to install, no styles to import. You own the code and edit it like any other file.
1. Run the shadcn CLI
npx shadcn@latest add https://gjs-select.gokhanyildiz.dev/r/gjs-select.json2. Use it
import { Select } from "@/components/ui/gjs-select"
const options = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Cherry", value: "cherry" },
]
export function Example() {
return (
<Select
options={options}
showSearch
allowClear
placeholder="Pick a fruit"
/>
)
}Showcase
Single, multi-select & tags — every mode, out of the box
Single-select, multi-select, and tags modes; search, clear, optgroups, sizes, and built-in states — all driven by simple props.
Modes
Single by default, or mode="multiple | tags"
Search, clear & groups
showSearch · allowClear · optgroups
Sizes
size="small | middle | large"
States
disabled · loading · status=error
Patterns
Real-world patterns
Async search, 10k-row virtualization, custom rendering, variants, validation states, RTL, and a Zod-validated form — the cases you actually ship.
Debounced async search
showSearch · filterOption={false} · onSearch → fetch · loading — server-side filtering over a live API route.
Responsive tag overflow
maxTagCount="responsive" collapses tags that no longer fit into a +N pill as the control resizes.
← Drag the dashed box wider or narrower →
Virtualized 10k options
virtual mounts only the rows in view (@tanstack/react-virtual), so a 10,000-option list opens and scrolls without jank.
10,000 options, only the visible rows rendered.
Custom option & tag rendering
optionRender and tagRender take full React nodes — avatars, secondary text, and bespoke pills.
Variants
variant="outlined | filled | borderless" — switch the chrome to match the surface it sits on.
Label in value + maxCount
labelInValue emits { label, value } objects; maxCount caps how many tags can be selected.
[]
Prefix & custom suffix
prefix renders a node before the value; suffixIcon swaps the trigger affordance.
Validation states
status="error | warning" tints the trigger to match form-validation feedback.
status="error"
status="warning"
Custom dropdown footer
dropdownRender wraps the menu — append a sticky footer, an action, or any custom content.
Right-to-left
direction="rtl" mirrors the full layout for Arabic, Hebrew, and Persian.
react-hook-form + Zod
SelectFormField binds Select to react-hook-form with a Zod resolver — validation status, error messages, and ARIA wiring come for free.
In overlays
Inside dialogs, sheets & drawers
The select opens a portal of its own. getPopupContainer scopes that dropdown to the overlay — a portable best-practice that stays predictable across modal libraries, z-index layers, and scrolling containers.
getPopupContainer, side by side
Two selects in one modal. Both stay interactive — gjs-select and the dialog share Radix's layer system. The left scopes its dropdown to the dialog (the portable choice); the right takes the default and portals to <body>.
Dialog — form
A modal form with single + multiple selects. getPopupContainer keeps both dropdowns inside the dialog.
Custom dialog (no Radix)
Your own modal — or antd Modal, react-modal, etc. getPopupContainer scopes the dropdown to it; the integration is identical everywhere.
Sheet — filter panel
A right-side sheet. Narrow width with maxTagCount="responsive" for tidy multi-selects.
Drawer — bottom sheet
A vaul drawer, common on mobile. getPopupContainer targets the content by id — vaul doesn't forward a ref.
Full-height dialog with scroll
A tall dialog whose body scrolls. Open the select at the bottom — the dropdown stays anchored and flips up when needed.
Virtualized list in a dialog
10,000 windowed options inside a dialog — page scroll is locked while the dropdown scrolls on its own.
Nested in a popover
A select opened inside a popover — a portal within a portal, stacked correctly.
Playground
Try it live
Tweak the props and copy the generated code straight into your project.
Mode
Size
Options
<Select
options={options}
mode="multiple"
showSearch
allowClear
/>Reference
Props API
A curated set of the most-used props. The component also forwards refs and standard combobox ARIA attributes.
| Prop | Type | Default | Description |
|---|---|---|---|
options | SelectItem<V>[] | [] | Options or optgroups to render. { label, value, disabled? }. |
value / defaultValue | V | V[] | null | — | Controlled / uncontrolled selected value(s). |
onChange | (value, option) => void | — | Fires when the selection changes. |
mode | "multiple" | "tags" | — | Multi-select, or tags mode that creates new entries. |
showSearch | boolean | false* | Filterable search input inside the control (* on by default in multi). |
allowClear | boolean | false | Show a clear button to reset the value on hover. |
size | "small" | "middle" | "large" | "middle" | Control height and typography. |
status | "error" | "warning" | — | Validation status styling for the control. |
variant | "outlined" | "filled" | "borderless" | "outlined" | Visual style of the control. |
loading | boolean | false | Show a spinner and loading state. |
maxCount | number | — | Cap selections; remaining options disable at the limit. |
tokenSeparators | string[] | — | Split typed/pasted input into multiple tags. |
virtual | boolean | false | Windowed rendering for very long option lists. |
filterSort | (a, b, info) => number | — | Custom ordering for the filtered options. |
labelInValue | boolean | false | Emit { label, value } objects in onChange. |
fieldNames | { label, value, options } | — | Map custom keys on your option data. |