All-in-one, type-safe select components built on top of shadcn/ui.
Shadcn provides great primitives, but it doesn’t offer a single select component that handles generic object types, search, async loading, and infinite scroll out of the box. This library aims to fill that gap.
- Works with any object type (fully generic)
- Searchable & filterable
- Async loading & infinite scroll
- Built using shadcn/ui primitives
- Highly customizable and extensible
This component is designed to work with shadcn/ui.
You must have the following components installed:
- button
- input
- command
- popover
- badge
- separator
This library does not bundle shadcn components by design.
Shadcn UI intentionally avoids complex, opinionated components. However, real-world apps often need:
- Generic object-based selects
- Infinite scrolling
- Custom rendering
- Single & multi select in one pattern
This library embraces Shadcn’s philosophy while providing a reusable, production-ready select abstraction.
For Next.js Add to your tsconfig.json or jsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}For Vite Add to your vite.config.ts:
import { defineConfig } from "vite";
import path from "path";
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});Assuming you've already installed the requirements above
npm install react-generic-selectif not already installed.
npx shadcn-ui@latest add button input command popover badge separatorExample with server-side search and infinite scroll and shadcn form and tanstack query.
const {
courseData,
fetchNextCourse,
hasNextCoursePage,
isFetchingCourse,
isFetchingNextCourse,
} = useInfiniteCourseQuery(courseTerm)
// Let's we get list of courses as an example.
{/*
[{
"id": "8c6af505-6850-4ae4-baaf-e050ba793546",
"code": "CS 101",
"name": "Introduction to Computer Science",
"createdAt": "2025-11-27T09:30:46.473392",
"createdBy": "System",
"updatedAt": "2025-11-27T09:30:46.473392",
"updatedBy": "System"
}]
*/}
const handleCourseSearchChange = useCallback(
(newSearchTerm: string) => {
setCourseTerm(newSearchTerm)
},
[courseTerm]
)
<FormField
control={form.control}
name="courseId"
render={({ field }) => (
<FormItem>
<FormLabel>Course</FormLabel>
<FormControl>
<GenericSingleSelect
options={courseData}
labelKey="name"
valueKey="id"
value={field.value}
onValueChange={field.onChange}
onLoadMore={fetchNextCourse}
hasNextPage={hasNextCoursePage}
isFetchingNextPage={isFetchingNextCourse}
onSearchChange={handleCourseSearchChange}
isLoading={isFetchingCourse && isFetchingNextCourse}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>;Notes:
onSearchChangeenables server-side search.onLoadMore,hasNextPage, andisFetchingNextPagesupport infinite scroll.labelKeyandvalueKeydefine which properties are displayed and stored.