Components
TypedCombobox A headless, type-safe combobox component with search functionality, full TypeScript inference, and customizable rendering.
TypedCombobox is a headless, type-safe combobox component built on top of Popover and Command components. It provides full TypeScript inference for option values with built-in search/filter functionality.
Key features:
Full type-safety for string, number, and boolean values
Built-in search/filter with customizable filter function
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-combobox.json pnpm dlx shadcn@latest add https://typedora-ui.netlify.app/r/typed-combobox.json npx shadcn@latest add https://typedora-ui.netlify.app/r/typed-combobox.json bunx --bun shadcn@latest add https://typedora-ui.netlify.app/r/typed-combobox.json
Code Preview
import { TypedCombobox } from "@/components/typed-combobox" ;
export function BasicUsage () {
return (
< TypedCombobox
placeholder = "Select a framework..."
searchPlaceholder = "Search frameworks..."
options = {
[
{ label : "Next.js" , value : "nextjs" },
{ label : "Remix" , value : "remix" },
{ label : "Astro" , value : "astro" },
{ label : "SvelteKit" , value : "sveltekit" },
{ label : "Nuxt" , value : "nuxt" },
] as const
}
onValueChange = {( value ) => {
console . log ( value );
}}
/>
);
}
Code Preview
import { TypedCombobox } from "@/components/typed-combobox" ;
export function NumberValues () {
return (
< TypedCombobox
placeholder = "Select priority..."
searchPlaceholder = "Search priorities..."
options = {
[
{ label : "Low" , value : 1 },
{ label : "Medium" , value : 2 },
{ label : "High" , value : 3 },
{ label : "Critical" , value : 4 },
] as const
}
onValueChange = {( value ) => {
console . log ( value );
}}
/>
);
}
Code Preview
import { TypedCombobox } from "@/components/typed-combobox" ;
export function BooleanValues () {
return (
< TypedCombobox
placeholder = "Select status..."
searchPlaceholder = "Search..."
options = {
[
{ label : "Active" , value : true },
{ label : "Inactive" , value : false },
] as const
}
onValueChange = {( value ) => {
console . log ( value );
}}
/>
);
}
Code Preview
import { CheckIcon } from "lucide-react" ;
import { TypedCombobox } from "@/components/typed-combobox" ;
import { cn } from "@/lib/utils" ;
const users = [
{ id : 1 , name : "John Doe" , email : "john@example.com" , avatar : "JD" },
{ id : 2 , name : "Jane Smith" , email : "jane@example.com" , avatar : "JS" },
{ id : 3 , name : "Bob Johnson" , email : "bob@example.com" , avatar : "BJ" },
] as const ;
export function CustomItem () {
return (
< TypedCombobox
placeholder = "Select a user..."
searchPlaceholder = "Search users..."
options = { users }
valueKey = "id"
labelKey = "name"
popoverWidth = { 300 }
renderItem = {({ option , isSelected , itemProps , Item }) => (
< Item { ... itemProps } className = "flex items-center gap-3 px-2 py-2" >
< div className = "flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-sm font-medium" >
{ option . avatar }
</ div >
< div className = "flex-1 min-w-0" >
< div className = "font-medium truncate" >{ option . name }</ div >
< div className = "text-xs text-muted-foreground truncate" >
{ option . email }
</ div >
</ div >
< CheckIcon
className = { cn (
"h-4 w-4 shrink-0" ,
isSelected ? "opacity-100" : "opacity-0" ,
)}
/>
</ Item >
)}
onValueChange = {( value ) => {
console . log ( value );
}}
/>
);
}
Code Preview
import { TypedCombobox } from "@/components/typed-combobox" ;
const groupedOptions = [
{
label : "Frontend" ,
options : [
{ label : "React" , value : "react" },
{ label : "Vue" , value : "vue" },
{ label : "Svelte" , value : "svelte" },
],
},
{
label : "Backend" ,
options : [
{ label : "Node.js" , value : "nodejs" },
{ label : "Python" , value : "python" },
{ label : "Go" , value : "go" },
],
},
] as const ;
export function GroupedOptions () {
return (
< TypedCombobox
placeholder = "Select a technology..."
searchPlaceholder = "Search technologies..."
options = { groupedOptions }
popoverWidth = { 250 }
onValueChange = {( value ) => {
console . log ( value );
}}
/>
);
}
Code Preview
import { TypedCombobox } from "@/components/typed-combobox" ;
export function DisabledOptions () {
return (
< TypedCombobox
placeholder = "Select a plan..."
searchPlaceholder = "Search plans..."
options = {
[
{ label : "Free" , value : "free" },
{ label : "Pro" , value : "pro" },
{ label : "Enterprise" , value : "enterprise" , disabled : true },
] as const
}
/>
);
}
Code Preview
"use client" ;
import { useState } from "react" ;
import { TypedCombobox } from "@/components/typed-combobox" ;
const frameworks = [
{ label : "Next.js" , value : "nextjs" },
{ label : "Remix" , value : "remix" },
{ label : "Astro" , value : "astro" },
{ label : "SvelteKit" , value : "sveltekit" },
] as const ;
type FrameworkValue = ( typeof frameworks )[ number ][ "value" ];
export function Controlled () {
const [ value , setValue ] = useState < FrameworkValue >( "nextjs" );
return (
< div className = "space-y-4" >
< TypedCombobox
placeholder = "Select a framework..."
searchPlaceholder = "Search..."
options = { frameworks }
value = { value }
onValueChange = { setValue }
/>
< p className = "text-sm text-muted-foreground" >
Selected: < code className = "font-mono" >{ value }</ code >
</ p >
</ div >
);
}
You can customize the search behavior using the filterFn prop to search across multiple fields:
Code Preview
import { TypedCombobox } from "@/components/typed-combobox" ;
const users = [
{ id : 1 , name : "John Doe" , email : "john@example.com" , role : "admin" },
{ id : 2 , name : "Jane Smith" , email : "jane@example.com" , role : "user" },
{ id : 3 , name : "Bob Johnson" , email : "bob@example.com" , role : "admin" },
{ id : 4 , name : "Alice Brown" , email : "alice@example.com" , role : "user" },
] as const ;
export function CustomFilter () {
return (
< TypedCombobox
placeholder = "Search by name, email, or role..."
searchPlaceholder = "Type to search..."
options = { users }
valueKey = "id"
labelKey = "name"
popoverWidth = { 300 }
filterFn = {( option , query ) => {
const q = query . toLowerCase ();
return (
option . name . toLowerCase (). includes ( q ) ||
option . email . toLowerCase (). includes ( q ) ||
option . role . toLowerCase (). includes ( q )
);
}}
onValueChange = {( value ) => {
console . log ( value );
}}
/>
);
} Search by name, email, or role...
By default, TypedCombobox uses value and label keys. You can customize this using valueKey and labelKey props:
Code Preview
import { TypedCombobox } from "@/components/typed-combobox" ;
const users = [
{ 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" },
] as const ;
export function CustomKeys () {
return (
< TypedCombobox
placeholder = "Select a user..."
searchPlaceholder = "Search users..."
options = { users }
valueKey = "id"
labelKey = "name"
popoverWidth = { 250 }
onValueChange = {( value ) => {
console . log ( value );
}}
/>
);
}
Prop Type Default Description optionsarray- Array of options or grouped options valueKey?TValueKey"value"The key to use for the option value labelKey?TLabelKey"label"The key to use for the option label value?ExtractOptionValue<TOptions, TValueKey>- Controlled value defaultValue?ExtractOptionValue<TOptions, TValueKey>- Default value for uncontrolled usage onValueChange?function- Callback when value changes placeholder?string"Select an option..."Placeholder for trigger button searchPlaceholder?string"Search..."Placeholder for search input emptyText?string"No results found."Text when no options match filterFn?function- Custom filter function for search popoverWidth?string | number200Width of the popover content triggerClassName?string- Class name for the trigger button contentClassName?string- Class name for the popover content renderTrigger?function- Custom render for trigger renderItem?function- Custom render for each item renderGroup?function- Custom render for each group renderEmpty?function- Custom render for empty state renderContent?function- Custom render for content wrapper disabled?booleanfalseWhether the combobox is disabled open?boolean- Controlled open state onOpenChange?function- Callback when open state changes
Prop Type Description valueTThe option value labelstringThe display label disabled?booleanWhether the option is disabled
Prop Type Description labelstringThe group label optionsarrayArray of options in this group
Prop Type Description selectedOptionTOption | undefinedCurrently selected option placeholderstringPlaceholder text openbooleanWhether the combobox is open disabledbooleanWhether the combobox is disabled ButtoncomponentDefault Button component
Prop Type Description optionTOptionThe option data isSelectedbooleanWhether this option is selected isDisabledbooleanWhether this option is disabled groupComboboxOptionGroup<TOption> | undefinedGroup this option belongs to itemPropsobjectProps to spread on CommandItem ItemcomponentDefault CommandItem component
Prop Type Description groupComboboxOptionGroup<TOption>The group data childrenReactNodeRendered items inside this group GroupcomponentDefault CommandGroup component
Prop Type Description searchQuerystringThe current search query EmptycomponentDefault CommandEmpty component
Prop Type Description childrenReactNodeRendered items/groups CommandcomponentCommand component CommandInputcomponentCommandInput component CommandListcomponentCommandList component searchQuerystringCurrent search query setSearchQueryfunctionUpdate search query searchPlaceholderstringSearch input placeholder emptyStateReactNodeEmpty state component