import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const priorities = {
urgent: "Urgent",
high: "High",
medium: "Medium",
low: "Low",
};
export default function SelectBasic() {
return (
<Select items={priorities}>
<SelectTrigger aria-label="Priority">
<SelectValue placeholder="Set priority" />
</SelectTrigger>
<SelectContent>
{Object.entries(priorities).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}- Add the registryOnce per project
pnpm dlx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json - Add select
pnpm dlx shadcn@latest add @jarvis/select
Usage
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const fruits = {
apple: "Apple",
banana: "Banana",
cherry: "Cherry",
};
export function FruitSelect() {
return (
<Select items={fruits} defaultValue="apple">
<SelectTrigger aria-label="Fruit">
<SelectValue placeholder="Pick a fruit" />
</SelectTrigger>
<SelectContent>
{Object.entries(fruits).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}Pass items to Select, as an object of labels keyed by value or an array of { value, label },
so SelectValue can show the selected label.
For a long list people will want to search, use a Combobox. For the platform's own picker, use a Native Select.
Composition
Examples
With a label
A FieldLabel in the same Field names the trigger. Without one, give the trigger an
aria-label.
Used for emails and the interface.
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const languages = { en: "English", es: "Español", fr: "Français", de: "Deutsch" };
export default function SelectWithLabel() {
return (
<Field className="w-full max-w-sm">
<FieldLabel htmlFor="language">Language</FieldLabel>
<Select items={languages} defaultValue="en">
<SelectTrigger id="language" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(languages).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldDescription>Used for emails and the interface.</FieldDescription>
</Field>
);
}Groups
Use SelectGroup, SelectLabel and SelectSeparator.
import * as React from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const timezones = [
{
region: "Americas",
zones: [
{ value: "america-los-angeles", label: "Pacific Time" },
{ value: "america-denver", label: "Mountain Time" },
{ value: "america-chicago", label: "Central Time" },
{ value: "america-new-york", label: "Eastern Time" },
],
},
{
region: "Europe",
zones: [
{ value: "europe-london", label: "London" },
{ value: "europe-berlin", label: "Berlin" },
{ value: "europe-helsinki", label: "Helsinki" },
],
},
{
region: "Asia Pacific",
zones: [
{ value: "asia-singapore", label: "Singapore" },
{ value: "asia-tokyo", label: "Tokyo" },
{ value: "australia-sydney", label: "Sydney" },
],
},
];
const items = timezones.flatMap((group) => group.zones);
export default function SelectGroups() {
return (
<Select items={items}>
<SelectTrigger aria-label="Time zone">
<SelectValue placeholder="Select a time zone" />
</SelectTrigger>
<SelectContent>
{timezones.map((group, index) => (
<React.Fragment key={group.region}>
{index > 0 && <SelectSeparator />}
<SelectGroup>
<SelectLabel>{group.region}</SelectLabel>
{group.zones.map((zone) => (
<SelectItem key={zone.value} value={zone.value}>
{zone.label}
</SelectItem>
))}
</SelectGroup>
</React.Fragment>
))}
</SelectContent>
</Select>
);
}Custom item content
Items can hold more than text. Pass SelectValue a function to render the selected item.
import * as React from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
/* Card marks from Simple Icons (https://simpleicons.org), CC0. */
function VisaMark(props: React.ComponentProps<"svg">) {
return (
<svg viewBox="0 8 24 8" aria-hidden="true" {...props}>
<path
fill="#1a1f71"
d="M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-.094-.368-.175-.503-.461-.658C1.447 8.864.677 8.627 0 8.479l.046-.217h3.3a.904.904 0 01.894.764l.817 4.338 2.018-5.102zm8.033 5.049c.008-1.979-2.736-2.088-2.717-2.972.006-.269.262-.555.822-.628a3.66 3.66 0 011.913.336l.34-1.59a5.207 5.207 0 00-1.814-.333c-1.917 0-3.266 1.02-3.278 2.479-.012 1.079.963 1.68 1.698 2.04.756.367 1.01.603 1.006.931-.005.504-.602.725-1.16.734-.975.015-1.54-.263-1.992-.473l-.351 1.642c.453.208 1.289.39 2.156.398 2.037 0 3.37-1.006 3.377-2.564m5.061 2.447H24l-1.565-7.496h-1.656a.883.883 0 00-.826.55l-2.909 6.946h2.036l.405-1.12h2.488zm-2.163-2.656l1.02-2.815.588 2.815zm-8.16-4.84l-1.603 7.496H8.34l1.605-7.496z"
/>
</svg>
);
}
function AmexMark(props: React.ComponentProps<"svg">) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<rect width="24" height="24" fill="#fff" />
<path
fill="#2e77bc"
d="M16.015 14.378c0-.32-.135-.496-.344-.622-.21-.12-.464-.135-.81-.135h-1.543v2.82h.675v-1.027h.72c.24 0 .39.024.478.125.12.13.104.38.104.55v.35h.66v-.555c-.002-.25-.017-.376-.108-.516-.06-.08-.18-.18-.33-.234l.02-.008c.18-.072.48-.297.48-.747zm-.87.407l-.028-.002c-.09.053-.195.058-.33.058h-.81v-.63h.824c.12 0 .24 0 .33.05.098.048.156.147.15.255 0 .12-.045.215-.134.27zM20.297 15.837H19v.6h1.304c.676 0 1.05-.278 1.05-.884 0-.28-.066-.448-.187-.582-.153-.133-.392-.193-.73-.207l-.376-.015c-.104 0-.18 0-.255-.03-.09-.03-.15-.105-.15-.21 0-.09.017-.166.09-.21.083-.046.177-.066.272-.06h1.23v-.602h-1.35c-.704 0-.958.437-.958.84 0 .9.776.855 1.407.87.104 0 .18.015.225.06.046.03.082.106.082.18 0 .077-.035.15-.08.18-.06.053-.15.07-.277.07zM0 0v10.096L.81 8.22h1.75l.225.464V8.22h2.043l.45 1.02.437-1.013h6.502c.295 0 .56.057.756.236v-.23h1.787v.23c.307-.17.686-.23 1.12-.23h2.606l.24.466v-.466h1.918l.254.465v-.466h1.858v3.948H20.87l-.36-.6v.585h-2.353l-.256-.63h-.583l-.27.614h-1.213c-.48 0-.84-.104-1.08-.24v.24h-2.89v-.884c0-.12-.03-.12-.105-.135h-.105v1.036H6.067v-.48l-.21.48H4.69l-.202-.48v.465H2.235l-.256-.624H1.4l-.256.624H0V24h23.786v-7.108c-.27.135-.613.18-.973.18H21.09v-.255c-.21.165-.57.255-.914.255H14.71v-.9c0-.12-.018-.12-.12-.12h-.075v1.022h-1.8v-1.066c-.298.136-.643.15-.928.136h-.214v.915h-2.18l-.54-.617-.57.6H4.742v-3.93h3.61l.518.602.554-.6h2.412c.28 0 .74.03.942.225v-.24h2.177c.202 0 .644.045.903.225v-.24h3.265v.24c.163-.164.508-.24.803-.24h1.89v.24c.194-.15.464-.24.84-.24h1.176V0H0zM21.156 14.955c.004.005.006.012.01.016.01.01.024.01.032.02l-.042-.035zM23.828 13.082h.065v.555h-.065zM23.865 15.03v-.005c-.03-.025-.046-.048-.075-.07-.15-.153-.39-.215-.764-.225l-.36-.012c-.12 0-.194-.007-.27-.03-.09-.03-.15-.105-.15-.21 0-.09.03-.16.09-.204.076-.045.15-.05.27-.05h1.223v-.588h-1.283c-.69 0-.96.437-.96.84 0 .9.78.855 1.41.87.104 0 .18.015.224.06.046.03.076.106.076.18 0 .07-.034.138-.09.18-.045.056-.136.07-.27.07h-1.288v.605h1.287c.42 0 .734-.118.9-.36h.03c.09-.134.135-.3.135-.523 0-.24-.045-.39-.135-.526zM18.597 14.208v-.583h-2.235V16.458h2.235v-.585h-1.57v-.57h1.533v-.584h-1.532v-.51M13.51 8.787h.685V11.6h-.684zM13.126 9.543l-.007.006c0-.314-.13-.5-.34-.624-.217-.125-.47-.135-.81-.135H10.43v2.82h.674v-1.034h.72c.24 0 .39.03.487.12.122.136.107.378.107.548v.354h.677v-.553c0-.25-.016-.375-.11-.516-.09-.107-.202-.19-.33-.237.172-.07.472-.3.472-.75zm-.855.396h-.015c-.09.054-.195.056-.33.056H11.1v-.623h.825c.12 0 .24.004.33.05.09.04.15.128.15.25s-.047.22-.134.266zM15.92 9.373h.632v-.6h-.644c-.464 0-.804.105-1.02.33-.286.3-.362.69-.362 1.11 0 .512.123.833.36 1.074.232.238.645.31.97.31h.78l.255-.627h1.39l.262.627h1.36v-2.11l1.272 2.11h.95l.002.002V8.786h-.684v1.963l-1.18-1.96h-1.02V11.4L18.11 8.744h-1.004l-.943 2.22h-.3c-.177 0-.362-.03-.468-.134-.125-.15-.186-.36-.186-.662 0-.285.08-.51.194-.63.133-.135.272-.165.516-.165zm1.668-.108l.464 1.118v.002h-.93l.466-1.12zM2.38 10.97l.254.628H4V9.393l.972 2.205h.584l.973-2.202.015 2.202h.69v-2.81H6.118l-.807 1.904-.876-1.905H3.343v2.663L2.205 8.787h-.997L.01 11.597h.72l.26-.626h1.39zm-.688-1.705l.46 1.118-.003.002h-.915l.457-1.12zM11.856 13.62H9.714l-.85.923-.825-.922H5.346v2.82H8l.855-.932.824.93h1.302v-.94h.838c.6 0 1.17-.164 1.17-.945l-.006-.003c0-.78-.598-.93-1.128-.93zM7.67 15.853l-.014-.002H6.02v-.557h1.47v-.574H6.02v-.51H7.7l.733.82-.764.824zm2.642.33l-1.03-1.147 1.03-1.108v2.253zm1.553-1.258h-.885v-.717h.885c.24 0 .42.098.42.344 0 .243-.15.372-.42.372zM9.967 9.373v-.586H7.73V11.6h2.237v-.58H8.4v-.564h1.527V9.88H8.4v-.507"
/>
</svg>
);
}
const paymentMethods = [
{ value: "visa", label: "Visa •• 7391", brand: "Visa", last4: "7391", expires: "11/27" },
{ value: "amex", label: "Amex •• 1008", brand: "Amex", last4: "1008", expires: "04/28" },
];
function CardBrand({ value }: { value: string }) {
const method = paymentMethods.find((item) => item.value === value);
if (!method) return null;
return (
<span
role="img"
aria-label={method.brand}
className={cn(
"flex h-3.5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-[2px] shadow-[inset_0_0_0_0.5px_rgb(0_0_0/0.12)]",
method.value === "amex" ? "bg-[#2e77bc]" : "bg-white px-0.5",
)}
>
{method.value === "amex" ? (
<AmexMark className="size-full" />
) : (
<VisaMark className="size-full" />
)}
</span>
);
}
export default function SelectPaymentMethod() {
return (
<Select items={paymentMethods} defaultValue="visa">
<SelectTrigger size="lg" className="w-32" aria-label="Payment method">
<SelectValue>
{(value: string) => (
<>
<CardBrand value={value} />
<span className="tabular-nums">
•• {paymentMethods.find((item) => item.value === value)?.last4}
</span>
</>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{paymentMethods.map((method) => (
<SelectItem key={method.value} value={method.value}>
<CardBrand value={method.value} />
<span className="tabular-nums">•• {method.last4}</span>
<span className="ml-auto pl-3 text-faint">Expires {method.expires}</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
}Sizes
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const sortOptions = {
updated: "Last updated",
created: "Date created",
name: "Name",
};
export default function SelectSizes() {
return (
<div className="flex flex-wrap items-center gap-3">
{(["sm", "default", "lg"] as const).map((size) => (
<Select key={size} items={sortOptions} defaultValue="updated">
<SelectTrigger size={size} aria-label="Sort by">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(sortOptions).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
))}
</div>
);
}Disabled
Set disabled on Select, or on a single SelectItem.
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const sortOptions = {
updated: "Last updated",
created: "Date created",
name: "Name",
};
export default function SelectDisabled() {
return (
<div className="flex flex-wrap items-center gap-3">
<Select items={sortOptions} defaultValue="name" disabled>
<SelectTrigger aria-label="Sort by">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(sortOptions).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select items={sortOptions} defaultValue="updated">
<SelectTrigger aria-label="Sort by">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(sortOptions).map(([value, label]) => (
<SelectItem key={value} value={value} disabled={value === "created"}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}Invalid
Set invalid on the Field and explain the problem with a FieldError.
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const priorities = {
urgent: "Urgent",
high: "High",
medium: "Medium",
low: "Low",
};
export default function SelectInvalid() {
return (
<Field invalid className="w-fit">
<FieldLabel>Priority</FieldLabel>
<Select items={priorities}>
<SelectTrigger>
<SelectValue placeholder="Set priority" />
</SelectTrigger>
<SelectContent>
{Object.entries(priorities).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldError>Choose a priority.</FieldError>
</Field>
);
}Props
SelectContent takes side, sideOffset, align, alignOffset and alignItemWithTrigger.
The tables leave out className, style and render. Parts built on Base UI take them as its styling and composition guides describe.
Select
| Prop | Type | Default |
|---|---|---|
children | React.ReactNode | — |
inputRefA ref to access the hidden input element. | React.Ref<HTMLInputElement> | — |
nameIdentifies the field when a form is submitted. | string | — |
formIdentifies the form that owns the hidden input. Useful when the select is rendered outside the form. | string | — |
autoCompleteProvides a hint to the browser for autofill. | string | — |
idThe id of the Select. | string | — |
requiredWhether the user must choose a value before submitting a form. | boolean | false |
readOnlyWhether the user should be unable to choose a different option from the select popup. | boolean | false |
disabledWhether the component should ignore user interaction. | boolean | false |
multipleWhether multiple items can be selected. | boolean | false |
highlightItemOnHoverWhether moving the pointer over items should highlight them.
Disabling this prop allows CSS | boolean | true |
defaultOpenWhether the select popup is initially open. To render a controlled select popup, use the | boolean | false |
onOpenChangeEvent handler called when the select popup is opened or closed. | ((open: boolean, eventDetails: SelectRootOpenChangeEventDetails) => void) | — |
onOpenChangeCompleteEvent handler called after any animations complete when the select popup is opened or closed. | ((open: boolean) => void) | — |
openWhether the select popup is currently open. | boolean | — |
modalDetermines if the select enters a modal state when open.
On touch devices, a | boolean | true |
actionsRefA ref to imperative actions.
| React.RefObject<SelectRootActions | null> | — |
itemsData structure of the items rendered in the select popup.
When specified, | readonly Group<any>[] | readonly { label: React.ReactNode; value: any; }[] | Record<string, React.ReactNode> | — |
itemToStringLabelWhen the item values are objects ( | ((itemValue: unknown) => string) | — |
itemToStringValueWhen the item values are objects ( | ((itemValue: unknown) => string) | — |
isItemEqualToValueCustom comparison logic used to determine if a select item value matches the current selected value. Useful when item values are objects without matching referentially.
Defaults to | ((itemValue: unknown, value: unknown) => boolean) | — |
defaultValueThe uncontrolled value of the select when it's initially rendered. To render a controlled select, use the | unknown | — |
valueThe value of the select. Use when controlled. | unknown | — |
onValueChangeEvent handler called when the value of the select changes. | ((value: unknown, eventDetails: SelectRootChangeEventDetails) => void) | — |
SelectValue
| Prop | Type | Default |
|---|---|---|
childrenAccepts a function that returns a | ((value: any) => React.ReactNode) | React.ReactNode | — |
placeholderThe placeholder value to display when no value is selected.
This is overridden by | React.ReactNode | — |
SelectTrigger
| Prop | Type | Default |
|---|---|---|
children | React.ReactNode | — |
disabledWhether the component should ignore user interaction. | boolean | — |
nativeButtonWhether the component renders a native | boolean | true |
size | "default" | "lg" | "sm" | "default" |
SelectContent
| Prop | Type | Default |
|---|---|---|
children | React.ReactNode | — |
finalFocusDetermines the element to focus when the select popup is closed.
| boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | null | void) | — |
sideWhich side of the anchor element to align the popup against. May automatically change to avoid collisions. | "bottom" | "inline-end" | "inline-start" | "left" | "right" | "top" | "bottom" |
sideOffsetDistance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a
| number | OffsetFunction | 4 |
alignHow to align the popup relative to the specified side. | "center" | "end" | "start" | "center" |
alignOffsetAdditional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a
| number | OffsetFunction | 0 |
alignItemWithTriggerWhether the positioner overlaps the trigger so the selected item's text is aligned with the trigger's value text. This only applies to mouse input and is automatically disabled if there is not enough space. | boolean | true |
SelectItem
| Prop | Type | Default |
|---|---|---|
children | React.ReactNode | — |
valueA unique value that identifies this select item. | any | null |
disabledWhether the component should ignore user interaction. | boolean | false |
labelSpecifies the text label to use when the item is matched during keyboard text navigation. Defaults to the item text content if not provided. | string | — |
nativeButtonWhether the component renders a native | boolean | false |
SelectSeparator
| Prop | Type | Default |
|---|---|---|
orientationThe orientation of the separator. | "horizontal" | "vertical" | 'horizontal' |
SelectScrollUpButton
| Prop | Type | Default |
|---|---|---|
keepMountedWhether to keep the HTML element in the DOM while the select popup is not scrollable. | boolean | false |
SelectScrollDownButton
| Prop | Type | Default |
|---|---|---|
keepMountedWhether to keep the HTML element in the DOM while the select popup is not scrollable. | boolean | false |