v1.0 · Production-grade Select for shadcn/ui

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

terminal
npx shadcn@latest add https://gjs-select.gokhanyildiz.dev/r/gjs-select.json

Get 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

terminal
npx shadcn@latest add https://gjs-select.gokhanyildiz.dev/r/gjs-select.json

2. Use it

example.tsx
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.

Submitted payload

— submit to validate —

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

preview.tsx
<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.

PropTypeDefaultDescription
optionsSelectItem<V>[][]Options or optgroups to render. { label, value, disabled? }.
value / defaultValueV | V[] | nullControlled / uncontrolled selected value(s).
onChange(value, option) => voidFires when the selection changes.
mode"multiple" | "tags"Multi-select, or tags mode that creates new entries.
showSearchbooleanfalse*Filterable search input inside the control (* on by default in multi).
allowClearbooleanfalseShow 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.
loadingbooleanfalseShow a spinner and loading state.
maxCountnumberCap selections; remaining options disable at the limit.
tokenSeparatorsstring[]Split typed/pasted input into multiple tags.
virtualbooleanfalseWindowed rendering for very long option lists.
filterSort(a, b, info) => numberCustom ordering for the filtered options.
labelInValuebooleanfalseEmit { label, value } objects in onChange.
fieldNames{ label, value, options }Map custom keys on your option data.