Components
TypedSelect A headless, type-safe select component with full TypeScript inference and customizable rendering.
TypedSelect is a headless, type-safe wrapper around the base Select component. It provides full TypeScript inference for option values while giving you complete control over rendering through render props.
Key features:
Full type-safety for string, number, and boolean values
Headless design with render props for complete customization
Support for grouped options
Controlled and uncontrolled usage
npm pnpm yarn bun
npx shadcn@latest add https://typedora-ui.netlify.app/r/typed-select.json pnpm dlx shadcn@latest add https://typedora-ui.netlify.app/r/typed-select.json npx shadcn@latest add https://typedora-ui.netlify.app/r/typed-select.json bunx --bun shadcn@latest add https://typedora-ui.netlify.app/r/typed-select.json
Code Preview
import { TypedSelect } from "@/components/typed-select" ;
export function BasicUsage () {
return (
< div >
< TypedSelect
options = {
[
{ label : "Apple" , value : "apple" },
{ label : "Banana" , value : "banana" },
{ label : "Orange" , value : "orange" },
] as const
}
onValueChange = {( value ) => {
}}
/>
</ div >
);
}
Code Preview
import { TypedSelect } from "@/components/typed-select" ;
export function NumberValues () {
return (
< div >
< TypedSelect
options = {
[
{ label : "One" , value : 1 },
{ label : "Two" , value : 2 },
{ label : "Three" , value : 3 },
] as const
}
onValueChange = {( value ) => {
}}
/>
</ div >
);
}
Code Preview
import { TypedSelect } from "@/components/typed-select" ;
export function BooleanValues () {
return (
< div >
< TypedSelect
options = {[
{ label : "Yes" , value : true },
{ label : "No" , value : false },
]}
onValueChange = {( value ) => {
}}
/>
</ div >
);
}
Code Preview
import { TypedSelect } from "@/components/typed-select" ;
export function CustomItem () {
return (
< div >
< TypedSelect
placeholder = "Select a plan"
options = {[
{ label : "Free" , value : "free" , description : "For personal use" },
{ label : "Pro" , value : "pro" , description : "For professionals" },
{ label : "Team" , value : "team" , description : "For small teams" },
]}
renderItem = {({ option , itemProps , Item }) => (
< Item { ... itemProps } className = "flex flex-col items-start py-2" >
< span className = "font-medium" >{ option . label }</ span >
< span className = "text-sm text-muted-foreground" >
{ option . description }
</ span >
</ Item >
)}
/>
</ div >
);
}
Code Preview
import { TypedSelect } from "@/components/typed-select" ;
export function GroupedOptions () {
return (
< div >
< TypedSelect
options = {
[
{
label : "Fruits" ,
options : [
{ label : "Apple" , value : "apple" },
{ label : "Banana" , value : "banana" },
],
},
{
label : "Vegetables" ,
options : [
{ label : "Carrot" , value : "carrot" },
{ label : "Broccoli" , value : "broccoli" },
],
},
] as const
}
onValueChange = {( value ) => {
}}
/>
</ div >
);
}
Code Preview
import { TypedSelect } from "@/components/typed-select" ;
export function DisabledOptions () {
return (
< div >
< TypedSelect
options = {[
{ label : "Apple" , value : "apple" },
{ label : "Banana" , value : "banana" , disabled : true },
{ label : "Orange" , value : "orange" },
]}
/>
</ div >
);
}
Code Preview
"use client" ;
import { useState } from "react" ;
import { TypedSelect } from "@/components/typed-select" ;
type User = {
id : string ;
name : string ;
};
const users : User [] = [
{ id : "1" , name : "Alice" },
{ id : "2" , name : "Bob" },
{ id : "3" , name : "Charlie" },
];
export function Controlled () {
const [ value , setValue ] = useState < User [ "id" ]>( users [ 0 ]. id );
return (
< div className = "space-y-4" >
< TypedSelect
value = { value }
options = { users }
valueKey = "id"
labelKey = "name"
onValueChange = { setValue }
/>
< p >Selected: { value }</ p >
</ div >
);
}
Code Preview
"use client" ;
import { useState } from "react" ;
import { TypedSelect } from "@/components/typed-select" ;
import { Input } from "@/components/ui/input" ;
export function SearchFilter () {
const [ search , setSearch ] = useState ( "" );
const allOptions = [
{ label : "Apple" , value : "apple" },
{ label : "Banana" , value : "banana" },
{ label : "Cherry" , value : "cherry" },
];
const filteredOptions = allOptions . filter (( opt ) =>
opt . label . toLowerCase (). includes ( search . toLowerCase ()),
);
return (
< TypedSelect
options = { filteredOptions }
renderContent = {({ children , Content }) => (
< Content
// Prevent Select from closing when clicking inside the search input
onPointerDownOutside = {( e ) => {
const target = e . target as HTMLElement ;
if ( target . closest ( "input" )) {
e . preventDefault ();
}
}}
>
< div className = "p-2 border-b" onKeyDown = {( e ) => e . stopPropagation ()}>
< Input
placeholder = "Search..."
value = { search }
onChange = {( e ) => setSearch ( e . target . value )}
// Prevent focus management interference from Radix
onPointerDown = {( e ) => e . stopPropagation ()}
/>
</ div >
{ children }
</ Content >
)}
/>
);
}
By default, TypedSelect uses value and label keys. You can customize this using valueKey and labelKey props:
Code Preview
"use client" ;
import { useState } from "react" ;
import { TypedSelect } from "@/components/typed-select" ;
// Example data using id/name keys instead of value/label
type User = {
id : number ;
name : string ;
email : string ;
};
const users : User [] = [
{ id : 1 , name : "John Doe" , email : "john@example.com" },
{ id : 2 , name : "Jane Smith" , email : "jane@example.com" },
{ id : 3 , name : "Bob Johnson" , email : "bob@example.com" },
];
export function CustomKeys () {
const [ selectedId , setSelectedId ] = useState < User [ "id" ]>();
return (
< div className = "space-y-4" >
< TypedSelect
placeholder = "Select a user"
options = { users }
valueKey = "id"
labelKey = "name"
value = { selectedId }
onValueChange = { setSelectedId }
renderItem = {({ option , itemProps , Item }) => (
< Item { ... itemProps } className = "flex flex-col items-start py-2" >
< span className = "font-medium" >{ option . name }</ span >
< span className = "text-sm text-muted-foreground" >
{ option . email }
</ span >
</ Item >
)}
/>
{ selectedId && (
< p className = "text-sm text-muted-foreground" >
Selected user ID: < strong >{ selectedId }</ strong >
</ p >
)}
</ div >
);
}
Prop Type optionsarrayvalueKey?TValueKeylabelKey?TLabelKeyvalue?ExtractOptionValue<TOption, TValueKey>defaultValue?ExtractOptionValue<TOption, TValueKey>onValueChange?functionplaceholder?stringrenderTrigger?functionrenderItem?functionrenderGroup?functionrenderContent?functiondisabled?booleanopen?booleanonOpenChange?functionname?stringrequired?boolean
Prop Type valueTlabelstringdisabled?boolean
Prop Type labelstringoptionsarray
Prop Type selectedOptionTOption | undefinedplaceholderstringopenbooleandisabledbooleanTriggerfunctionValuefunction
Prop Type optionTOptionisSelectedbooleanisDisabledbooleangroupSelectOptionGroup<TOption> | undefineditemPropsobjectItemfunction
Prop Type groupSelectOptionGroup<TOption>childrenReactNodeGroupfunctionLabelfunction
Prop Type childrenReactNodeContentfunction