A fully type-safe, headless-friendly Select and Multi-Select component for React Native, powered by [@gorhom/bottom-sheet](https://github.com/gorhom/react-native-bottom-sheet).
npm install react-native-modern-selectA fully type-safe, headless-friendly Select and Multi-Select component for React Native, powered by
@gorhom/bottom-sheet.
react-native-modern-select is designed for real production apps and internal design systems where:
- the data shape is not fixed
- strong typing matters
- UI must be customizable
- and large option lists must remain usable
---
- ✅ Single select & multi select
- ✅ Fully type-safe generic API (Select)
- ✅ Works with any data shape (no forced {label, value} model)
- ✅ Built-in search
- ✅ Bottom sheet powered by @gorhom/bottom-sheet
- ✅ Custom input renderer
- ✅ Custom option renderer
- ✅ Custom footer (for multi-select)
- ✅ Confirm / close footer for multi-select
- ✅ Styling hooks for default UI
- ✅ Suitable for design systems and component libraries
---
``bash`
npm install react-native-modern-select
or
`bash`
yarn add react-native-modern-select
---
This package depends on the following libraries which must already be installed in your app:
`bash`
npm install @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler
---
Because this component uses @gorhom/bottom-sheet, your application must already be configured correctly.
---
Wrap your application root:
`tsx
import { GestureHandlerRootView } from "react-native-gesture-handler";
`
---
In babel.config.js:
`js`
module.exports = {
presets: ["module:metro-react-native-babel-preset"],
plugins: ["react-native-reanimated/plugin"],
};
---
`tsx
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
`
---
`tsx
import { Select } from "react-native-modern-select";
type User = {
id: string;
name: string;
};
const [user, setUser] = useState
value={user}
options={users}
onChange={setUser}
getKey={(u) => u.id}
getLabel={(u) => u.name}
/>;
`
---
`tsx
const [selectedUsers, setSelectedUsers] = useState
multiple
value={selectedUsers}
options={users}
onChange={setSelectedUsers}
getKey={(u) => u.id}
getLabel={(u) => u.name}
/>;
`
In multi-select mode:
- selections are toggled immediately
- the bottom sheet remains open
- a footer is displayed for confirmation / closing
---
The component uses a discriminated union based on the multiple flag.
`ts`
multiple?: false
value: T | null
onChange: (value: T) => void
`ts`
multiple: true
value: readonly T[]
onChange: (value: T[]) => void
TypeScript will enforce the correct contract automatically.
---
In most cases, you do not need to explicitly pass the generic type to Select.
TypeScript automatically infers the item type from the options (and/or value) prop:
`tsx`
options={users}
value={selectedUser}
...
/>
However, if you use null as the value and your options are not strongly typed (for example, inline array literals or an empty array), TypeScript may not be able to correctly infer the generic type.
In that case, pass the type explicitly to the component:
`tsx`
options={users}
value={null}
...
/>
This helps TypeScript resolve the correct type and prevents inference-related errors.
---
Search is enabled by default.
`tsx`
Disable search:
`tsx`
---
`tsx`
...
renderInput={(label) => (
)}
/>
When renderInput is provided, the default input UI is not rendered.
---
`tsx`
...
renderOption={(item, selected) => (
{selected &&
)}
/>
---
A default footer button is shown for multi-select.
You can fully replace it:
`tsxApply (${selected.length})
multiple
...
renderFooter={({ selected, confirm }) => (
}`
onPress={confirm}
/>
)}
/>
Footer render context:
`ts`
{
selected: readonly T[]
confirm: () => void
close: () => void
}
---
`tsx`
...
containerStyle={{ marginTop: 12 }}
inputStyle={{ borderColor: "#4f46e5" }}
optionStyle={{ paddingHorizontal: 20 }}
optionTextStyle={{ fontSize: 14 }}
/>
These style props affect only the default UI.
They are ignored when you use renderInput or renderOption.
---
`tsx`
Default:
`ts`
["60%"];
---
Select is fully generic and controlled.
---
| Prop | Type | Description |
| ------------------- | ------------------------------------------- | --------------------------------------- |
| options | readonly T[] | List of selectable items |getLabel
| | (item: T) => string | Returns the label shown for an item |getKey
| | (item: T) => string | Returns a unique stable key for an item |placeholder
| | string | Placeholder text for the default input |disabled
| | boolean | Disables opening the select |isSearchable
| | boolean | Enables or disables the search field |searchPlaceholder
| | string | Placeholder for the search input |renderInput
| | (label: string \| null) => ReactNode | Replaces the default input UI |renderOption
| | (item: T, selected: boolean) => ReactNode | Replaces the default option row |snapPoints
| | (string \| number)[] | Bottom sheet snap points |containerStyle
| | StyleProp | Style for the pressable wrapper |inputStyle
| | StyleProp | Style for the default input container |optionStyle
| | StyleProp | Style for each option row |optionTextStyle
| | StyleProp | Style for the default option text |confirmText
| | string | Label for the default footer button |renderFooter
| | (ctx) => ReactNode | Custom footer renderer for multi-select |
---
| Prop | Type | Description |
| ---------- | -------------------- | ------------------------------- |
| multiple | false \| undefined | Enables single-select mode |value
| | T \| null | Selected value |onChange
| | (value: T) => void | Called when a value is selected |
---
| Prop | Type | Description |
| ---------- | ---------------------- | ----------------------------- |
| multiple | true | Enables multi-select mode |value
| | readonly T[] | Selected values |onChange
| | (value: T[]) => void | Called when selection changes |
---
- This component is fully controlled.
- It does not store selection state internally.
- In multi-select mode, selections are applied immediately.
- The footer is intended for UX confirmation and closing only.
---
This component intentionally avoids enforcing a { label, value } model.
Instead, you provide:
`ts`
getLabel(item);
getKey(item);
This allows the component to work directly with domain models such as:
- users
- products
- roles
- tags
- countries
- CRM / ERP entities
without intermediate mapping layers.
---
Contributions are welcome.
---
`bash`
npm install
npm run build
To test the package inside a React Native app:
`bash`
npm install ../react-native-modern-select
(Using a local file install is recommended instead of npm link for React Native projects.)
---
- Keep the public API backward compatible
- Do not introduce hard dependencies on navigation, forms or state libraries
- Do not bundle native dependencies
- All new features must be fully typed
- Prefer extending the existing headless API:
- renderInputrenderOption
- renderFooter`
-
- Keep default UI minimal and unopinionated
---
Please include:
- React Native version
- iOS / Android
- minimal reproduction steps
---
MIT
---
Built on top of:
- @gorhom/bottom-sheet
- react-native-reanimated
- react-native-gesture-handler