# React Native Pickers
> @elik56a/react-native-pickers: wheel, time, date (calendar, range, multiple, wheel), date-time, select (local or server search), horizontal list (segmented control, chips) and emoji pickers, plus bottom sheets and form fields, for React Native, Expo and React Native Web, with zero native code (pure JavaScript). Native iOS, Material and web designs by default (design="auto"), or one design everywhere; fully themeable.
This file contains all documentation content in a single document following the llmstxt.org standard.
## React Native Pickers, zero native code
## React Native Pickers
**One dependency-free package for every picker a React Native app needs** — wheel, time, duration, date (calendar, range, multiple, wheel), month, week strip, date & time, select / dropdown, segmented control and chips, emoji, color, the bottom sheets to put them in, and the form fields that open them. Each one looks native on iOS, Android and the web, and every detail is yours to customize.
**npm**
```bash
npm install @elik56a/react-native-pickers
```
**yarn**
```bash
yarn add @elik56a/react-native-pickers
```
**pnpm**
```bash
pnpm add @elik56a/react-native-pickers
```
**Expo**
```bash
npx expo install @elik56a/react-native-pickers
```
It is **100% JavaScript with zero native code**, so it runs unchanged in **Expo Go**, **bare React Native** and **React Native Web**. There's no `pod install`, no rebuild and no config plugin.
```tsx live
import { useState } from 'react';
import { DatePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(new Date());
return ;
}
```
:::tip Every example on this site is live
Edit the code and the result updates instantly. Each example is a complete file you can copy into your app as-is, imports included.
:::
### What's included
| Component | What it is |
| --- | --- |
| [`WheelPicker`](./components/wheel-picker) | 3D scrolling wheel with infinite loop, disabled items, tap-to-type, custom rows |
| [`MultiWheelPicker`](./components/multi-wheel-picker) | Several wheels sharing one selection band (durations, units, anything multi-column) |
| [`TimePicker`](./components/time-picker) | Wheels, a Material clock dial or columns to click; 12h / 24h from locale, minute & second steps, min / max time |
| [`DatePicker`](./components/date-picker) | Calendar with **single**, **range** and **multiple** selection, or a day/month/year **wheel** |
| [`MonthPicker`](./components/month-picker) | A month and year, like a card's expiry: wheels on iOS, a grid of months in Material and on the web |
| [`WeekStrip`](./components/week-strip) | One week of days in a row, with marked days; swipe or arrows to change weeks |
| [`DurationPicker`](./components/duration-picker) | A length of time, like a timer or a workout: wheels, Material number boxes or columns to click, the value in seconds |
| [`DateTimePicker`](./components/date-time-picker) | Day and time together: one row of wheels, or a calendar with the time under or beside it |
| [`SelectPicker`](./components/select-picker) | Form field that opens a searchable bottom sheet, modal or dropdown; single or multi-select with chips, groups, icons; [search a server](./recipes/server-search) with `loadOptions` |
| [`SelectList`](./components/select-list) | The same list, inline |
| [`CountryPicker`](./components/country-picker) | About 250 countries with flags and dialing codes, localized names, preferred countries on top |
| [`PhoneField`](./components/phone-field) | Phone number input: a country code button, the number formatted as it's typed, an E.164 value |
| [`TimeZonePicker`](./components/time-zone-picker) | Every IANA time zone with its GMT offset, sorted by offset, the device's zone on top |
| [`HorizontalList`](./components/horizontal-list) | Segmented control or chips that scroll sideways and reveal the next item |
| [`EmojiPicker`](./components/emoji-picker) | 1,923 emojis, categories, search, skin tones, recents, version filtering |
| [`ColorPicker`](./components/color-picker) | iOS's Grid / Spectrum / Sliders, Google's swatch circles, or the browser's spectrum; hex with opacity and a copy button |
| [`BottomSheet`](./components/bottom-sheet) | Animated sheet with drag-to-dismiss, for your own content |
| [`DatePickerSheet`, `TimePickerSheet`, `DateTimePickerSheet`, `DurationPickerSheet`, `MonthPickerSheet`, `ColorPickerSheet`, `WheelPickerSheet`, `EmojiPickerSheet`](./components/picker-sheets) | Ready-made pickers in a sheet with Cancel / Done and draft state |
| [`DatePickerField`, `TimePickerField`, `DateTimePickerField`, `DurationPickerField`, `MonthPickerField`, `ColorPickerField`, `WheelPickerField`, `EmojiPickerField`, `PickerField`](./components/picker-fields) | Form fields that open a picker sheet and show the picked value, styled like `SelectPicker` |
### Why this library
- **No native code.** Works in Expo Go and on the web with nothing to link, and ships in OTA updates.
- **Native on every platform.** iOS on iPhone, [Material Design](./guides/designs) on Android, a web design in the browser, all from one code path. Prefer one look everywhere? `` and the iOS design runs on Android and the web too.
- **One consistent API.** Every picker takes `value` + `onChange` (controlled) or `defaultValue` (uncontrolled), a `theme`, a `styles` object for each visual part, and render props for full control.
- **Yours to customize.** Light and dark themes are built in and follow the OS automatically. Override any token (colors, radius, spacing, fonts) globally with `PickersProvider` or per component, style any part through `styles`, and replace rows, days or triggers with render props. Your settings always win over the design.
- **International.** Month, weekday and AM/PM names, the 12/24-hour clock, the first day of the week and the date-wheel order all come from `Intl` for any locale. Layouts flip for RTL.
- **Accessible.** Wheels are `adjustable` (VoiceOver / TalkBack swipe up and down). Days announce their full date, and selected / disabled states are exposed.
- **Fast.** Animations use the native driver. Wheels and the emoji grid are virtualized with fixed row layouts, and rows are memoized.
- **Typed.** Written in TypeScript, with generics for value types and discriminated unions for selection modes, so `mode="range"` gives you a typed `{ start, end }`.
### Requirements
| | Version |
| --- | --- |
| `react` | `>= 18` |
| `react-native` | `>= 0.72` |
| Expo | Any SDK (works in Expo Go) |
| `react-native-web` | Supported |
No other peer dependencies: no Reanimated, no Gesture Handler, no SVG.
### Where to next
- [Installation](./getting-started/installation): install and optional setup
- [Quick start](./getting-started/quick-start): every picker in five minutes
- [Core concepts](./getting-started/core-concepts): controlled vs uncontrolled, sheets and drafts, designs, theming, slots
- [Designs](./guides/designs): the iOS, Material and web looks, and how to pick or customize one
- [Showcase](/showcase): real app screens built with the pickers, live on iOS, Android and the web
- [API reference](./api/reference): every export in one page
---
## For AI assistants
This documentation is published in machine-readable form, following the [llms.txt](https://llmstxt.org) convention:
| File | Contents |
| --- | --- |
| [`/llms.txt`](pathname:///llms.txt) | Index of every page with a one-line summary |
| [`/llms-full.txt`](pathname:///llms-full.txt) | **The entire documentation in one Markdown file**: every component, prop table and example |
| `/docs/**/*.md` | Each page as plain Markdown |
Paste `llms-full.txt` into your assistant's context, or point Cursor / Claude / Copilot at the URL.
`llms-full.txt` also ships inside the npm package, so a coding agent can read it offline from `node_modules/@elik56a/react-native-pickers/llms-full.txt`, always matching the installed version.
### Cheat sheet
```text
Package: @elik56a/react-native-pickers (npm i @elik56a/react-native-pickers)
Zero native code (pure JS), no peer deps besides react >=18 and react-native >=0.72.
Works in Expo Go, bare RN, React Native Web. All exports are named exports from the package root.
Looks native per platform by default (design 'auto': iOS on iOS, Material on Android, web on web). One look everywhere:
(or 'material' | 'web'); per component: design="material". theme/styles/explicit props always win over the design.
Every component takes: theme (DeepPartial), design ('ios'|'material'|'web'), styles (slot styles), style, testID.
Native apps: prefer subpaths so Metro skips unused pickers (root import bundles ~130 KB of emoji data):
/wheel /time /date /calendar /datetime /select /list /emoji /sheet /core e.g. '@elik56a/react-native-pickers/date'
PickersProvider and theme/date utils live in /core. Subpaths need RN 0.79+ (Metro package exports).
Controlled: value + onChange. Uncontrolled: defaultValue. SelectPicker/SelectList/HorizontalList are controlled-only.
onChange fires on user changes only (not when value is set programmatically). Wheels fire onChange when they settle;
onValueChanging fires on every row crossing (use for haptics).
WheelPicker items: (T | {label, value, disabled?})[] value onChange(value, index) loop itemHeight=40 visibleItems=5
variant '3d'|'flat' keyboardInput=true isEqual renderItem({item,index,selected,theme}) styles
MultiWheelPicker columns: [{ key, items, value, onChange, flex?, width?, loop?, suffix? }] separator: node | (leftIndex)=>node (suffix = unit text after a column)
TimePicker value: Date onChange(date) format '12h'|'24h'|'auto' minuteInterval showSeconds minTime/maxTime locale
display 'wheel'(ios) | 'dial'(material clock) | 'input'(typed fields) | 'list'(web columns); default from the design
DurationPicker a length of time, not a time of day: value: number (seconds) onChange(seconds) showHours=true showSeconds=false
display 'wheel'(ios) | 'input'(material number boxes) | 'list'(web columns); default from the design
min max (seconds; default max 23h59m59s, or 59m59s without hours) minuteInterval secondInterval loop (minutes/seconds)
labels {hours:'h',minutes:'min',seconds:'s'} formatDuration(5400) → '1 h 30 min'
DateTimePicker value: Date onChange(date) display 'wheel'(ios default)|'calendar'(material/web default) minDate maxDate (moments)
timePosition 'beside'|'below' timeDisplay 'wheel'|'dial'|'input'|'list' minuteInterval format locale calendarProps
DatePicker display 'calendar'(default)|'wheel'
mode 'single' value: Date|null onChange(date)
mode 'range' value: {start,end} onChange(range) minRangeDays maxRangeDays allowSingleDayRange
mode 'multiple' value: Date[] onChange(dates) maxSelections
mode 'ranges' value: DateRange[] onChange(ranges) maxRanges minRangeDays maxRangeDays allowSingleDayRange
minDate maxDate isDateDisabled(date) markedDates{'YYYY-MM-DD':{dots?,label?,textColor?,disabled?}}
locale firstDayOfWeek(0=Sun) initialMonth onMonthChange renderDay(state,theme) dayHeight=44
DateWheelPicker value: Date onChange order ['day','month','year'] monthFormat 'long'|'short'|'numeric' min/maxDate
MonthPicker a month without a day: value: Date (first of the month) onChange(month) display 'wheel'(ios: month+year wheels) | 'grid'(material/web: months under the year, tap the year for years)
minDate maxDate (any day in the month) yearRange monthFormat 'long'|'short'|'numeric' (wheels) locale
WeekStrip one week of days in a row: value: Date|null onChange(date) swipe/arrows page weeks onWeekChange(weekStart) initialWeek
minDate maxDate isDateDisabled markedDates firstDayOfWeek locale showHeader formatTitle(weekStart) weekdayFormat renderDay (calendar's day cells and slots)
SelectPicker options: {label,value,description?,icon?,group?,disabled?,keywords?}[] value onChange(value, option)
multiple → value T[], onChange(values, options), maxSelected, chips
label placeholder searchable(dropdown: type in field, default on; sheet: >8 auto) clearable+onClear (single: reset value in onClear) renderTrigger({open,selected}) renderChevron({open,theme})
presentation 'auto'(dropdown on web, bottom sheet on native)|'bottomSheet'|'modal'|'dropdown'
SelectList same list inline: options value onChange multiple listHeight
server search: loadOptions(query) => Promise (debounced by searchDebounce=300ms, loader until results, stale answers ignored; options = shown before typing; searchPrompt='Type to search' when none, emptyText after an empty search)
or manual: onSearchChange(query) + loading + onEndReached; loadingText renderLoading(theme); no local filtering with either
CountryPicker a SelectPicker with ~250 countries built in: value CountryCode ('IL') or CountryCode[] with multiple onChange(code, option)
preferred (pinned on top) countries exclude showDialCode locale (names via Intl.DisplayNames, English fallback; search matches English/native names, ISO code, +dial)
every SelectPicker prop except options/loadOptions/server search; getCountry(code) → {code,name,nativeName?,dialCode,flag,keywords} getAllCountries() getCountryFlag(code)
PhoneField phone number input: flag + calling code button (CountryPicker list) then the number, formatted as typed ('(201) 555-0123', '54-123-4567')
value/defaultValue: E.164 string ('+972541234567', '' when empty) onChange(value, {country, nationalNumber, valid}) valid = length fits the country
country/defaultCountry='US'/onCountryChange label placeholder(default example) disabled presentation countrySheetTitle='Country code' sheet
preferred countries exclude locale preferredLabel allCountriesLabel; styles: label field input countryButton dialCode chevron
a leading trunk 0 is dropped (not IT/SM/VA); pasting '+44…' switches country; ~50 country formats, others grouped in threes
utils: parsePhone(intl, current?) → {country?, nationalNumber} formatNationalNumber(digits, code) isValidPhone(digits, code) toE164(digits, code)
TimeZonePicker a SelectPicker with all 418 IANA zones built in: value IANA name ('Europe/Lisbon') or string[] with multiple onChange(id, option)
label = city, description 'GMT+1 · Central European Standard Time' (zone name where the engine has it, else region); sorted by offset
preferred (default: the device's zone, under 'Your time zone'; [] pins none) timeZones (only these) date (offsets for that moment, DST-aware) locale
old names ('Asia/Calcutta', 'Europe/Kiev') accepted as value, reported as current ('Asia/Kolkata'); search matches city, IANA name, region, GMT/UTC offset, zone name
utils: getDeviceTimeZone() getTimeZoneOffset(id, date?) → minutes formatGmtOffset(330) → 'GMT+5:30' getTimeZoneName(id, locale?, date?) normalizeTimeZone(id) getTimeZone(id) getAllTimeZones()
HorizontalList items: {label,value,disabled?,icon?}[] (icon: node or ({selected,disabled,color}) => node) value onChange(value, item) variant 'segmented'|'chips' iconPosition 'left'|'right'|'top'|'bottom' showArrows (‹ › scroll buttons, default false) arrowPosition 'outside'|'overlay' (default 'outside') renderArrow(side, disabled) renderItem (tap at the edge scrolls the next item into view)
EmojiPicker onSelect(emoji, data) columns=8 height=380 categories exclude maxEmojiVersion skinTone recent onRecentChange categoryBarPosition 'auto'(bottom on ios/material, top on web)|'top'|'bottom'
ColorPicker value: hex ('#E4572E', or '#E4572E80' with showAlpha) onChange('#RRGGBB'|'#RRGGBBAA', fires while dragging)
display 'tabs'(ios: Grid/Spectrum/Sliders) | 'swatches'(material: circles + Custom) | 'spectrum'(web: square, hue, hex, eyedropper); default from the design
colors (hex | {value,label})[]=DEFAULT_COLORS showCustom=true showAlpha=false onCopy(hex) (copy button; web clipboard by default, native needs it, e.g. expo-clipboard) labels normalizeHex hexToHsv hsvToHex withAlpha
BottomSheet visible onClose title headerLeft headerRight footer height(<=1 fraction | px) enableSwipeToClose draggable(header drags too; native:true web:false; handle always drags) expandable expandedHeight
Sheets (draft; commit on Done):
DatePickerSheet visible onClose value onConfirm + all calendar props + mode + display + confirmOnSelect
TimePickerSheet visible onClose value onConfirm + TimePicker props
WheelPickerSheet visible onClose items value onConfirm
EmojiPickerSheet visible onClose onSelect closeOnSelect=true
ColorPickerSheet visible onClose value onConfirm(color) + ColorPicker props
DateTimePickerSheet visible onClose value onConfirm + DateTimePicker props
DurationPickerSheet visible onClose value onConfirm(seconds) + DurationPicker props
MonthPickerSheet visible onClose value onConfirm(month) + MonthPicker props
PickerSheet visible onClose onConfirm children + useDraft(value, visible)
Fields (a SelectPicker-style field that opens the sheet and shows the confirmed value; field owns open state):
DatePickerField / TimePickerField / DateTimePickerField / DurationPickerField / MonthPickerField / ColorPickerField / WheelPickerField / EmojiPickerField
= the matching *Sheet props WITHOUT visible (value, onConfirm | onSelect, mode, locale, title...)
+ label placeholder='Select…' displayValue(default: value formatted for locale) clearable+onClear(reset value there)
disabled renderTrigger({open,displayValue,disabled}) renderChevron({open,theme}) onOpen onClose style testID
presentation 'auto'(dropdown on web, bottom sheet on native)|'bottomSheet'|'modal'|'dropdown'; design is passed on to the sheet
styles = field slots (label trigger triggerText placeholder chevron clear) + the picker's slots
EmojiPickerField also takes value: string (the emoji shown). Sheet title defaults to label.
PickerField same field props, children: ({visible, onClose}) =>
Theme: ; per-component `theme` and `design` props. `platform` makes pickers behave as on that OS (preview native in a browser). `design` is the look: 'auto' = ios on iOS, web on web, material elsewhere; sheet actions in the header (ios: Cancel/Done) or footer (material: Cancel/OK), radios for single select and outlined floating-label fields in material, a date headline above Material calendars, TimePicker display: 'wheel' (ios) | 'dial' (material clock dial with a keyboard-entry toggle) | 'input' (typed hour/minute fields) | 'list' (web: hour/minute/AM-PM columns to click); dial and input fall back to wheels with showSeconds. DateTimePicker display defaults: 'wheel' on ios, 'calendar' in material (time input under it) and web (time columns beside it); timePosition 'beside'|'below' overrides where the time goes (beside moves below when both don't fit), timeDisplay 'wheel'|'dial'|'input'|'list' how it's picked. Calendar: ios tints the chosen day, material fills it and rings today with narrow S M T weekdays, web uses filled rounded squares, ripple in material and a dim while pressed on ios/web.
tokens: colors{background,surface,sheet,text,textSecondary,textDisabled,primary,onPrimary,primaryMuted,border,backdrop,indicator,handle}
radius{sm,md,lg,full,sheet} spacing{xs..xl} typography{fontFamily,fontFamilyMedium,sizes,weights} animation{duration}
Keyboard (web, built in): arrows move focus through calendars, lists and grids by position (RTL-aware) and step wheels, sliders and the dial; Home/End; Esc closes sheets; focus returns to the field.
Slots: every component takes `styles={{ slotName: style }}`.
Utils: toDateKey(d) 'YYYY-MM-DD' local; addDays addMonths isSameDay compareDay isBetweenDays formatDate(d, locale, opts)
searchEmojis findEmoji withSkinTone getAllEmojis; defaultFilter for select search.
```
### Canonical examples
```tsx
import { useState } from 'react';
import {
DatePicker,
DatePickerField,
DatePickerSheet,
DateTimePicker,
HorizontalList,
PickersProvider,
SelectPicker,
TimePicker,
TimePickerField,
WheelPicker,
} from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
function Examples() {
const [size, setSize] = useState('M');
const [time, setTime] = useState(new Date());
const [date, setDate] = useState(null);
const [range, setRange] = useState({ start: null, end: null });
const [country, setCountry] = useState(null);
const [open, setOpen] = useState(false);
const [view, setView] = useState('day');
return (
<>
setOpen(false)} mode="range" value={range} onConfirm={setRange} />
{/* In forms: a field that opens the sheet and shows the value, no open state needed */}
>
);
}
// Optional, once at the root: brand colors, and one design on every platform instead of the native one per OS
export const App = () => (
);
```
---
## Install React Native Pickers
## Installation
React Native Pickers is pure JavaScript: one package, with no native linking, pods or rebuilds.
**npm**
```bash
npm install @elik56a/react-native-pickers
```
**yarn**
```bash
yarn add @elik56a/react-native-pickers
```
**pnpm**
```bash
pnpm add @elik56a/react-native-pickers
```
**Expo**
```bash
npx expo install @elik56a/react-native-pickers
```
That's it. The package is pure JavaScript, so there is:
- no `pod install`
- no native rebuild — it works in **Expo Go**
- no Babel plugin, Metro config or config plugin
- no other dependency (no Reanimated, Gesture Handler or SVG)
Peer dependencies are `react >= 18` and `react-native >= 0.72`, which your app already has.
### Optional: add the theme provider
Every component works with sensible light and dark defaults that follow the OS appearance, and looks native on each platform (iOS, Material on Android, web). To use your brand colours everywhere, or one design on every platform, wrap your app once:
```tsx title="App.tsx"
import { PickersProvider } from '@elik56a/react-native-pickers';
export default function App() {
return (
);
}
```
See [Theming](../guides/theming) for every token and [Designs](../guides/designs) for the looks.
### React Native Web
Nothing extra is needed beyond a standard `react-native-web` setup (Expo web already has one). If you bundle manually, alias `react-native` to `react-native-web`:
```js title="webpack.config.js"
module.exports = {
resolve: { alias: { 'react-native$': 'react-native-web' } },
};
```
This documentation site is itself built this way: every live example is the real library running on React Native Web.
### Importing
Everything is a named export from the package root:
```tsx
import {
WheelPicker,
MultiWheelPicker,
TimePicker,
DatePicker,
Calendar,
DateWheelPicker,
SelectPicker,
SelectList,
HorizontalList,
EmojiPicker,
BottomSheet,
PickerSheet,
DatePickerSheet,
TimePickerSheet,
WheelPickerSheet,
EmojiPickerSheet,
PickersProvider,
} from '@elik56a/react-native-pickers';
```
The package is marked `sideEffects: false`, so bundlers that tree-shake (web) drop what you don't import. On native, Metro resolves the TypeScript source through the `react-native` export condition.
#### Smaller native bundles
Metro doesn't tree-shake, so a root import bundles every picker on iOS and Android, including about 130 KB of emoji data. Import from the feature subpaths instead and Metro only bundles what you use:
```tsx
import { DatePicker, DatePickerSheet } from '@elik56a/react-native-pickers/date';
import { SelectPicker } from '@elik56a/react-native-pickers/select';
import { PickersProvider } from '@elik56a/react-native-pickers/core';
```
| Subpath | Exports |
| --- | --- |
| `/wheel` | `WheelPicker`, `MultiWheelPicker`, `WheelPickerSheet` |
| `/time` | `TimePicker`, `TimePickerSheet` |
| `/date` | `DatePicker`, `DateWheelPicker`, `DatePickerSheet` |
| `/calendar` | `Calendar` |
| `/datetime` | `DateTimePicker`, `DateTimePickerSheet` |
| `/select` | `SelectPicker`, `SelectList` |
| `/list` | `HorizontalList` |
| `/emoji` | `EmojiPicker`, `EmojiPickerSheet`, emoji utils |
| `/sheet` | `BottomSheet`, `PickerSheet`, `useDraft` |
| `/core` | `PickersProvider`, themes, date utils |
Each subpath also exports its types. Subpaths need React Native 0.79+ (Expo SDK 53+), where Metro reads package `exports` by default. On older versions, set `resolver.unstable_enablePackageExports = true` in `metro.config.js` or keep the root import.
---
## Quick start
Each block below is a complete, runnable component. Edit it, and copy it straight into your app when you're done.
### Wheel
Pass primitives or `{ label, value }` objects.
```tsx live
import { useState } from 'react';
import { WheelPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [size, setSize] = useState('M');
return (
);
}
```
### Time
The 12h / 24h format comes from the locale automatically.
```tsx live
import { useState } from 'react';
import { TimePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [time, setTime] = useState(new Date());
return ;
}
```
### Date (calendar)
```tsx live
import { useState } from 'react';
import { DatePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(null);
return ;
}
```
### Date range
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { DatePicker, formatDate } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
export default function Example() {
const [range, setRange] = useState({ start: null, end: null });
return (
<>
{range.start ? formatDate(range.start) : '—'} →{' '}
{range.end ? formatDate(range.end) : '—'}
>
);
}
```
### Date (wheel)
```tsx live
import { useState } from 'react';
import { DatePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [birthday, setBirthday] = useState(new Date(1990, 4, 17));
return (
);
}
```
### Select
Tap the field to open a searchable bottom sheet.
```tsx live
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';
const languages = [
{ label: 'English', value: 'en' },
{ label: 'Español', value: 'es' },
{ label: 'Français', value: 'fr' },
{ label: 'Deutsch', value: 'de' },
{ label: 'עברית', value: 'he', keywords: ['hebrew'] },
];
export default function Example() {
const [lang, setLang] = useState(null);
return (
);
}
```
### Emoji
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { EmojiPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [text, setText] = useState('');
return (
<>
{text} setText((t) => t + emoji)}
/>
>
);
}
```
### In a bottom sheet
Every picker also ships as a sheet. The sheet keeps a **draft** and only calls `onConfirm` when the user taps **Done**; **Cancel**, a backdrop tap or a swipe down discards it.
```tsx live
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
import { DatePickerSheet, formatDate } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
export default function Example() {
const [open, setOpen] = useState(false);
const [range, setRange] = useState({ start: null, end: null });
return (
<>
setOpen(true)}
style={{ backgroundColor: '#C8460D', padding: 14, borderRadius: 12 }}
>
{range.start && range.end
? `${formatDate(range.start)} → ${formatDate(range.end)}`
: 'Pick dates'}
setOpen(false)}
title="Check-in / Check-out"
mode="range"
value={range}
onConfirm={setRange}
minDate={new Date()}
/>
>
);
}
```
### In a form field
A field opens the sheet for you and shows the confirmed value, so there's no open state to manage. Each sheet has one: `DatePickerField`, `TimePickerField`, `DateTimePickerField`, `WheelPickerField` and `EmojiPickerField`.
```tsx live
import { useState } from 'react';
import { View } from 'react-native';
import {
DatePickerField,
TimePickerField,
} from '@elik56a/react-native-pickers';
export default function Example() {
const [day, setDay] = useState(null);
const [time, setTime] = useState();
return (
);
}
```
→ [Picker fields](../components/picker-fields)
### Pick a design
Each picker looks native on its platform by default. Use the switch at the top left of any example to see iOS, Android and Web. To use one look on every platform, set `design` on the provider or on one component:
```tsx live
import { useState } from 'react';
import { TimePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [time, setTime] = useState(new Date());
return (
);
}
```
→ [Designs](../guides/designs)
### Next
- [Core concepts](./core-concepts): the few ideas every component shares
- [Components](../components/wheel-picker): full props for each picker
---
## Core concepts
Every component follows the same handful of conventions. Learn them once and you know the whole library.
### Controlled and uncontrolled
Wheels, time, date and emoji state can be used either way:
```tsx
// Controlled: you own the state
const [date, setDate] = useState(new Date());
// Uncontrolled: the component owns it; you only listen
console.log(d)} />
```
| Component | Controlled | Uncontrolled |
| --- | --- | --- |
| `WheelPicker` | `value` | `defaultValue` |
| `TimePicker` | `value` | `defaultValue` (default: now) |
| `DatePicker` / `Calendar` | `value` | `defaultValue` |
| `DateWheelPicker` | `value` | `defaultValue` (default: today) |
| `EmojiPicker` skin tone | `skinTone` | `defaultSkinTone` |
| `EmojiPicker` recents | `recent` | `defaultRecent` |
| `SelectPicker` / `SelectList` | `value` | — (always controlled) |
| `HorizontalList` | `value` | — (always controlled) |
:::caution Select is controlled-only
`SelectPicker` and `SelectList` don't keep their own selection. Always pass `value` and update it in `onChange`.
:::
Setting `value` from outside (for example, a "Reset" button) moves the component **without** firing `onChange`. `onChange` only reports changes the user made.
### `onChange` vs `onValueChanging`
Wheel-based pickers (`WheelPicker`, `MultiWheelPicker` columns, `TimePicker`, `DateWheelPicker`) fire two callbacks:
| Callback | When | Use it for |
| --- | --- | --- |
| `onChange` | Once, after the wheel **settles** on a new value | Updating state, saving |
| `onValueChanging` | On **every row crossing** while scrolling | Haptics, live previews |
```tsx
Haptics.selectionAsync()}
/>
```
### Sheets and drafts
`DatePickerSheet`, `TimePickerSheet` and `WheelPickerSheet` wrap a picker in a [`BottomSheet`](../components/bottom-sheet) with **Cancel** and **Done**:
- The sheet copies `value` into a **draft** each time it opens.
- Scrolling or tapping changes only the draft.
- **Done** calls `onConfirm(draft)`, then `onClose()`.
- **Cancel**, a backdrop tap, a swipe down or Android back calls `onClose()` only, so the draft is discarded.
```tsx
setOpen(false)} value={time} onConfirm={setTime} />
```
`EmojiPickerSheet` has no Done button: picking an emoji calls `onSelect` and (by default) closes the sheet. `SelectPicker` opens its own sheet; you don't manage `visible`.
### Fields
In a form, the picker hides behind a field. `SelectPicker` is one, and every sheet has a field version that takes the same props without `visible` / `onClose`: [`DatePickerField`, `TimePickerField`, `DateTimePickerField`, `WheelPickerField`, `EmojiPickerField`](../components/picker-fields). The field owns the open state, shows the confirmed value formatted for `locale`, and looks the same as `SelectPicker`'s field. By default it opens a bottom sheet on iOS and Android and a dropdown under the field on the web; `presentation` picks `'bottomSheet'`, `'modal'` or `'dropdown'`.
```tsx
```
### Designs
Pickers look native wherever they run: the iOS design on iOS, Material Design on Android, a web design in the browser. The design sets shapes and layout (where Done sits, a radio or a ✓, wheels or a clock dial), never your colors. Use one look everywhere, or for one component, with `design`:
```tsx
{/* the iOS look on Android and the web too */}
```
→ [Designs guide](../guides/designs)
### Theming
Three layers, each merged on top of the previous one:
1. Built-in `lightTheme` / `darkTheme`, chosen from the OS appearance, with the [design](../guides/designs)'s shapes
2. `` for your whole app
3. The `theme` prop on any single component
```tsx
{/* green only here */}
```
→ [Theming guide](../guides/theming)
### Slot styles
Every visual part of a component has a named **slot**. Pass styles for any of them through `styles`:
```tsx
```
Slot names are typed, so your editor autocompletes them. Each component page lists its slots. → [Styling guide](../guides/styling)
### Render props
When styles aren't enough, replace a part entirely:
| Component | Render props |
| --- | --- |
| `WheelPicker` | `renderItem`, `renderIndicator` |
| `DatePicker` / `Calendar` | `renderDay`, `renderArrow`, `formatTitle` |
| `SelectPicker` / `SelectList` | `renderTrigger`, `renderOption`, `renderGroupHeader`, `renderLoading` |
| `HorizontalList` | `renderItem` |
| `EmojiPicker` | `renderEmoji`, `categoryIcons` |
| `*PickerField` / `PickerField` | `renderTrigger`, `renderChevron` |
→ [Render props guide](../guides/render-props)
### Strings and i18n
No component hard-codes a user-facing string you can't change. Done, Cancel, placeholders, search hints, empty states and category labels are all props. Month, weekday and AM/PM names come from `Intl` for whatever `locale` you pass. → [i18n & RTL](../guides/i18n-rtl)
---
## iOS, Material & web designs
## Designs
Every picker looks native on the platform it runs on, from one code path: **iOS** on iPhone and iPad, **Material Design** on Android, and a **web** design in the browser. You don't write anything for this; it's the default.
A design changes shapes and layout: where Cancel and Done sit, how a field and a chosen day look, whether time is picked on wheels, a clock dial or columns. It never changes your colors, fonts or anything you set yourself.
```tsx live
import { useState } from 'react';
import { View } from 'react-native';
import {
DatePickerField,
HorizontalList,
TimePicker,
} from '@elik56a/react-native-pickers';
import type { Design } from '@elik56a/react-native-pickers';
const DESIGNS = [
{ label: 'iOS', value: 'ios' },
{ label: 'Material', value: 'material' },
{ label: 'Web', value: 'web' },
] as const;
export default function Example() {
const [design, setDesign] = useState('material');
const [date, setDate] = useState(null);
const [time, setTime] = useState(new Date());
return (
);
}
```
### What each design changes
| | `ios` | `material` | `web` |
| --- | --- | --- | --- |
| Fields | label above, 48 tall | outlined, 56 tall; the label sits inside, then floats onto the border | label above, 40 tall, focus ring while open |
| Sheet corners | 24 | 28 | 12 |
| Cancel / confirm | beside a centered title, **Done** | after the content, **OK** | after the content, **Done** |
| Single select | ✓ beside the chosen option | a radio on every option | ✓ beside the chosen option |
| Option rows | rounded and tinted when chosen | edge to edge, untinted | compact, rounded and tinted |
| Calendar | chosen day tinted, today's number coloured, SUN MON | chosen day filled, today ringed, S M T | chosen day a filled square, today shaded, Sun Mon |
| Date sheet | calendar | the picked date in large type above the calendar | calendar |
| Time | wheels | a clock dial | columns of hours and minutes to click |
| Date & time | one row of wheels | calendar with time fields to type into under it | calendar with the time columns beside it |
| Duration | wheels with units | number boxes to type into | columns to click |
| Month | month and year wheels | a grid of months under the year | a grid of months under the year |
| WeekStrip | chosen day tinted, SUN MON | chosen day filled, S M T | chosen day a filled square, Sun Mon |
| Colour | Grid, Spectrum and Sliders tabs, swatches in a row to scroll | swatch circles, then **Custom** for a saturation square and hue slider | a saturation square, eyedropper, hue slider and hex code, then swatches |
| HorizontalList | a sliding raised thumb; filled chips | outlined segments and chips, the chosen one filled with a ✓ | a sliding raised thumb; filled chips |
| Search fields | a short gray search bar | a tall pill | an outlined input with a focus ring |
| EmojiPicker | categories at the bottom, the current one on a gray fill, UPPERCASE titles | categories at the bottom, the current one in a tinted pill | categories on top, underlined |
| Press | dims while held | ripple | dims while held |
`design="auto"` (the default) picks `ios` on iOS, `web` on the web, and `material` everywhere else (Android, Windows, macOS).
### One design everywhere
Set `design` on the provider to use one look on every platform. To keep the iOS look on Android and the web:
```tsx
```
Or Material everywhere, for an app built on Material Design:
```tsx
```
### One component
`design` on a component wins over the provider's. A field passes its design to the sheet it opens.
```tsx
```
### Only part of a design
Some choices a design makes are props of their own, so you can keep the rest of a design and change just that:
| To get | Set |
| --- | --- |
| Wheels for time in any design | `display="wheel"` on `TimePicker` |
| A clock dial, typed fields or columns | `display="dial"`, `"input"` or `"list"` on `TimePicker` |
| One row of date and time wheels | `display="wheel"` on `DateTimePicker` |
| A calendar with time wheels | `display="calendar"` and `timeDisplay="wheel"` on `DateTimePicker` |
| The time beside or under the calendar | `timePosition="beside"` or `"below"` on `DateTimePicker` |
| `Mon` instead of `M` | `weekdayFormat="short"` on `DatePicker` |
| Your own button labels | `confirmLabel` / `cancelLabel` on any sheet or field |
### Customize a design
A design is a starting point. Everything you set wins over it:
- **Theme tokens.** `theme` and `darkTheme` on `PickersProvider` (colors, radius, spacing, fonts) are merged on top of the design's tokens. `radius.sheet` sets sheet corners in every design. See [Theming](./theming).
- **Slot styles.** Every visual part takes a style through `styles`. See [Styling with slots](./styling).
- **Render props.** Replace a row, a day, a trigger or a chevron and keep the behavior. See [Render props](./render-props).
- **Explicit props.** `confirmLabel`, `weekdayFormat`, `display` and the others above always win over the design's defaults.
`DESIGN_TOKENS` shows exactly what each design changes on the light or dark theme. In your own components, `usePickerTheme().design` tells you which design to match:
```tsx
const theme = usePickerTheme();
const radius = theme.design === 'material' ? theme.radius.full : theme.radius.md;
```
### Upgrading from 0.10
Before 0.11, every platform got the iOS look: wheels for times, one row of wheels for `DateTimePicker`, **Done** in the sheet header. Android and web apps now get the Material and web designs. To keep the old look, set `design="ios"` on the provider. To keep only part of it, use the props in [Only part of a design](#only-part-of-a-design).
### Upgrading from 0.12
`EmojiPicker` now follows the design too: on iOS and Android its category bar moves to the bottom, like the platform's own emoji keyboard, and search fields (in `EmojiPicker`, `SelectList` and `SelectPicker`) take each design's shape. To keep the bar on top, set `categoryBarPosition="top"`.
### Previewing other platforms
`design` sets the look. To also see how pickers *behave* on another platform in a browser (fields opening a bottom sheet instead of a dropdown, room for the iOS home indicator), set `platform` on the provider. See [Previewing other platforms](./platforms#previewing-other-platforms). The iOS / Android / Web switch on every live example on this site does both.
---
## React Native Bottom Sheet
## BottomSheet
`BottomSheet` is the React Native bottom sheet every picker sheet is built on, exported so your own content can use it: filters, menus, confirmations, forms. It slides up over a dimmed backdrop, can be dragged down to dismiss, and is built on React Native's `Modal`, `Animated` and `PanResponder` only.
```tsx
import { BottomSheet } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { Pressable, Text, View } from 'react-native';
import { BottomSheet } from '@elik56a/react-native-pickers';
export default function Example() {
const [open, setOpen] = useState(false);
return (
<>
setOpen(true)}
style={{ backgroundColor: '#C8460D', padding: 14, borderRadius: 12 }}
>
Open sheet
setOpen(false)}
title="Share"
footer={
setOpen(false)}
style={{
backgroundColor: '#F4F2EE',
padding: 14,
borderRadius: 12,
}}
>
Close
}
>
{['Copy link', 'Send via message', 'Share to…'].map((a) => (
{a}
))}
>
);
}
```
### Try the props
### Bottom sheet or modal
`presentation` switches between a bottom sheet and a centered modal:
- `'auto'` (default): a modal on web windows wider than the sheet (688 px), a bottom sheet everywhere else.
- `'bottomSheet'`: always slides up from the bottom, with a drag handle.
- `'modal'`: always a centered dialog that fades in, with no drag handle or swipe-to-close.
```tsx
…
```
### Opening and closing
`BottomSheet` is fully controlled: render it with `visible` and change `visible` in `onClose`. `onClose` is called when the user:
- taps the backdrop (`closeOnBackdropPress`, default `true`)
- drags the sheet down past about 30% of its height (max 140 px), or flicks it down; on the web, scrolls the wheel down over it (`enableSwipeToClose`, default `true`)
- presses Android's back button
When `visible` turns `false`, the sheet animates out and then unmounts its children. `onOpened` and `onClosed` fire when the animations finish, which is handy for focusing inputs or running work after the sheet is gone.
### Header
The header row shows when you pass `title`, `headerLeft` or `headerRight`. A string `title` is styled for you (use the `title` slot); a node is rendered as-is. The drag handle and header are both drag areas.
```tsx
}
headerRight={}
>
…
```
### Height
- **Default**: the sheet sizes to its content, up to `maxHeightRatio` (default `0.9`) of the window.
- **`height` ≤ 1**: a fraction of the window height (`height={0.6}` → 60%).
- **`height` > 1**: pixels.
With a fixed height, the content area becomes `flex: 1`, so a `FlatList` or `ScrollView` inside fills it and scrolls.
`bottomInset` adds bottom padding for the home indicator (default 24 on iOS, 12 elsewhere). Pass your safe-area inset for pixel-perfect results:
```tsx
const insets = useSafeAreaInsets();
```
On tablets and the web the sheet is centred with a maximum width of 640.
### Expanding to full screen
The sheet can grow past its content height to `expandedHeight` (default: the whole window below the status bar), and collapse back:
| | Expand | Collapse, then close |
| --- | --- | --- |
| **iOS / Android** | Drag the handle or header up | Drag it down |
| **Web** | Wheel up over the sheet, or drag the handle up | Wheel down, or drag the handle down |
Released past halfway, or with a flick, the sheet snaps to the expanded height; otherwise it settles back. A list or scroll view inside keeps the wheel until it reaches its top or bottom, then the sheet takes over.
The handle always drags. On the web the header row doesn't by default (`draggable={false}`), so mouse drags there still select text; pass `draggable` to make the whole header a drag area. `expandable={false}` keeps the sheet at its content height on every platform.
Try it: open the sheet, then scroll the wheel up over it (or drag the header, since this example turns `draggable` on). Wide web windows show a centered modal by default, so the examples here force `presentation="bottomSheet"`:
```tsx live
import { useState } from 'react';
import { Pressable, ScrollView, Text, View } from 'react-native';
import { BottomSheet } from '@elik56a/react-native-pickers';
const comments = Array.from(
{ length: 30 },
(_, i) => `Comment #${i + 1}: looks great!`
);
export default function Example() {
const [open, setOpen] = useState(false);
return (
<>
setOpen(true)}
style={{ backgroundColor: '#C8460D', padding: 14, borderRadius: 12 }}
>
Open comments
setOpen(false)}
title="Comments"
presentation="bottomSheet"
draggable
height={0.45}
expandedHeight={0.9}
>
{comments.map((c) => (
{c}
))}
>
);
}
```
The list scrolls first; once it's back at the top, wheeling up expands the sheet, and wheeling down at the bottom collapses it, then closes it.
#### Configuring it
```tsx
// Expand to 85% of the window instead of full screen
// Expand to a fixed height in px
// Mouse dragging on the web too
// No expanding: drag (or wheel) down still closes it
// Fixed at its height: no expanding, no swipe or wheel to close
```
Every picker sheet takes the same props through `sheetProps`:
```tsx
```
### Keyboard
On iOS the sheet is wrapped in a `KeyboardAvoidingView`, so text inputs inside stay visible above the keyboard.
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `visible` | `boolean` | **required** | Show or hide (animated). |
| `onClose` | `() => void` | **required** | Requested close: backdrop, swipe, Android back. |
| `children` | `ReactNode` | **required** | Sheet content. |
| `title` | `ReactNode` | — | Header title; strings get the title style. |
| `headerLeft` | `ReactNode` | — | Left side of the header. |
| `headerRight` | `ReactNode` | — | Right side of the header. |
| `footer` | `ReactNode` | — | Content pinned below the children. |
| `presentation` | `'auto' \| 'bottomSheet' \| 'modal'` | `'auto'` | Bottom sheet or centered modal; `auto` is a modal on wide web windows. |
| `enableSwipeToClose` | `boolean` | `true` | Drag down (wheel down on the web) to dismiss (bottom sheet only). |
| `draggable` | `boolean` | `true` native / `false` web | The header row drags too, not just the handle. |
| `expandable` | `boolean` | `true` | Grow to `expandedHeight`: drag up on native, wheel up on the web. |
| `expandedHeight` | `number` | window − status bar | Expanded height: fraction (≤ 1) or px. |
| `closeOnBackdropPress` | `boolean` | `true` | Tap outside to dismiss. |
| `maxHeightRatio` | `number` | `0.9` | Max height as a fraction of the window. |
| `height` | `number` | content | Fixed height: fraction (≤ 1) or px. |
| `bottomInset` | `number` | `24` iOS / `12` | Bottom padding. |
| `onOpened` | `() => void` | — | Open animation finished. |
| `onClosed` | `() => void` | — | Close animation finished (content unmounted). |
| `styles` | `SlotStyles` | — | Slots below. |
| `theme` | `DeepPartial` | — | Theme override (`sheet`, `backdrop`, `handle`, `radius.sheet`…). |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID on the modal. |
#### Slots
| Slot | Element |
| --- | --- |
| `backdrop` | Dimmed overlay |
| `sheet` | The sheet surface |
| `handle` | Grab handle |
| `header` | Header row |
| `title` | String title text |
| `content` | Children wrapper |
| `footer` | Footer wrapper |
#### Theme tokens used
`colors.sheet` (background), `colors.backdrop`, `colors.handle`, `radius.sheet` (top corners), `animation.duration`.
### Related
- [PickerSheet](./picker-sheets#pickersheet): a BottomSheet with Cancel / Done buttons
- [Filters sheet recipe](../recipes/filters-sheet)
---
## React Native Color Picker
## ColorPicker
`ColorPicker` picks a colour the way each platform does: [iOS's colour picker, Google's swatch circles, or the browser's colour input](#three-layouts). The value is a hex string such as `'#E4572E'`.
It needs no native module, SVG or gradient library: the gradients are drawn as slices of solid colour.
```tsx
import { ColorPicker } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { ColorPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [color, setColor] = useState('#2196F3');
return ;
}
```
### Try the props
### Three layouts
Each [design](../guides/designs) has its own, or pick one with `display`:
| `display` | Default in | What it is |
| --- | --- | --- |
| `'tabs'` | iOS | Like `UIColorPickerViewController`: **Grid** (one block of colours, the chosen one ringed in white), **Spectrum** (every hue across, white to black down) and **Sliders** (red, green and blue with the hex code), then the colour beside your swatches in a row to scroll |
| `'swatches'` | Material | Like Google's apps: circles with a check on the chosen one, and a **Custom** circle at the end that opens a saturation square, a hue slider and the hex code. A colour off the palette opens it already |
| `'spectrum'` | web | Like the browser's colour input: a saturation square, the colour beside a hue slider, the hex code, then your swatches. Where the browser has an eyedropper (Chrome, Edge), a button picks a colour from the screen |
Squares and the spectrum take a tap anywhere, as well as a drag. Screen readers step them across with increment and decrement, and up and down with two named actions.
### The value is a hex code
`value` and `defaultValue` take a hex code with 3 or 6 digits, with or without `#`, in any case. `onChange` always gives `'#RRGGBB'` in capitals, so you can compare it with `===`. Without `value` or `defaultValue`, the picker starts on blue, `#2196F3`.
`onChange` fires on every step while dragging, so the colour can preview live. Use [`ColorPickerSheet`](#in-a-sheet) or [`ColorPickerField`](#in-a-form-field) to commit only on **Done**.
### Opacity
`showAlpha` adds an opacity slider over a checkerboard. Below full opacity, the value gains two hex digits: `'#E4572E80'` is half see-through. Fully opaque colours stay six digits. Without `showAlpha`, a value with opacity is shown and returned opaque.
```tsx live
import { useState } from 'react';
import { ColorPicker } from '@elik56a/react-native-pickers';
export default function Overlay() {
const [color, setColor] = useState('#3F51B580');
return ;
}
```
### Your own swatches
`colors` replaces the palette. Each colour is a hex code, or `{ value, label }` to give it a name for screen readers; a colour without a name is read as its code. They wrap to fill the width, except on iOS, where they sit in one row to scroll. `showCustom={false}` leaves only the swatches, in the design's shape.
```tsx live
import { useState } from 'react';
import { ColorPicker } from '@elik56a/react-native-pickers';
const LABELS = [
{ value: '#D73A4A', label: 'Bug' },
{ value: '#0075CA', label: 'Documentation' },
{ value: '#A2EEEF', label: 'Enhancement' },
{ value: '#7057FF', label: 'Good first issue' },
{ value: '#008672', label: 'Help wanted' },
{ value: '#E4E669', label: 'Invalid' },
{ value: '#D876E3', label: 'Question' },
{ value: '#FFFFFF', label: 'Wontfix' },
];
export default function LabelColor() {
const [color, setColor] = useState('#7057FF');
return ;
}
```
The default palette is exported as `DEFAULT_COLORS`, to add to rather than replace.
### Copy the hex code
The hex field has a copy button in its corner. On the web it uses the browser's clipboard. React Native has no clipboard of its own without a native module, so on iOS and Android the button shows when you pass `onCopy`, for example from [`expo-clipboard`](https://docs.expo.dev/versions/latest/sdk/clipboard/):
```tsx
import * as Clipboard from 'expo-clipboard';
```
`onCopy` also replaces the browser's clipboard on the web, to show your own toast, say.
### Other languages
`labels` renames the tabs, sliders and buttons, which screen readers read too:
```tsx
```
The keys are `grid`, `spectrum`, `sliders`, `hue`, `saturation`, `brightness`, `opacity`, `red`, `green`, `blue`, `hex`, `copy`, `copied`, `eyedropper` and `custom`.
### In a sheet
```tsx
import { ColorPickerSheet } from '@elik56a/react-native-pickers';
setOpen(false)} title="Label colour" value={color} onConfirm={setColor} />
```
It takes the [shared sheet props](./picker-sheets#shared-props) and the picker's, and keeps a draft until **Done**, like the other [picker sheets](./picker-sheets#how-drafts-work).
### In a form field
`ColorPickerField` opens the sheet and shows the confirmed colour: a small swatch and its hex code.
→ [Picker fields](./picker-fields)
### Utilities
| Export | Description |
| --- | --- |
| `normalizeHex(input)` | `'#RRGGBB'` in capitals for a 3- or 6-digit code with or without `#`, `'#RRGGBBAA'` for 4 or 8 digits below full opacity, or `null` |
| `hexToHsv(hex)` | `{ hue, saturation, brightness }`, each 0–1 |
| `hsvToHex(hsv)` | `'#RRGGBB'` |
| `withAlpha(hex, alpha)` | Adds an opacity 0–1: `withAlpha('#E4572E', 0.5)` is `'#E4572E80'` |
| `DEFAULT_COLORS` | The default palette: `{ value, label }[]` |
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | — | Selected colour as hex (controlled). |
| `defaultValue` | `string` | `'#2196F3'` | Initial colour when uncontrolled. |
| `onChange` | `(color: string) => void` | — | Fires with `'#RRGGBB'`, or `'#RRGGBBAA'` below full opacity. |
| `display` | `'tabs' \| 'swatches' \| 'spectrum'` | from the design | iOS's tabs, Google's circles, or the browser's spectrum. |
| `colors` | `(string \| { value, label? })[]` | `DEFAULT_COLORS` | Your swatches. |
| `showCustom` | `boolean` | `true` | Show the grid, spectrum, sliders and hex field. Without them only `colors` show. |
| `showAlpha` | `boolean` | `false` | Add an opacity slider. |
| `onCopy` | `(hex: string) => unknown` | the browser's clipboard on the web | Copy the hex code; shows the copy button. |
| `labels` | `Partial` | English | Names of the tabs, sliders and buttons. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | `container`, `swatch`, `pad`, `slider`, `sliderThumb`, `sliderLabel`, `hexInput` and `preview`. |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. The parts get `${testID}-hex`, `-hue`, `-opacity`, `-pad`, `-spectrum`, `-red`, `-green` and `-blue`. |
`ColorPickerSheet` takes these plus the [sheet props](./picker-sheets#shared-props) and `onConfirm(color)`. `ColorPickerField` takes the sheet's props plus the [field props](./picker-fields#props).
### Related
- [EmojiPicker](./emoji-picker)
- [Picker fields](./picker-fields)
---
## React Native Country Picker
## CountryPicker
`CountryPicker` is a React Native form field for choosing a country: about 250 countries with flag emojis and international dialing codes, searchable by English or native name, ISO code or `+` dialing code. Country names follow the device's language. It's a [`SelectPicker`](./select-picker) with the country list built in, so it takes the same props: a bottom sheet on phones, a dropdown on the web, multiple selection, a clear button and the [iOS, Material or web design](../guides/designs).
No images or data files to add: flags are emoji, and the list is about 11 KB, parsed on first use.
```tsx
import { CountryPicker } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { CountryPicker } from '@elik56a/react-native-pickers';
import type { CountryCode } from '@elik56a/react-native-pickers';
export default function Example() {
const [country, setCountry] = useState('IL');
return ;
}
```
Try searching `deutschland`, `usa`, `+44` or `JP`. The value is the ISO 3166-1 alpha-2 code, such as `'IL'`. [`getCountry`](#utilities) turns it into the name, flag and dialing code.
### Try the props
### Preferred countries and dialing codes
`preferred` pins countries to the top under their own header, in the order you give. `showDialCode` adds each country's code under its name.
```tsx live
import { useState } from 'react';
import { CountryPicker } from '@elik56a/react-native-pickers';
import type { CountryCode } from '@elik56a/react-native-pickers';
export default function Example() {
const [country, setCountry] = useState(null);
return (
);
}
```
### Phone numbers
For a phone number, use [`PhoneField`](./phone-field): this country list behind a flag and calling code button, with the number formatted as it's typed and stored in E.164.
### Several countries
`multiple` turns the field into chips, as with `SelectPicker`. `maxSelected` caps the count.
```tsx live
import { useState } from 'react';
import { CountryPicker } from '@elik56a/react-native-pickers';
import type { CountryCode } from '@elik56a/react-native-pickers';
export default function Example() {
const [visited, setVisited] = useState(['FR', 'JP']);
return ;
}
```
### A subset of countries
`countries` limits the list to the ones you ship to, and `exclude` drops some. Both take ISO codes, and the list is always sorted by name.
```tsx live
import { useState } from 'react';
import { CountryPicker } from '@elik56a/react-native-pickers';
import type { CountryCode } from '@elik56a/react-native-pickers';
const EU_SHIPPING: CountryCode[] = ['AT', 'BE', 'DE', 'ES', 'FR', 'IE', 'IT', 'NL', 'PT'];
export default function Example() {
const [country, setCountry] = useState(null);
return ;
}
```
Pass `countries`, `exclude` and `preferred` from a constant, not a new array on every render, so the list is built once.
### Country names in other languages
Names come from `Intl.DisplayNames` in `locale`, the device locale by default. Search still matches the English and native names in every language. Where the JavaScript engine has no `Intl.DisplayNames`, names fall back to English.
```tsx live
import { useState } from 'react';
import { CountryPicker } from '@elik56a/react-native-pickers';
import type { CountryCode } from '@elik56a/react-native-pickers';
export default function Example() {
const [country, setCountry] = useState('DE');
return (
);
}
```
### Utilities
The country data is exported for your own UIs, such as a flag next to a username or a dialing code in a form.
| Export | Description |
| --- | --- |
| `getCountry(code)` | `{ code, name, nativeName?, dialCode, flag, keywords }` for an ISO code in any case, or `undefined` |
| `getAllCountries()` | Every `Country`, sorted by code |
| `getCountryFlag(code)` | The flag emoji for an ISO code: `'jp'` → 🇯🇵 |
```ts
getCountry('il'); // { code: 'IL', name: 'Israel', nativeName: 'יִשְׂרָאֵל', dialCode: '+972', flag: '🇮🇱', keywords: [] }
```
### Props
Every [`SelectPicker` prop](./select-picker#props) works, apart from the server search ones (`options`, `loadOptions`, `onSearchChange`, `loading` and the rest): the list is built in. `value` is a `CountryCode`, or `CountryCode[]` with `multiple`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `countries` | `CountryCode[]` | all | Only these countries. |
| `exclude` | `CountryCode[]` | — | Leave these out. |
| `preferred` | `CountryCode[]` | — | Shown first, in this order, under their own header. |
| `preferredLabel` | `string` | `'Suggested'` | Header above `preferred`. |
| `allCountriesLabel` | `string` | `'All countries'` | Header above the rest when `preferred` is set. |
| `showDialCode` | `boolean` | `false` | Show each country's dialing code under its name. |
| `locale` | `string` | device locale | Language of the country names. |
| `placeholder` | `string` | `'Select a country'` | Shown while nothing is selected. |
| `searchPlaceholder` | `string` | `'Search countries'` | Search field placeholder. |
| `sheet` | `SheetChromeProps` | `sheetProps.height: 0.8` | Sheet options. The sheet opens at 80% of the screen so it doesn't jump as search narrows the list. |
#### Types
```ts
type CountryCode = 'AD' | 'AE' | 'AF' | … ; // ISO 3166-1 alpha-2
interface Country {
code: CountryCode;
name: string; // English
nativeName?: string; // in the country's own language, when different
dialCode: string; // '+972'
flag: string; // '🇮🇱'
keywords: string[]; // aliases ('USA') and other dialing codes
}
```
Country data comes from [countries-list](https://github.com/annexare/Countries) (MIT).
---
## React Native Date Picker & Calendar
## DatePicker & Calendar
`DatePicker` is a React Native date picker that shows a month calendar. It supports **single**, **range** and **multiple** selection, min / max dates, disabled days, dots and labels under days, swipe navigation, a quick month jump and any locale.
```tsx
import { DatePicker, Calendar } from '@elik56a/react-native-pickers';
```
- **`DatePicker`** is the entry point. With `display="calendar"` (the default) it renders a `Calendar`, and with `display="wheel"` it renders a [`DateWheelPicker`](./date-wheel-picker).
- **`Calendar`** is what `DatePicker` renders by default, with the same props minus `display`. `` and `` look and behave the same; import `Calendar` directly if you never need the wheel.
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { DatePicker, formatDate } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(null);
return (
<>
{date
? formatDate(date, undefined, { dateStyle: 'full' })
: 'No date selected'}
>
);
}
```
This page covers single selection and everything the calendar shares. **Range** and **multiple** selection are on the [Date range & multiple](./date-range-picker) page.
### Try the props
### Min and max dates
Days outside `[minDate, maxDate]` are disabled (struck through). The arrows stop at the first and last allowed months, and the month grid greys out months outside the range. Only the **day** matters; the time of day is ignored.
```tsx live
import { useState } from 'react';
import { DatePicker, addDays } from '@elik56a/react-native-pickers';
export default function Example() {
const today = new Date();
const [date, setDate] = useState(addDays(today, 2));
return (
);
}
```
### Disabling days
`isDateDisabled` runs for every visible day. Return `true` to disable it.
```tsx live
import { useState } from 'react';
import { DatePicker, toDateKey } from '@elik56a/react-native-pickers';
const holidays = new Set(['2026-12-25', '2026-12-26', '2027-01-01']);
export default function Example() {
const [date, setDate] = useState(null);
return (
d.getDay() === 0 || d.getDay() === 6 || holidays.has(toDateKey(d))
}
/>
);
}
```
You can also disable specific days through `markedDates` with `disabled: true`.
:::tip Keep it cheap
`isDateDisabled` is called up to 42 times per render. Avoid heavy work inside it; precompute a `Set` of disabled keys and check it with [`toDateKey`](../api/date-utils#todatekey).
:::
### Marked dates: dots, labels and colours
`markedDates` is keyed by `YYYY-MM-DD` (local time; build keys with [`toDateKey`](../api/date-utils#todatekey)). Each entry can have:
| Field | Type | Effect |
| --- | --- | --- |
| `dots` | `string[]` | Up to 3 coloured dots under the number (events, statuses) |
| `label` | `string` | Small text under the number (prices, counts) |
| `textColor` | `string` | Colour of the day number |
| `disabled` | `boolean` | Disable the day |
```tsx live
import { useState } from 'react';
import { DatePicker, toDateKey, addDays } from '@elik56a/react-native-pickers';
export default function Example() {
const today = new Date();
const [date, setDate] = useState(null);
const marks = {
[toDateKey(addDays(today, 1))]: { dots: ['#22C55E'] },
[toDateKey(addDays(today, 3))]: { dots: ['#22C55E', '#F59E0B', '#EF4444'] },
[toDateKey(addDays(today, 5))]: { label: '$89' },
[toDateKey(addDays(today, 6))]: { label: '$129', textColor: '#EF4444' },
[toDateKey(addDays(today, 8))]: { disabled: true, label: 'Full' },
};
return ;
}
```
### Locale and first day of week
Month and weekday names come from `Intl.DateTimeFormat` for the `locale` you pass. `firstDayOfWeek` (0 = Sunday … 6 = Saturday) defaults to the locale's first day when `Intl.Locale` week info is available, else Sunday.
```tsx live
import { useState } from 'react';
import { View } from 'react-native';
import { DatePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(new Date());
return (
);
}
```
### Navigation
- **Arrows** step one month. They're disabled at `minDate` / `maxDate`.
- **Swipe** left / right on the days to change month (`enableSwipe`, on by default). The direction flips in RTL.
- **Quick jump**: tap the month title to open a grid of months (the arrows step by year), tap the title again for a grid of years (the arrows step by 12 years), and tap it once more to go back to days. Picking a year opens its months, and picking a month opens its days (`enableQuickJump`, on by default). The year grid is limited to `yearRange`, which defaults to the `minDate` / `maxDate` years or 100 years either side of today.
- `initialMonth` sets the first month shown. By default it's the month of the selected date (or range start, or first multiple date), else today.
- `onMonthChange(month)` fires with the first day of the newly shown month.
In single mode, when `value` changes from outside to a date in another month, the calendar follows it.
### Layout
```tsx live
import { useState } from 'react';
import { DatePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(new Date());
return (
month.toLocaleDateString('en-US', { month: 'short', year: '2-digit' })
}
/>
);
}
```
- `fixedWeeks` (default `true`) always renders 6 rows so the height never jumps between months. Set it to `false` to render only the weeks the month needs.
- `showOutsideDays` (default `true`) shows the trailing and leading days of neighbouring months (tappable, dimmed).
- `dayHeight` (default `44`) is the row height; the selection circle is `min(dayHeight − 4, 40)`.
- `showWeekdays={false}` hides the weekday header row.
### Custom days and arrows
`renderDay(state, theme)` fully replaces a day cell's content (taps and the range band are still handled for you). `renderArrow(direction, disabled)` replaces the arrows.
```tsx live
import { useState } from 'react';
import { View, Text } from 'react-native';
import { DatePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(new Date());
return (
(
{dir === 'prev' ? '⬅️' : '➡️'}
)}
renderDay={(s, theme) => (
{s.date.getDate()}
)}
/>
);
}
```
`DayState` passed to `renderDay`:
```ts
interface DayState {
date: Date;
selected: boolean; // the selected day, a range end, or one of multiple
rangeStart: boolean;
rangeEnd: boolean;
inRange: boolean; // strictly between start and end
today: boolean;
disabled: boolean;
outside: boolean; // belongs to the previous / next month
marking?: MarkedDate;
}
```
### Styling
```tsx live
import { useState } from 'react';
import { DatePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(new Date());
return (
);
}
```
#### Slots
| Slot | Element |
| --- | --- |
| `container` | Outer view |
| `header` | Title + arrows row |
| `headerTitle` | Month / year title text |
| `arrow` | Arrow button |
| `arrowText` | Default arrow glyph |
| `weekdays` | Weekday header row |
| `weekdayText` | Weekday labels |
| `week` | Each week row |
| `day` | Day cell (pressable, full width of the column) |
| `dayInner` | The circle inside the cell |
| `dayText` | Day number |
| `selectedDay` / `selectedDayText` | Selected circle / number |
| `todayDay` / `todayDayText` | Today's circle / number |
| `disabledDayText` | Disabled day numbers |
| `outsideDayText` | Days from neighbouring months |
| `rangeBand` | Range highlight band |
| `dot` | Marked-date dots |
| `dayLabel` | Marked-date label text |
| `gridCell` / `gridCellText` | Month and year grid cells (quick jump) |
### Props
`DatePicker` accepts everything below plus `display`. With `display="wheel"`, see [DateWheelPicker](./date-wheel-picker) instead.
#### Selection
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `display` | `'calendar' \| 'wheel'` | `'calendar'` | `DatePicker` only. |
| `mode` | `'single' \| 'range' \| 'multiple' \| 'ranges'` | `'single'` | Selection mode; changes the type of `value`. |
| `value` | `Date \| null` · `DateRange` · `Date[]` · `DateRange[]` | — | Selection (controlled), typed by `mode`. |
| `defaultValue` | same as `value` | `null` · `{start:null,end:null}` · `[]` · `[]` | Initial selection when uncontrolled. |
| `onChange` | `(date: Date) => void` · `(range: DateRange) => void` · `(dates: Date[]) => void` · `(ranges: DateRange[]) => void` | — | Fires on every tap that changes the selection. |
| `minRangeDays` | `number` | — | `range` and `ranges`. See [range](./date-range-picker#props). |
| `maxRangeDays` | `number` | — | `range` and `ranges`. |
| `allowSingleDayRange` | `boolean` | `true` | `range` and `ranges`. |
| `maxSelections` | `number` | — | `multiple` only. |
| `maxRanges` | `number` | — | `ranges` only. |
#### Calendar
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `minDate` | `Date` | — | Earliest selectable day. |
| `maxDate` | `Date` | — | Latest selectable day. |
| `isDateDisabled` | `(date: Date) => boolean` | — | Return `true` to disable a day. |
| `markedDates` | `Record` | — | Keyed by `YYYY-MM-DD`: `{ dots?, label?, textColor?, disabled? }`. |
| `locale` | `string` | device | Month / weekday names and default first day. |
| `firstDayOfWeek` | `number` (0–6) | from locale, else `0` | 0 = Sunday … 6 = Saturday. |
| `initialMonth` | `Date` | selected date or today | Month shown first. |
| `onMonthChange` | `(month: Date) => void` | — | Fires with the first day of the new month. |
| `showOutsideDays` | `boolean` | `true` | Show neighbouring months' days. |
| `fixedWeeks` | `boolean` | `true` | Always 6 rows. |
| `enableSwipe` | `boolean` | `true` | Swipe horizontally to change months. |
| `enableQuickJump` | `boolean` | `true` | Tap the title to cycle days → months → years. |
| `weekdayFormat` | `'short' \| 'narrow'` | from the design: `'narrow'` in Material, else `'short'` | `Mon` vs `M`. |
| `showWeekdays` | `boolean` | `true` | Show the weekday header. |
| `formatTitle` | `(month: Date, locale?: string) => string` | `"October 2026"` | Custom title text. |
| `renderDay` | `(state: DayState, theme: PickerTheme) => ReactNode` | — | Custom day cell content. |
| `renderArrow` | `(direction: 'prev' \| 'next', disabled: boolean) => ReactNode` | — | Custom arrows. |
| `yearRange` | `[number, number]` | min/max or ±100 years | Years available in the year grid. |
| `dayHeight` | `number` | `44` | Height of a day row. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | See [slots](#slots). |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. |
#### Types
```ts
interface DateRange { start: Date | null; end: Date | null }
interface MarkedDate { dots?: string[]; disabled?: boolean; label?: string; textColor?: string }
type CalendarProps = CalendarBaseProps & CalendarSelectionProps;
type DatePickerProps = ({ display?: 'calendar' } & CalendarProps) | ({ display: 'wheel' } & DateWheelPickerProps);
```
### Accessibility
Each day is a button whose label is the full date ("Wednesday, October 14, 2026") with `selected` / `disabled` state. The arrows are labelled "Previous" / "Next", and the title is a button when quick jump is on.
### In a form field
`DatePickerField` is a field that opens a [`DatePickerSheet`](./picker-sheets#datepickersheet) and shows the confirmed date. Switch `presentation` in the panel: `auto` opens a dropdown under the field on the web and a bottom sheet on iOS and Android.
→ [Picker fields](./picker-fields)
### Related
- [Date range & multiple](./date-range-picker)
- [MonthPicker](./month-picker): a month and year without a day
- [WeekStrip](./week-strip): one week of days in a row
- [DatePickerSheet](./picker-sheets#datepickersheet)
- [DatePickerField](./picker-fields): the calendar behind a form field
- [Booking recipe](../recipes/booking-range)
---
## React Native Date Range Picker
## Date range & multiple dates
Pick a check-in / check-out date range, several ranges, or several separate dates, in React Native. `DatePicker` (and `Calendar`) switch selection model with `mode`. TypeScript narrows `value` and `onChange` for each mode.
| `mode` | `value` | `onChange` | Extra props |
| --- | --- | --- | --- |
| `'single'` (default) | `Date \| null` | `(date: Date) => void` | — |
| `'range'` | `DateRange` (`{ start: Date \| null; end: Date \| null }`) | `(range: DateRange) => void` | `minRangeDays`, `maxRangeDays`, `allowSingleDayRange` |
| `'multiple'` | `Date[]` | `(dates: Date[]) => void` | `maxSelections` |
| `'ranges'` | `DateRange[]` | `(ranges: DateRange[]) => void` | `maxRanges`, `minRangeDays`, `maxRangeDays`, `allowSingleDayRange` |
The modes are also exported as constants: `mode={CALENDAR_MODE.ranges}`.
### Try the props
### Range
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { DatePicker, formatDate } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
export default function Example() {
const [range, setRange] = useState({ start: null, end: null });
const nights =
range.start && range.end
? Math.round((+range.end - +range.start) / 864e5)
: 0;
return (
<>
{range.start ? formatDate(range.start) : 'Check-in'} →{' '}
{range.end ? formatDate(range.end) : 'Check-out'}
{nights ? ` · ${nights} night${nights > 1 ? 's' : ''}` : ''}
>
);
}
```
#### How taps work
1. **First tap** sets the start: `onChange({ start: day, end: null })`.
2. **Second tap** on or after the start sets the end: `onChange({ start, end: day })`.
3. A tap **before** the start, or any tap **after a complete range**, starts a new range from that day.
The band between start and end uses `primaryMuted`; the ends use `primary`.
#### Ranges never cross disabled days
If the days between start and the tapped day include a disabled one (from `minDate`/`maxDate`, `isDateDisabled` or `markedDates`), the tap starts a **new range** instead of producing a range that spans an unavailable night. That's exactly what booking flows need:
```tsx live
import { useState } from 'react';
import { DatePicker, toDateKey, addDays } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
export default function Example() {
const today = new Date();
const booked = new Set([
toDateKey(addDays(today, 4)),
toDateKey(addDays(today, 5)),
toDateKey(addDays(today, 12)),
]);
const [range, setRange] = useState({ start: null, end: null });
return (
booked.has(toDateKey(d))}
/>
);
}
```
#### Minimum and maximum length
Length is measured in **days between start and end** (nights): a range from the 10th to the 13th is 3.
| Prop | Effect when violated |
| --- | --- |
| `minRangeDays` | A second tap that would be too short is **ignored**; the start stays selected |
| `maxRangeDays` | A second tap that would be too long **starts a new range** from that day |
| `allowSingleDayRange={false}` | Tapping the start again is ignored (by default it makes a one-day range, `start === end`) |
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { DatePicker } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
export default function Example() {
const [range, setRange] = useState({ start: null, end: null });
return (
<>
Stay 2 – 7 nights
>
);
}
```
#### Range styling
```tsx live
import { useState } from 'react';
import { DatePicker, addDays } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
export default function Example() {
const t = new Date();
const [range, setRange] = useState({
start: addDays(t, 2),
end: addDays(t, 6),
});
return (
);
}
```
### Multiple
Tap days to toggle them. `onChange` always receives the dates **sorted ascending**. With `maxSelections`, further taps on unselected days are ignored once the limit is reached (deselecting still works).
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { DatePicker, formatDate } from '@elik56a/react-native-pickers';
export default function Example() {
const [dates, setDates] = useState([]);
return (
<>
{dates.length
? dates
.map((d) =>
formatDate(d, undefined, { day: 'numeric', month: 'short' })
)
.join(', ')
: 'Pick up to 5 days'}
>
);
}
```
### Multiple ranges
`mode="ranges"` picks several date ranges, e.g. blackout periods or trip legs. Each range is built with two taps, just like range mode. Tapping any day of a finished range removes it. Ranges never overlap, and `onChange` receives them **sorted by start**. The range rules (`minRangeDays`, `maxRangeDays`, `allowSingleDayRange`) apply to each range. With `maxRanges`, taps that would start another range are ignored once the limit is reached.
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { DatePicker, formatDate } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
export default function Example() {
const [ranges, setRanges] = useState([]);
const short = (d: Date | null) =>
d ? formatDate(d, undefined, { day: 'numeric', month: 'short' }) : '…';
return (
<>
{ranges.length
? ranges.map((r) => `${short(r.start)} – ${short(r.end)}`).join(', ')
: 'Pick up to 3 ranges'}
>
);
}
```
### Uncontrolled
```tsx
console.log(r)} />
console.log(d)} />
```
### In a sheet
`DatePickerSheet` disables **Done** until the range is complete (both `start` and `end`). In ranges mode, it waits until no range is half-picked.
```tsx
setOpen(false)}
title="Your stay"
mode="range"
value={range}
onConfirm={setRange}
minDate={new Date()}
minRangeDays={1}
/>
```
→ [DatePickerSheet](./picker-sheets#datepickersheet)
### In a form field
`DatePickerField` takes the same props without `visible`, and shows the confirmed range (`Oct 5, 2026 – Oct 9, 2026`) or dates in the field. Switch `presentation` in the panel: `auto` opens a dropdown under the field on the web and a bottom sheet on iOS and Android.
→ [Picker fields](./picker-fields)
### Props
All [calendar props](./date-picker#calendar) apply. Mode-specific props:
| Prop | Mode | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `minRangeDays` | `range`, `ranges` | `number` | — | Minimum nights (end − start in days). Shorter second taps are ignored. |
| `maxRangeDays` | `range`, `ranges` | `number` | — | Maximum nights. Longer second taps start a new range. |
| `allowSingleDayRange` | `range`, `ranges` | `boolean` | `true` | Allow `start === end`. |
| `maxSelections` | `multiple` | `number` | — | Maximum number of selected days. |
| `maxRanges` | `ranges` | `number` | — | Maximum number of ranges. |
### Working with ranges
The [date utilities](../api/date-utils) make common range checks one-liners:
```ts
import { isBetweenDays, isSameDay, compareDay, addDays } from '@elik56a/react-native-pickers';
const nights = Math.round((range.end!.getTime() - range.start!.getTime()) / 86_400_000);
const includesToday = isSameDay(range.start, new Date()) || isBetweenDays(new Date(), range.start, range.end);
const isFuture = compareDay(range.start!, new Date()) > 0;
```
---
## React Native Date Time Picker
## DateTimePicker
`DateTimePicker` is a React Native date and time picker: pick a day and a time in one control. It has two displays, and each [design](../guides/designs) has its own default:
- **`wheel`** (the iOS default): one row of wheels like the iOS date-and-time picker. The day wheel shows `Today`, `Tue, Oct 27`… next to the hour, minute and AM/PM wheels.
- **`calendar`** (the Material and web default): a month grid and the time: hour and minute fields to type into under it in Material, wheels under it on iOS, and on the web columns to click beside it. `timePosition="below"` puts the time under the calendar instead, and `"beside"` puts it next to the calendar in any design when there's room for both. `timeDisplay` picks how the time is chosen in any design: `'wheel'`, `'dial'`, `'input'` or `'list'`.
```tsx
import { DateTimePicker } from '@elik56a/react-native-pickers';
```
```tsx live wide
import { useState } from 'react';
import { Text } from 'react-native';
import { DateTimePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(new Date());
return (
<>
{date.toLocaleString()}
>
);
}
```
### Try the props
### Calendar display
```tsx live wide
import { useState } from 'react';
import { DateTimePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(new Date());
return (
);
}
```
Pass Calendar options (`markedDates`, `isDateDisabled`, `firstDayOfWeek`…) through `calendarProps`. The time wheels show 3 rows here to keep the control short; change that with `visibleItems`.
### Min and max
`minDate` and `maxDate` are **moments**, so their time counts too. The day wheel lists only the days between them. On the first day, times before `minDate` are disabled; on the last day, times after `maxDate` are disabled. A change that lands outside the range is pulled back to the nearest bound.
```tsx live wide
import { useState } from 'react';
import { Text } from 'react-native';
import { DateTimePicker, addDays } from '@elik56a/react-native-pickers';
export default function Example() {
const now = new Date();
const [date, setDate] = useState(now);
return (
<>
From now, up to 2 weeks ahead
>
);
}
```
Without `minDate` / `maxDate`, the day wheel lists a year either side of today. If `value` is outside that span, the wheel stretches to include it. For dates further away, such as a birthday, pass `minDate` / `maxDate` or use [`DatePicker`](./date-picker).
### Day labels
Today reads `Today` (from `Intl.RelativeTimeFormat`, so it's translated); other days use the locale's short weekday, month and day. Write your own labels with `formatDay`:
```tsx
d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })} />
```
### In a sheet
```tsx
import { DateTimePickerSheet } from '@elik56a/react-native-pickers';
setOpen(false)} title="Reminder" value={date} onConfirm={setDate} minDate={new Date()} />
```
It takes the same props as `DateTimePicker`, plus the [sheet props](./picker-sheets). The picked value is only reported when the user taps **Done**.
### In a form field
`DateTimePickerField` opens the same sheet from a field, and shows the confirmed moment (`Oct 26, 2026, 9:30 AM`). Switch `presentation` in the panel: `auto` opens a dropdown under the field on the web and a bottom sheet on iOS and Android.
→ [Picker fields](./picker-fields)
### Props
All [TimePicker](./time-picker#props) wheel options apply too: `format`, `minuteInterval`, `showSeconds`, `secondInterval`, `loop`, `padHours`, `dayPeriodLabels`, `dayPeriodFirst`, `separator`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Date` | — | Selected date and time (controlled). |
| `defaultValue` | `Date` | now | Initial value when uncontrolled. |
| `onChange` | `(date: Date) => void` | — | Fires when a wheel settles or a day is tapped. Receives a **new** `Date`. |
| `onValueChanging` | `(date: Date) => void` | — | Fires on every tick while scrolling (haptics). |
| `display` | `'wheel' \| 'calendar'` | `'wheel'` on iOS, `'calendar'` in Material and on the web | One row of wheels, or a calendar with the time. |
| `timePosition` | `'beside' \| 'below'` | `'beside'` on the web, `'below'` in iOS and Material | Calendar display: the time beside the calendar or under it. Beside moves under the calendar when there is no room for both. |
| `timeDisplay` | `'wheel' \| 'dial' \| 'input' \| 'list'` | wheels in iOS, `'input'` in Material, `'list'` on the web | Calendar display: how the time is picked. `'dial'` and `'input'` fall back to wheels with `showSeconds`. |
| `minDate` | `Date` | — | Earliest selectable moment. |
| `maxDate` | `Date` | — | Latest selectable moment. |
| `locale` | `string` | device | BCP 47 locale for day labels, clock format and the calendar. |
| `formatDay` | `(date: Date, locale?: string) => string` | `Today`, `Tue, Oct 27` | Day wheel labels (wheel display). |
| `calendarProps` | `Partial` | — | Passed to the [Calendar](./date-picker#props) (calendar display). |
| `itemHeight` | `number` | `40` | Row height. |
| `visibleItems` | `number` | `5` (wheel); with the calendar `3` wheels or `5` web rows under it, `9` beside it | Visible rows. |
| `variant` | `'3d' \| 'flat'` | `'3d'` | Wheel style. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | [MultiWheelPicker slots](./multi-wheel-picker#slots) of the wheels. |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. The wheels get `${testID}-day`, `-hour`, `-minute`… (in the calendar display: `-calendar`, `-time-hour`…). |
---
## React Native Date Wheel Picker
## DateWheelPicker
`DateWheelPicker` is a React Native date wheel picker with day, month and year wheels, like the classic iOS date picker. Best for dates far from today (birthdays, expiry dates) where a calendar would need many taps.
```tsx
import { DateWheelPicker } from '@elik56a/react-native-pickers';
// or:
```
```tsx live noplatforms
import { useState } from 'react';
import { Text } from 'react-native';
import { DateWheelPicker, formatDate } from '@elik56a/react-native-pickers';
export default function Example() {
const [birthday, setBirthday] = useState(new Date(1990, 4, 17));
return (
<>
{formatDate(birthday, undefined, { dateStyle: 'long' })}
>
);
}
```
`` renders exactly this component with the same props.
### Try the props
### Column order from the locale
The order of the wheels follows the locale's date format: month / day / year for `en-US`, day / month / year for `en-GB`, `de`, `he`, `fr`, and year / month / day for `ja`, `zh`, `ko`. Override it with `order`.
```tsx live noplatforms
import { useState } from 'react';
import { View, Text } from 'react-native';
import { DateWheelPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [d, setD] = useState(new Date(2000, 0, 31));
return (
en-GBja-JPCustom order, numeric months
);
}
```
### Month format
| `monthFormat` | Example |
| --- | --- |
| `'long'` (default) | January |
| `'short'` | Jan |
| `'numeric'` | 01 (days are zero-padded too) |
### Day clamping
When the month or year changes to one with fewer days, the day is clamped automatically: Jan 31 → February becomes Feb 28 (or 29 in leap years). The day wheel always shows exactly the days in the selected month.
### Min, max and year range
`minDate` and `maxDate` limit the selection; days and months outside the range are disabled and the value is clamped into the range. The year wheel spans `minDate`'s year to `maxDate`'s year. Without them it uses `yearRange` (default: 100 years before and after today).
```tsx live noplatforms
import { useState } from 'react';
import { DateWheelPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [expiry, setExpiry] = useState(new Date(2028, 5, 1));
return (
);
}
```
:::tip Month + year only
Pass only the columns you need in `order`, for example `['month', 'year']` for card expiry dates. The day is kept from `value`, and clamped if needed.
:::
### Looping
The day and month wheels loop by default (`loop`); the year wheel never loops.
### In a form field
`DatePickerField` with `display="wheel"` opens these wheels in a sheet from a field, and shows the confirmed date. Switch `presentation` in the panel: `auto` opens a dropdown under the field on the web and a bottom sheet on iOS and Android.
→ [Picker fields](./picker-fields)
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Date` | — | Selected date (controlled). Time of day is preserved. |
| `defaultValue` | `Date` | today | Initial date when uncontrolled (clamped to min/max). |
| `onChange` | `(date: Date) => void` | — | Fires when a wheel settles. |
| `onValueChanging` | `(date: Date) => void` | — | Fires on every tick (haptics). |
| `locale` | `string` | device | Month names and default column order. |
| `minDate` | `Date` | — | Earliest selectable date. |
| `maxDate` | `Date` | — | Latest selectable date. |
| `order` | `('day' \| 'month' \| 'year')[]` | from locale | Columns and their order. |
| `monthFormat` | `'long' \| 'short' \| 'numeric'` | `'long'` | Month labels. |
| `yearRange` | `[number, number]` | today ±100 | Years when no min/max is set. |
| `loop` | `boolean` | `true` | Loop day and month wheels. |
| `itemHeight` | `number` | `40` | Row height. |
| `visibleItems` | `number` | `5` | Visible rows. |
| `variant` | `'3d' \| 'flat'` | `'3d'` | Wheel style. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | [MultiWheelPicker slots](./multi-wheel-picker#slots). |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. |
```ts
type DatePart = 'day' | 'month' | 'year';
```
### Related
- [Birthday recipe](../recipes/birthday)
- [DatePickerSheet with `display="wheel"`](./picker-sheets#datepickersheet)
- [DatePickerField with `display="wheel"`](./picker-fields): the wheel behind a form field
---
## React Native Duration Picker
## DurationPicker
`DurationPicker` picks a **length of time**, such as a timer, a workout, a cooking time or a reminder: hours, minutes and optionally seconds, each with its unit beside it. It looks native on each platform: [wheels like the iOS countdown timer, number boxes to type into in Material, columns to click on the web](#wheels-boxes-or-columns), or the one you pick with `display`. The value is a number of seconds.
For a **time of day** (an alarm at 7:30 AM), use [`TimePicker`](./time-picker). A duration has no AM/PM and no locale clock, its hours don't wrap at midnight, and its limits are lengths, not times.
```tsx
import { DurationPicker } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { DurationPicker, formatDuration } from '@elik56a/react-native-pickers';
export default function Example() {
const [seconds, setSeconds] = useState(25 * 60);
return (
<>
{seconds} s = {formatDuration(seconds)}
>
);
}
```
### Try the props
### The value is in seconds
`value`, `defaultValue` and `onChange` use a number of seconds: `5400` is 1 h 30 min. Store it as is, or turn it into text with [`formatDuration`](#formatduration). Without `value` or `defaultValue`, the picker starts at `0`.
### Wheels, boxes or columns
Each [design](../guides/designs) has its own default:
| `display` | Default in | What it is |
| --- | --- | --- |
| `'wheel'` | iOS | Hours, minutes and seconds wheels with their units at the selection band, like the iOS countdown timer |
| `'input'` | Material | A box per unit to type the number into, as in Material's time input. A number that isn't one of the steps yet (`3` on the way to `30` with `minuteInterval={15}`) waits for the next digit |
| `'list'` | web | A column of cells per unit under its label, the chosen one filled, as in `TimePicker` on the web |
```tsx
```
### Hours, minutes and seconds
Hours and minutes show by default. `showSeconds` adds a seconds wheel, and `showHours={false}` drops the hours, so the minutes count past 59 (up to 90 for a 90-minute run):
```tsx live
import { useState } from 'react';
import { DurationPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [rest, setRest] = useState(90);
return ;
}
```
### Min, max and intervals
`min` and `max` are in seconds. Values outside them are dimmed, and a combination that lands outside (such as 2 h with 45 min while the max is 2 h) is pulled back to the nearest limit. Without `max`, the hours go up to 23, or the minutes up to 59 without hours.
`minuteInterval` and `secondInterval` show every n-th minute or second. A value between steps shows rounded **down**, and `DurationPickerSheet` confirms what the wheels show.
```tsx live
import { useState } from 'react';
import { DurationPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [workout, setWorkout] = useState(45 * 60);
return ;
}
```
### Unit labels and other languages
`labels` sets the unit beside each wheel, box or column and in the field text. Screen readers read each one as "Hours", "Minutes" or "Seconds", followed by its value.
```tsx live
import { useState } from 'react';
import { DurationPicker } from '@elik56a/react-native-pickers';
const GERMAN = { hours: 'Std.', minutes: 'Min.', seconds: 'Sek.' };
export default function Example() {
const [seconds, setSeconds] = useState(3600);
return ;
}
```
### `formatDuration`
Turns seconds into the field's text: the units that aren't zero, exact to the second.
```ts
formatDuration(5400); // '1 h 30 min'
formatDuration(95); // '1 min 35 s'
formatDuration(5400, { showHours: false }); // '90 min'
formatDuration(0); // '0 min'
```
It takes `showHours` and `labels`, like the picker.
### In a sheet
```tsx
import { DurationPickerSheet } from '@elik56a/react-native-pickers';
setOpen(false)} title="Timer" value={seconds} onConfirm={setSeconds} />
```
It takes the [shared sheet props](./picker-sheets#shared-props) and the picker's, and keeps a draft until **Done**, like the other [picker sheets](./picker-sheets#how-drafts-work).
### In a form field
`DurationPickerField` opens the sheet and shows the confirmed duration, like `1 h 30 min`. Switch `presentation` in the panel: `auto` opens a dropdown under the field on the web and a bottom sheet on iOS and Android.
→ [Picker fields](./picker-fields)
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | — | Selected duration in seconds (controlled). |
| `defaultValue` | `number` | `0` | Initial duration when uncontrolled. |
| `onChange` | `(seconds: number) => void` | — | Fires when any wheel settles. |
| `onValueChanging` | `(seconds: number) => void` | — | Fires on every tick while scrolling (haptics). |
| `display` | `'wheel' \| 'input' \| 'list'` | from the design | Wheels (iOS), boxes to type into (Material), or columns to click (web). |
| `showHours` | `boolean` | `true` | Show the hours wheel. Without it, minutes count past 59. |
| `showSeconds` | `boolean` | `false` | Add a seconds wheel. |
| `min` | `number` | `0` | Shortest selectable duration, in seconds. |
| `max` | `number` | 23 h 59 min 59 s, or 59 min 59 s without hours | Longest selectable duration, in seconds. |
| `minuteInterval` | `number` | `1` | Minute step: 1, 5, 15… |
| `secondInterval` | `number` | `1` | Second step. |
| `labels` | `{ hours?, minutes?, seconds? }` | `{ hours: 'h', minutes: 'min', seconds: 's' }` | Units beside the wheels and in the field. |
| `loop` | `boolean` | `true` | Loop the minute and second wheels. Hours never loop. |
| `itemHeight` | `number` | `40` | Wheel row height. |
| `visibleItems` | `number` | `5` | Visible wheel rows, or cells in a column. |
| `variant` | `'3d' \| 'flat'` | `'3d'` | Wheel style. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | [MultiWheelPicker slots](./multi-wheel-picker#slots), for the wheels. The `separator` slot styles their unit labels. |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. Each wheel, box or column gets `${testID}-hours`, `-minutes` and `-seconds`. |
`DurationPickerSheet` takes these plus the [sheet props](./picker-sheets#shared-props) and `onConfirm(seconds)`. `DurationPickerField` takes the sheet's props plus the [field props](./picker-fields#props).
### Related
- [TimePicker](./time-picker): for a time of day
- [Duration & timer recipe](../recipes/duration)
- [MultiWheelPicker](./multi-wheel-picker): build other unit pickers with the column `suffix`
---
## React Native Emoji Picker
## EmojiPicker
`EmojiPicker` is a full React Native emoji keyboard: 1,923 emojis in 9 categories, keyword search, skin tones, recently used, a category bar, and filtering by Emoji version so older devices never show empty boxes. Emojis render with the system font; no images or fonts are bundled.
```tsx
import { EmojiPicker } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { EmojiPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [text, setText] = useState('Hello ');
return (
<>
{text}
setText((t) => t + emoji)} />
>
);
}
```
### Try the props
### `onSelect`
```ts
onSelect: (emoji: string, data: Emoji) => void
```
`emoji` is the character to insert, **with the current skin tone applied**. `data` is the base emoji's metadata:
```ts
interface Emoji {
emoji: string; // base character, e.g. '👍'
name: string; // 'thumbs up'
keywords: string[]; // search tags
category: EmojiCategory;
version: number; // Emoji version it was introduced in, e.g. 13.1
skins?: string[]; // 5 skin-tone variants (light → dark) when supported
}
```
### Categories
```ts
type EmojiCategory = 'smileys' | 'people' | 'animals' | 'food' | 'travel' | 'activities' | 'objects' | 'symbols' | 'flags';
```
`categories` filters **and** orders the sections and the category bar:
```tsx live
import { EmojiPicker } from '@elik56a/react-native-pickers';
export default function Example() {
return (
{}}
showSkinTones={false}
/>
);
}
```
Hide specific characters with `exclude={['🖕', '🍆']}`.
### Search
The search field matches names and keywords, ranks name-prefix matches first, and requires every word to match (`cat face` → 🐱). Search across a custom list in your own UI with [`searchEmojis`](../api/emoji-utils#searchemojis).
### Skin tones
The ✋ button next to search opens a tone menu. Tones are numbers:
```ts
type SkinTone = 0 | 1 | 2 | 3 | 4 | 5; // 0 = default yellow, 1–5 = light → dark
```
Tones apply to every emoji that supports them, in the grid and in what `onSelect` returns. Control or persist the tone:
```tsx
const [tone, setTone] = useState(0);
```
Use `defaultSkinTone` for uncontrolled, or `showSkinTones={false}` to hide the button.
### Recently used
Picked emojis go to a **Recently used** section at the top (up to `maxRecent`, default 24, most recent first). The list stores **base** emojis, so they re-render in whatever tone is current.
In `EmojiPickerSheet` and `EmojiPickerField`, the sheet keeps them, so they last between openings. A pick that closes the sheet shows up under **Recently used** the next time it opens, not while it closes, so the grid doesn't jump as it fades or slides away, as on the iOS emoji keyboard.
The picker keeps recents in memory by default. To persist them across launches, control them:
```tsx title="Persisting recents with AsyncStorage"
import AsyncStorage from '@react-native-async-storage/async-storage';
function Picker({ onPick }: { onPick: (e: string) => void }) {
const [recent, setRecent] = useState([]);
useEffect(() => {
AsyncStorage.getItem('emoji-recent').then((v) => v && setRecent(JSON.parse(v)));
}, []);
return (
{
setRecent(next);
AsyncStorage.setItem('emoji-recent', JSON.stringify(next));
}}
onSelect={onPick}
/>
);
}
```
Seed a starter set with `defaultRecent={['👍', '❤️', '😂']}`, or turn the section off with `showRecent={false}`.
### Older devices: `maxEmojiVersion`
Emojis newer than the device's font render as empty boxes (common on older Android). Hide them by version:
```tsx
import { Platform } from 'react-native';
```
### Designs
Like every picker, it follows the [design](../guides/designs) of the platform it runs on, after that platform's own emoji picker:
| | `ios` | `material` | `web` |
| --- | --- | --- | --- |
| Categories | at the bottom, like the iOS emoji keyboard | at the bottom, like Gboard | on top |
| Current category | a gray rounded fill | a tinted pill | an underline |
| Search | a short gray search bar | a tall pill | an outlined input with a focus ring |
| Section titles | UPPERCASE | Sentence case | Sentence case |
Use the iOS / Android / Web switch on any example above to compare. `categoryBarPosition="top"` or `"bottom"` puts the bar in one place on every platform.
### Layout
```tsx live
import { EmojiPicker } from '@elik56a/react-native-pickers';
export default function Example() {
return (
{}}
/>
);
}
```
- `columns` (default 8) sets emojis per row. Cells are square and fill the width.
- `emojiSize` sets the font size (default about 62% of the cell).
- `height` (default 380) is the total height including search and category bar.
- `categoryBarPosition`: `'auto'` (default) puts the bar where the [design](#designs) does, at the bottom on iOS and Android and on top on the web; `'top'` or `'bottom'` fixes it. Hide the bar with `showCategoryBar={false}`.
- `showSectionHeaders={false}` removes the "Smileys & Emotion" style headers.
### Translating labels and icons
Every label is replaceable. The `categoryLabels` keys are the categories plus `recent` and `search`. Icons can be strings (rendered as text) or any React node:
```tsx live
import { EmojiPicker } from '@elik56a/react-native-pickers';
export default function Example() {
return (
{}}
/>
);
}
```
### Custom emoji cells
`renderEmoji(emoji, data, size)` replaces the character, for example to render a custom image set like Twemoji:
```tsx
}
onSelect={insert}
/>
```
### In a sheet
```tsx
setOpen(false)} onSelect={(e) => setText((t) => t + e)} />
```
The sheet closes after each pick by default (`closeOnSelect={false}` to keep it open). → [EmojiPickerSheet](./picker-sheets#emojipickersheet)
### In a form field
`EmojiPickerField` opens the sheet from a field and shows `value`, the emoji you keep in state. Use `renderTrigger` for an icon button instead of a field. Switch `presentation` in the panel: `auto` opens a dropdown under the field on the web and a bottom sheet on iOS and Android.
→ [Picker fields](./picker-fields#custom-trigger)
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onSelect` | `(emoji: string, data: Emoji) => void` | **required** | Called with the toned character and its metadata. |
| `columns` | `number` | `8` | Emojis per row. |
| `emojiSize` | `number` | ~62% of cell | Emoji font size. |
| `height` | `number` | `380` | Total height. |
| `categories` | `EmojiCategory[]` | all 9 | Which categories to show, in order. |
| `exclude` | `string[]` | — | Characters to hide. |
| `maxEmojiVersion` | `number` | — | Hide emojis newer than this Emoji version. |
| `searchable` | `boolean` | `true` | Show the search field. |
| `searchPlaceholder` | `string` | `'Search emoji'` | Search hint. |
| `emptyText` | `string` | `'No emoji found'` | Empty search result text. |
| `showCategoryBar` | `boolean` | `true` | Show the category tabs. |
| `categoryBarPosition` | `'auto' \| 'top' \| 'bottom'` | `'auto'` | Tab bar position. `auto`: bottom in the iOS and Material designs, top on the web. |
| `showSectionHeaders` | `boolean` | `true` | Section title rows. |
| `categoryLabels` | `Partial>` | English | Section and tab labels. |
| `categoryIcons` | `Partial>` | emoji icons | Tab icons. |
| `skinTone` | `SkinTone` | — | Controlled tone. |
| `defaultSkinTone` | `SkinTone` | `0` | Initial tone. |
| `onSkinToneChange` | `(tone: SkinTone) => void` | — | Tone picked in the menu. |
| `showSkinTones` | `boolean` | `true` | Show the tone button. |
| `recent` | `string[]` | — | Controlled recents (base characters). |
| `defaultRecent` | `string[]` | `[]` | Initial recents. |
| `onRecentChange` | `(recent: string[]) => void` | — | New recents after each pick. |
| `showRecent` | `boolean` | `true` | Show and update recents. |
| `maxRecent` | `number` | `24` | Recents kept. |
| `renderEmoji` | `(emoji: string, data: Emoji, size: number) => ReactNode` | — | Custom cell content. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | Slots below. |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. |
#### Slots
| Slot | Element |
| --- | --- |
| `container` | Outer view |
| `header` | Search + tone row |
| `searchInput` | Search `TextInput` |
| `skinToneButton` / `skinToneMenu` | Tone button / menu |
| `categoryBar` | Tab bar |
| `categoryTab` / `activeCategoryTab` | Tabs |
| `categoryIndicator` | Active-tab underline (web design; iOS and Material mark the tab itself) |
| `sectionHeader` / `sectionHeaderText` | Section titles |
| `emojiCell` / `emojiText` | Grid cells |
| `empty` / `emptyText` | No search results |
### Data and utilities
The emoji data comes from [emojibase](https://emojibase.dev) (MIT) and is parsed lazily on first use. The helpers are exported for your own UIs (reactions bars, `:shortcode` autocomplete): `getAllEmojis`, `searchEmojis`, `findEmoji`, `withSkinTone`, `EMOJI_CATEGORIES`, `DEFAULT_CATEGORY_LABELS`, `DEFAULT_CATEGORY_ICONS`. → [Emoji utilities](../api/emoji-utils)
### Related
- [Chat input recipe](../recipes/chat-emoji)
- [EmojiPickerField](./picker-fields): the picker behind a form field
---
## React Native Horizontal List
## HorizontalList
`HorizontalList` is a row of choices that scrolls sideways: a segmented control by default, or a row of chips. When the user taps the item at the edge and more items are hidden past it, the list scrolls a little so the next item comes into view.
```tsx
import { HorizontalList } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { HorizontalList } from '@elik56a/react-native-pickers';
const views = [
{ label: 'Day', value: 'day' },
{ label: 'Week', value: 'week' },
{ label: 'Month', value: 'month' },
];
export default function Example() {
const [view, setView] = useState('week');
return ;
}
```
### Segmented controls
A few items share the track's width. With more than fit, the track scrolls, and the thumb follows the selection along it.
```tsx live
import { useState } from 'react';
import { View } from 'react-native';
import { HorizontalList } from '@elik56a/react-native-pickers';
const sorts = ['Popular', 'Newest', 'Price'].map((s) => ({ label: s, value: s }));
const ranges = ['1D', '1W', '1M', '3M', '6M', 'YTD', '1Y', '2Y', '5Y', '10Y', 'All'].map((r) => ({ label: r, value: r }));
export default function Example() {
const [sort, setSort] = useState('Popular');
const [range, setRange] = useState('1M');
return (
);
}
```
### Chips
`variant="chips"` shows separate pills. With more items than fit, tap the last visible one: the list scrolls to show the next.
```tsx live
import { useState } from 'react';
import { HorizontalList } from '@elik56a/react-native-pickers';
const cuisines = [
'All', 'Italian', 'Japanese', 'Mexican', 'Indian', 'Thai', 'Chinese', 'Korean', 'Vietnamese', 'Greek',
'Lebanese', 'Turkish', 'French', 'Spanish', 'Ethiopian', 'Peruvian', 'Brazilian', 'American', 'Vegan', 'Seafood',
].map((c) => ({ label: c, value: c }));
export default function Example() {
const [cuisine, setCuisine] = useState('All');
return ;
}
```
### Scroll arrows
`showArrows` adds round ‹ › buttons that scroll the list about a page. They're off by default: swiping already scrolls on touch screens, so arrows help most with a mouse on the web, or a long row of chips.
By default (`arrowPosition="outside"`) the arrows sit beside the list and show while its items overflow; the one that can't scroll further is dimmed, so the list keeps its width.
```tsx live
import { useState } from 'react';
import { HorizontalList } from '@elik56a/react-native-pickers';
const countries = [
['🇺🇸', 'United States'], ['🇨🇦', 'Canada'], ['🇲🇽', 'Mexico'], ['🇧🇷', 'Brazil'], ['🇦🇷', 'Argentina'],
['🇬🇧', 'United Kingdom'], ['🇮🇪', 'Ireland'], ['🇫🇷', 'France'], ['🇩🇪', 'Germany'], ['🇪🇸', 'Spain'],
['🇵🇹', 'Portugal'], ['🇮🇹', 'Italy'], ['🇳🇱', 'Netherlands'], ['🇸🇪', 'Sweden'], ['🇳🇴', 'Norway'],
['🇬🇷', 'Greece'], ['🇹🇷', 'Turkey'], ['🇮🇱', 'Israel'], ['🇪🇬', 'Egypt'], ['🇿🇦', 'South Africa'],
['🇮🇳', 'India'], ['🇹🇭', 'Thailand'], ['🇻🇳', 'Vietnam'], ['🇯🇵', 'Japan'], ['🇰🇷', 'South Korea'],
['🇦🇺', 'Australia'], ['🇳🇿', 'New Zealand'],
].map(([flag, name]) => ({ label: name, value: name, icon: flag }));
export default function Example() {
const [country, setCountry] = useState('Japan');
return (
);
}
```
#### Over the edges
`arrowPosition="overlay"` floats the arrows over the list's edges instead, each shown only while items are hidden past it, so the list keeps its full width.
```tsx live
import { useState } from 'react';
import { HorizontalList } from '@elik56a/react-native-pickers';
const topics = [
'For you', 'Following', 'Design', 'Engineering', 'React Native', 'TypeScript', 'Product', 'Startups', 'AI',
'Open source', 'Accessibility', 'Performance', 'Testing', 'Animation', 'Career', 'Remote work', 'Writing',
].map((t) => ({ label: t, value: t }));
export default function Example() {
const [topic, setTopic] = useState('For you');
return (
);
}
```
#### Custom arrows
`renderArrow(side, disabled)` replaces the ‹ › glyph, and the `arrow` slot styles the button around it.
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { HorizontalList, usePickerTheme } from '@elik56a/react-native-pickers';
const genres = [
'Pop', 'Rock', 'Hip-hop', 'Jazz', 'Classical', 'Electronic', 'R&B', 'Country', 'Reggae', 'Metal', 'Folk', 'Blues', 'Latin', 'K-pop',
].map((g) => ({ label: g, value: g }));
export default function Example() {
const theme = usePickerTheme();
const [genre, setGenre] = useState('Jazz');
return (
(
{side === 'left' ? '←' : '→'}
)}
styles={{ arrow: { backgroundColor: theme.colors.primaryMuted, borderWidth: 0, boxShadow: 'none' } }}
accessibilityLabel="Genre"
/>
);
}
```
### Icons
Give an item an `icon` and choose its side with `iconPosition`: `'left'` (the default), `'right'`, `'top'` or `'bottom'`. `'left'` and `'right'` follow the layout direction, so they swap in RTL. An icon can also be a function of `{ selected, disabled, color }`, to color it like its label.
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { HorizontalList } from '@elik56a/react-native-pickers';
const modes = [
{ label: 'Walk', value: 'walk', icon: '🚶' },
{ label: 'Bike', value: 'bike', icon: '🚲' },
{ label: 'Bus', value: 'bus', icon: '🚌' },
{ label: 'Car', value: 'car', icon: '🚗' },
].map((m) => ({
...m,
icon: ({ selected }: { selected: boolean }) => {m.icon},
}));
export default function Example() {
const [mode, setMode] = useState('bike');
return ;
}
```
Icons can be any view. Here each color filter has a swatch on its left:
```tsx live
import { useState } from 'react';
import { View } from 'react-native';
import { HorizontalList } from '@elik56a/react-native-pickers';
const colors = [
['Black', '#1f1f1f'], ['White', '#f4f4f4'], ['Gray', '#9e9e9e'], ['Navy', '#1e3a8a'], ['Blue', '#3b82f6'],
['Green', '#16a34a'], ['Olive', '#708238'], ['Yellow', '#facc15'], ['Orange', '#f97316'], ['Red', '#dc2626'],
['Pink', '#ec4899'], ['Purple', '#9333ea'], ['Brown', '#8b5a2b'], ['Beige', '#d6c6a5'],
].map(([name, hex]) => ({
label: name,
value: name,
icon: ,
}));
export default function Example() {
const [color, setColor] = useState('Navy');
return ;
}
```
### Disabled items
A `disabled` item is dimmed and can't be chosen, like sizes that are sold out.
```tsx live
import { useState } from 'react';
import { HorizontalList } from '@elik56a/react-native-pickers';
const soldOut = ['XS', 'XXL'];
const sizes = ['XS', 'S', 'M', 'L', 'XL', 'XXL', '3XL'].map((s) => ({ label: s, value: s, disabled: soldOut.includes(s) }));
export default function Example() {
const [size, setSize] = useState('M');
return ;
}
```
### Any value type
Values can be anything: numbers, ids, objects. `HorizontalList` infers `T` from `items`, so `onChange` gets the typed value.
```tsx live
import { useState } from 'react';
import { Text, View } from 'react-native';
import { HorizontalList, usePickerTheme } from '@elik56a/react-native-pickers';
const bill = 64.5;
const tips = [0, 10, 15, 18, 20, 25, 30].map((percent) => ({ label: percent ? `${percent}%` : 'No tip', value: percent }));
export default function Example() {
const theme = usePickerTheme();
const [tip, setTip] = useState(18);
return (
Total: ${(bill * (1 + tip / 100)).toFixed(2)}
);
}
```
Object values are compared with `Object.is`. When they're recreated (say, refetched from a server), pass `isEqual`:
```tsx
a.id === b.id} />
```
### Custom items
`renderItem` draws each item's content, and the `item` slot resizes the pill around it. Here, a strip of the next three weeks:
```tsx live
import { useState } from 'react';
import { Text, View } from 'react-native';
import { HorizontalList } from '@elik56a/react-native-pickers';
const dates = Array.from({ length: 21 }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() + i);
return date;
});
const days = dates.map((date, i) => ({ label: date.toDateString(), value: i }));
export default function Example() {
const [day, setDay] = useState(0);
return (
{
const date = dates[item.value];
const color = selected ? theme.colors.primary : theme.colors.text;
return (
{item.value === 0 ? 'Today' : date.toLocaleDateString(undefined, { weekday: 'short' })}
{date.getDate()}
);
}}
accessibilityLabel="Day"
/>
);
}
```
### Try the props
### Behaviour
- **Controlled**: pass `value` and update it in `onChange`. Tapping the selected item calls `onChange` again.
- In `segmented`, the thumb slides to the selected item; items share the track's width while they fit, and scroll when they don't.
- Tapping an item whose neighbor is hidden scrolls that neighbor into view, plus a little of the one after it, so the user sees there's more.
- With `showArrows`, the arrows need a frame around the list: `style` lays out that frame and `styles.container` styles the list itself.
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `ReadonlyArray>` | **required** | `{ label, value, disabled?, icon? }`. `icon` is a node, or a function of `{ selected, disabled, color }`. |
| `value` | `T \| null` | — | Selected value. |
| `onChange` | `(value: T, item) => void` | — | Called on tap. |
| `variant` | `'segmented' \| 'chips'` | `'segmented'` | Track with a sliding thumb, or separate pills. |
| `iconPosition` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'left'` | Where each icon sits around its label. |
| `showArrows` | `boolean` | `false` | Round ‹ › buttons that scroll about a page, shown while the items overflow. |
| `arrowPosition` | `'outside' \| 'overlay'` | `'outside'` | Beside the list, the one that can't scroll further dimmed; or over its edges, each shown while items hide past it. |
| `renderArrow` | `(side: 'left' \| 'right', disabled: boolean) => ReactNode` | — | Custom arrow content. |
| `isEqual` | `(a: T, b: T) => boolean` | `Object.is` | Value equality. |
| `renderItem` | `(info: { item, selected, disabled, theme }) => ReactNode` | — | Custom item content. |
| `accessibilityLabel` | `string` | — | Names the list for screen readers. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | Slots below. |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID; items get `{testID}-item-{index}`. |
#### Slots
| Slot | Element |
| --- | --- |
| `container` | The scroll view (the track in `segmented`) |
| `content` | The row inside it |
| `item` / `selectedItem` | Each item / the selected one |
| `itemLabel` / `selectedItemLabel` | Item label |
| `thumb` | The sliding thumb (`segmented`) |
| `arrow` / `arrowText` | Each scroll arrow / its glyph (`showArrows`) |
### Accessibility
The list is a `radiogroup` named by `accessibilityLabel`; items are `radio`s with `checked` and `disabled` states. Scroll arrows are buttons labelled "Scroll left" and "Scroll right".
### Related
- [Category filter chips](../recipes/category-chips): chips filtering a menu
- [Filters bottom sheet](../recipes/filters-sheet): a segmented sort order inside a sheet
- [Flight search form](../recipes/flight-search-form): trip type and cabin in a booking form
---
## React Native Month Picker
## MonthPicker
`MonthPicker` picks a **month and year** without a day: a card's expiry date, a report's month, a billing period, a graduation date. It looks native on each platform: [month and year wheels on iOS, a grid of months under the year in Material and on the web](#wheels-or-a-grid), or the one you pick with `display`.
```tsx
import { MonthPicker } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { MonthPicker, formatMonthYear } from '@elik56a/react-native-pickers';
export default function Example() {
const [month, setMonth] = useState(new Date(2026, 9, 1));
return (
<>
{formatMonthYear(month)}
>
);
}
```
### Try the props
### The value is the first of the month
`value` and `onChange` use a `Date` on the first day of the month, at midnight: October 2026 is `new Date(2026, 9, 1)`. A `value` on another day shows its month. Read the parts with `getMonth()` (0–11) and `getFullYear()`.
### Wheels or a grid
Each [design](../guides/designs) has its own default:
| `display` | Default in | What it is |
| --- | --- | --- |
| `'wheel'` | iOS | Month and year wheels, in the order the locale writes them (year first in Japanese and Chinese), like the iOS date picker's month mode |
| `'grid'` | Material, web | The year with arrows, above its 12 months to tap. Tap the year for a page of 12 years |
```tsx
```
`monthFormat` sets the wheel's month names: `'long'` (October), `'short'` (Oct) or `'numeric'` (10). The grid always shows short names.
### Min and max
`minDate` and `maxDate` limit the months: months outside them are disabled, and the arrows and year grid stop at their years. Any day of the month counts, so `minDate={new Date()}` allows the current month. Without them, the years go 100 back and 100 ahead, or set `yearRange`.
```tsx live
import { useState } from 'react';
import { MonthPicker } from '@elik56a/react-native-pickers';
const now = new Date();
const inTenYears = new Date(now.getFullYear() + 10, 11, 1);
export default function CardExpiry() {
const [expiry, setExpiry] = useState(new Date(now.getFullYear() + 3, 0, 1));
return ;
}
```
### In a sheet
```tsx
import { MonthPickerSheet } from '@elik56a/react-native-pickers';
setOpen(false)} title="Report month" value={month} onConfirm={setMonth} />
```
It takes the [shared sheet props](./picker-sheets#shared-props) and the picker's, and keeps a draft until **Done**, like the other [picker sheets](./picker-sheets#how-drafts-work).
### In a form field
`MonthPickerField` opens the sheet and shows the confirmed month for `locale`, like `October 2026`. For a card's `10/29`, pass your own `displayValue`.
```tsx
```
→ [Picker fields](./picker-fields)
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Date` | — | Selected month (controlled): its first day. |
| `defaultValue` | `Date` | this month | Initial month when uncontrolled. |
| `onChange` | `(month: Date) => void` | — | Fires with the first day of the picked month. |
| `display` | `'wheel' \| 'grid'` | from the design | Wheels (iOS) or a grid of months (Material, web). |
| `locale` | `string` | device | Month names and the wheel order. |
| `minDate` / `maxDate` | `Date` | — | Earliest and latest selectable month: any day in it. |
| `yearRange` | `[number, number]` | ±100 years | Years when there's no min/max. |
| `monthFormat` | `'long' \| 'short' \| 'numeric'` | `'long'` | Wheel month names. |
| `loop` | `boolean` | `true` | Loop the month wheel. |
| `itemHeight` / `visibleItems` / `variant` | | `40` / `5` / `'3d'` | Wheel row height, rows and style. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | [Wheel slots](./multi-wheel-picker#slots), or the grid's `header`, `headerTitle`, `arrow`, `arrowText`, `gridCell` and `gridCellText`. |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. The wheels get `${testID}-month` and `-year`. |
`MonthPickerSheet` takes these plus the [sheet props](./picker-sheets#shared-props) and `onConfirm(month)`. `MonthPickerField` takes the sheet's props plus the [field props](./picker-fields#props).
### Related
- [DatePicker](./date-picker): a day in a calendar
- [DateWheelPicker](./date-wheel-picker): day, month and year wheels
- [WeekStrip](./week-strip): a day from a week strip
---
## React Native Multi-Column Wheel Picker
## MultiWheelPicker
`MultiWheelPicker` puts several React Native [`WheelPicker`](./wheel-picker)s side by side, sharing **one** selection band, the way iOS shows time and date wheels. `TimePicker` and `DateWheelPicker` are built on it. Use it for durations, heights, weights, currency + amount, or anything multi-column.
```tsx
import { MultiWheelPicker } from '@elik56a/react-native-pickers';
```
```tsx live noplatforms
import { useState } from 'react';
import { MultiWheelPicker } from '@elik56a/react-native-pickers';
const range = (n: number) =>
Array.from({ length: n }, (_, i) => ({
label: String(i).padStart(2, '0'),
value: i,
}));
export default function Example() {
const [h, setH] = useState(1);
const [m, setM] = useState(30);
const [s, setS] = useState(0);
return (
);
}
```
### Try the props
### Columns
Each entry in `columns` is a `WheelPicker`'s props (minus the shared ones below) plus layout keys:
```ts
type WheelColumn = Omit, 'itemHeight' | 'visibleItems' | 'variant' | 'showIndicator' | 'theme'> & {
key: string; // React key, required
flex?: number; // flex weight, default 1
width?: number; // fixed width instead of flex
suffix?: string; // text after the column at the selection band, such as a unit
};
```
So every column has its own `items`, `value`, `onChange`, `onValueChanging`, `loop`, `renderItem`, `keyboardInput`, `isEqual`, `textAlign`, `disabled`, `styles` and so on. `itemHeight`, `visibleItems`, `variant` and `theme` are set once on the `MultiWheelPicker` so all columns line up.
### Labels and separators
`separator` is placed between every pair of columns. A string is rendered as bold text (style it with the `separator` slot), and any React node is rendered as-is. Pass a function to choose per gap: it gets the index of the column on the left and can return `null` to skip that gap.
```tsx live noplatforms
import { useState } from 'react';
import { Text } from 'react-native';
import { MultiWheelPicker } from '@elik56a/react-native-pickers';
const feet = [3, 4, 5, 6, 7].map((v) => ({ label: `${v} ft`, value: v }));
const inches = Array.from({ length: 12 }, (_, i) => ({
label: `${i} in`,
value: i,
}));
export default function Example() {
const [ft, setFt] = useState(5);
const [inch, setInch] = useState(9);
return (
<>
{ft}′{inch}″ = {Math.round((ft * 12 + inch) * 2.54)} cm
>
);
}
```
For a unit after each column, such as `min`, set the column's `suffix` instead: it's shown after the column at the selection band in the theme's secondary text color, and the `separator` slot styles it too. [`DurationPicker`](./duration-picker) labels its wheels this way.
### Column widths
Use `flex` for proportional columns and `width` for fixed ones:
```tsx live noplatforms
import { useState } from 'react';
import { MultiWheelPicker } from '@elik56a/react-native-pickers';
const amounts = Array.from({ length: 100 }, (_, i) => (i + 1) * 10);
export default function Example() {
const [cur, setCur] = useState('USD');
const [amount, setAmount] = useState(250);
return (
);
}
```
### Dependent columns
Columns are plain props, so derive one from another in render. Here the model list depends on the brand:
```tsx live noplatforms
import { useState } from 'react';
import { MultiWheelPicker } from '@elik56a/react-native-pickers';
const models: Record = {
Apple: ['iPhone 15', 'iPhone 16', 'iPhone 17'],
Google: ['Pixel 8', 'Pixel 9', 'Pixel 10'],
Samsung: ['Galaxy S24', 'Galaxy S25', 'Galaxy Z Fold'],
};
export default function Example() {
const [brand, setBrand] = useState('Google');
const [model, setModel] = useState('Pixel 9');
return (
{
setBrand(b);
setModel(models[b][0]);
},
},
{
key: 'model',
items: models[brand],
value: model,
onChange: setModel,
flex: 1.4,
},
]}
/>
);
}
```
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns` | `WheelColumn[]` | **required** | One entry per wheel; see [Columns](#columns). |
| `separator` | `ReactNode \| ((leftIndex: number) => ReactNode)` | — | Content between columns. Strings use the `separator` slot style. |
| `itemHeight` | `number` | `40` | Row height shared by all columns. |
| `visibleItems` | `number` | `5` | Visible rows (odd) shared by all columns. |
| `variant` | `'3d' \| 'flat'` | `'3d'` | Wheel style for all columns. |
| `style` | `StyleProp` | — | Outer container style. |
| `styles` | `SlotStyles` | — | Slot styles, applied to every column (a column's own `styles` win). |
| `theme` | `DeepPartial` | — | Theme override for all columns. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID on the container. |
#### Slots
| Slot | Element |
| --- | --- |
| `container` | Outer view |
| `indicator` | The shared selection band |
| `separator` | String separators |
| `item`, `itemText`, `selectedItemText`, `disabledItemText` | Forwarded to every column's wheel |
```ts
type MultiWheelSlot = 'container' | 'indicator' | 'separator' | WheelPickerSlot;
```
### Related
- [TimePicker](./time-picker) and [DateWheelPicker](./date-wheel-picker) are pre-built `MultiWheelPicker`s
- [Duration recipe](../recipes/duration)
---
## React Native Phone Number Input
## PhoneField
`PhoneField` is a React Native phone number input: a country button with the flag and calling code, then the number, grouped the way that country writes it as the user types. The value is one string in E.164, such as `'+972541234567'`, ready to store or send to an SMS service.
The country list is [`CountryPicker`](./country-picker): searchable, in the device language, a bottom sheet on phones and a dropdown on the web. The field follows the [iOS, Material or web design](../guides/designs). No dependency and no metadata download: formats for about 50 countries add 2 KB.
```tsx
import { PhoneField } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { PhoneField } from '@elik56a/react-native-pickers';
export default function Example() {
const [phone, setPhone] = useState('');
return ;
}
```
Type `054 123 4567`: the leading 0 is dropped, the number shows as `54-123-4567`, and the value is `+972541234567`. Paste `+44 7911 123456` and the field switches to the United Kingdom.
### Try the props
### Checking the number
`onChange` passes the E.164 value and the details: the country, the digits after the calling code, and `valid`, whether the number has as many digits as that country's numbers do.
```tsx live
import { useState } from 'react';
import { Text, View } from 'react-native';
import { PhoneField } from '@elik56a/react-native-pickers';
import type { PhoneDetails } from '@elik56a/react-native-pickers';
export default function Example() {
const [phone, setPhone] = useState('');
const [details, setDetails] = useState(null);
return (
{
setPhone(value);
setDetails(info);
}}
/>
{details ? `${details.country} · ${details.valid ? 'looks right' : 'too short or too long'}` : 'Type a number'}
);
}
```
`valid` checks the length only, which catches most typos. For carrier checks, verify on your server, as with an SMS code.
### The country
The country follows the number: a `value` or a pasted number with another calling code shows its own country. Before there is a number, `defaultCountry` picks it, or control it with `country` and `onCountryChange`, for example from the device region.
Countries sharing a calling code keep the one chosen: a US number with a Caribbean area code stays American. Picking another country keeps the digits and moves them to its calling code.
### How numbers are formatted
About 50 countries, those with the most phone users, have their own pattern: `(201) 555-0123` in the US, `6 12 34 56 78` in France, `54-123-4567` in Israel. Countries sharing a calling code with one of them use its pattern, so Canada and the Caribbean look American. The rest are grouped in threes. Grouping is only for display; the value is always digits.
A leading 0, the trunk prefix most countries dial at home, is dropped, except in Italy, San Marino and the Vatican, where it's part of the number.
### Utilities
| Export | Description |
| --- | --- |
| `parsePhone(international, current?)` | `{ country?, nationalNumber }` for a number like `'+44 7911 123456'` |
| `formatNationalNumber(digits, country)` | `'2015550123'`, `'US'` → `'(201) 555-0123'` |
| `isValidPhone(digits, country)` | Whether the digits after the calling code have a valid length |
| `toE164(digits, country)` | `'541234567'`, `'IL'` → `'+972541234567'` |
```ts
const { country, nationalNumber } = parsePhone(user.phone);
const shown = country ? formatNationalNumber(nationalNumber, country) : user.phone;
```
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | — | The number in E.164, `''` when empty. Leave it out to keep the number inside. |
| `defaultValue` | `string` | `''` | Starting number when `value` isn't set. |
| `onChange` | `(value: string, details: PhoneDetails) => void` | — | Called as the user types or picks a country. |
| `country` | `CountryCode` | — | The country whose calling code is used. |
| `defaultCountry` | `CountryCode` | `'US'` | Starting country when `country` isn't set. |
| `onCountryChange` | `(country: CountryCode) => void` | — | Called when the user picks a country or pastes a number from another. |
| `label` | `string` | — | Above the field, or on its border in Material. Screen readers hear *Phone number* without one. |
| `placeholder` | `string` | an example, e.g. `(123) 456-7890` | Shown while empty. |
| `disabled` | `boolean` | `false` | |
| `countries` / `exclude` / `preferred` | `CountryCode[]` | — | Which countries the list offers, and which come first. See [CountryPicker](./country-picker#a-subset-of-countries). |
| `preferredLabel` / `allCountriesLabel` | `string` | `'Suggested'` / `'All countries'` | Headers of the list. |
| `locale` | `string` | device locale | Language of the country names. |
| `presentation` | `'auto' \| 'dropdown' \| 'bottomSheet' \| 'modal'` | `'auto'` | Where the country list opens: a dropdown on the web, a bottom sheet on native. |
| `countrySheetTitle` | `string` | `'Country code'` | Title of the country sheet. |
| `sheet` | `SheetChromeProps` | `sheetProps.height: 0.8` | Options for the country sheet, as [`CountryPicker`](./country-picker#props)'s. |
| `theme` / `design` | | | See [Theming](../guides/theming) and [Designs](../guides/designs). |
| `style` | `ViewStyle` | — | The field's container. |
| `styles` | `{ label, field, input, countryButton, dialCode, chevron }` | — | [Slot styles](../guides/styling). |
| `testID` | `string` | — | On the text input; the country button gets `${testID}-country`. |
#### Types
```ts
interface PhoneDetails {
country: CountryCode;
nationalNumber: string; // digits after the calling code, e.g. '541234567'
valid: boolean; // has as many digits as the country's numbers do
}
```
---
## React Native Date, Time & Wheel Picker Fields
## Picker fields
In a form, a picker usually isn't on screen. The user taps a field, picks in a bottom sheet (a dropdown under the field on the web), and sees the value in the field once it closes. Each picker has a field that does this, with the same look as [`SelectPicker`](./select-picker)'s field:
| Field | Opens | Shows |
| --- | --- | --- |
| `DatePickerField` | [`DatePickerSheet`](./picker-sheets#datepickersheet) | `Oct 5, 2026`, a range `Oct 5, 2026 – Oct 9, 2026` or a list of dates |
| `TimePickerField` | [`TimePickerSheet`](./picker-sheets#timepickersheet) | `9:30 AM` or `09:30`, following `format` and `locale` |
| `DateTimePickerField` | [`DateTimePickerSheet`](./date-time-picker#in-a-sheet) | `Oct 26, 2026, 9:30 AM` |
| `DurationPickerField` | [`DurationPickerSheet`](./duration-picker#in-a-sheet) | `1 h 30 min` |
| `MonthPickerField` | [`MonthPickerSheet`](./month-picker#in-a-sheet) | `October 2026` |
| `ColorPickerField` | [`ColorPickerSheet`](./color-picker#in-a-sheet) | A swatch and `#E4572E` |
| `WheelPickerField` | [`WheelPickerSheet`](./picker-sheets#wheelpickersheet) | The selected item's `label` |
| `EmojiPickerField` | [`EmojiPickerSheet`](./picker-sheets#emojipickersheet) | `value`, the emoji you pass |
| `PickerField` | Any sheet you render | `displayValue` |
```tsx
import { DatePickerField, TimePickerField, DateTimePickerField, WheelPickerField, EmojiPickerField, PickerField } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { View } from 'react-native';
import {
DatePickerField,
DateTimePickerField,
EmojiPickerField,
SelectPicker,
TimePickerField,
WheelPickerField,
} from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
const guests = Array.from({ length: 8 }, (_, i) => ({
label: `${i + 1} guest${i ? 's' : ''}`,
value: i + 1,
}));
const rooms = ['Standard', 'Deluxe', 'Suite'].map((r) => ({
label: r,
value: r,
}));
export default function Example() {
const [stay, setStay] = useState({ start: null, end: null });
const [arrival, setArrival] = useState();
const [guestCount, setGuestCount] = useState();
const [room, setRoom] = useState(null);
const [reminder, setReminder] = useState();
const [mood, setMood] = useState();
return (
setReminder(undefined)}
/>
);
}
```
Each field takes the props of the sheet it opens, without `visible`, plus the [field props](#props). The field owns the open state; you keep the value. As with the sheets, `onConfirm` fires only on **Done**, so your state changes only when the user confirms. On the web they open as dropdowns; switch `presentation` in the panel to see the same form with bottom sheets or modals.
### Try the props
### Dropdown, bottom sheet or modal
`presentation` sets where the picker opens:
| `presentation` | Opens |
| --- | --- |
| `'auto'` (default) | A dropdown on the web, a bottom sheet on native. |
| `'dropdown'` | Under the field, like a menu, or above it when there's more room there. **Cancel** and **Done** sit below the picker; a tap outside cancels. It's at least 320 px wide, so a calendar fits under a narrow field. It opens on the side with room for the whole picker; in a window too short for it, the dropdown scrolls. |
| `'bottomSheet'` | A bottom sheet with the title and buttons in its header. |
| `'modal'` | A centered dialog. |
```tsx
```
`sheetProps` and `title` apply to the bottom sheet and the modal; the dropdown has no header. [`SelectPicker`](./select-picker#presentation) takes the same values and the same `'auto'` default.
### What the field shows
Dates and times are formatted with `Intl` for the field's `locale` (the device locale by default). `TimePickerField` and `DateTimePickerField` follow `format` (`'12h'`, `'24h'` or `'auto'`) and `showSeconds`.
For your own wording, pass `displayValue`:
```tsx
```
When `displayValue` is empty and there's no value, the field shows `placeholder` (default `'Select…'`).
### Label and sheet title
`label` is drawn above the field, used as its accessibility label and, unless you set `title`, as the sheet's title (a bottom sheet or modal; the dropdown has no title).
```tsx
```
### Clearing
`clearable` shows a × button while the field has a value. Reset your state in `onClear`:
```tsx
setDue(null)} />
```
### Custom trigger
`renderTrigger` replaces the field box, and gets `{ open, displayValue, disabled }`. The label still renders above it, and the sheet still opens from the field.
```tsx live
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
import { EmojiPickerField } from '@elik56a/react-native-pickers';
export default function Example() {
const [icon, setIcon] = useState('🌱');
return (
(
{displayValue}
)}
/>
);
}
```
`renderChevron` swaps only the chevron icon and keeps its rotation, as in [SelectPicker](./select-picker#custom-chevron).
### Your own sheet: `PickerField`
`PickerField` is the field without a picker. Its child is a function that gets `{ visible, onClose }` for the sheet you render, so any [`BottomSheet`](./bottom-sheet) or [`PickerSheet`](./picker-sheets#pickersheet) can open from a field. A `PickerSheet` inside follows the field's `presentation`, so it opens as a dropdown on the web; a `presentation` set on the `PickerSheet` itself wins.
```tsx live
import { useState } from 'react';
import { Switch, Text, View } from 'react-native';
import {
PickerField,
PickerSheet,
useDraft,
} from '@elik56a/react-native-pickers';
type Prefs = { push: boolean; email: boolean };
const NotificationsSheet = ({
visible,
onClose,
prefs,
onSave,
}: {
visible: boolean;
onClose: () => void;
prefs: Prefs;
onSave: (p: Prefs) => void;
}) => {
const [draft, setDraft] = useDraft(prefs, visible);
return (
{
onSave(draft);
onClose();
}}
>
{(['push', 'email'] as const).map((k) => (
{k}
setDraft({ ...draft, [k]: v })}
/>
))}
);
};
export default function Example() {
const [prefs, setPrefs] = useState({ push: true, email: false });
const summary =
[prefs.push && 'Push', prefs.email && 'Email']
.filter(Boolean)
.join(' + ') || 'Off';
return (
{(sheet) => (
)}
);
}
```
### Styling
`style` styles the wrapper (label and field). `styles` takes the field's slots and the slots of the picker inside the sheet, so one object styles both:
```tsx
```
| Slot | Element |
| --- | --- |
| `label` | Label text above the field |
| `trigger` | The field box |
| `triggerText` | The value |
| `placeholder` | Placeholder text |
| `chevron` | The drawn chevron (a bordered `View`: `borderColor` sets its color, `width` / `height` its size) |
| `clear` | × button text |
The theme (`theme` prop or [`PickersProvider`](../guides/theming)) applies to the field and the sheet.
### Props
Every field takes these, plus its sheet's props without `visible`: `value`, `onConfirm` (`onSelect` for emoji), `title`, `confirmLabel`, `sheetProps`, the picker's own props (`mode`, `minDate`, `locale`, `items`, `minuteInterval`…). The picker's own `style` and `testID` aren't available on a field; `style` and `testID` belong to the field.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | — | Label above the field. Also its accessibility label, and the sheet title unless `title` is set. |
| `placeholder` | `string` | `'Select…'` | Shown while there's no value. |
| `displayValue` | `string` | the value, formatted for `locale` | Text shown in the field. |
| `disabled` | `boolean` | `false` | The field doesn't open. |
| `clearable` | `boolean` | `false` | Show a × button while there's a value. |
| `onClear` | `() => void` | — | Called by ×. Reset your value here. |
| `renderTrigger` | `(info: { open, displayValue, disabled }) => ReactNode` | — | Replace the field box. |
| `renderChevron` | `(info: { open, theme }) => ReactNode` | — | Custom chevron icon. It still rotates on open and close. |
| `presentation` | `'auto' \| 'dropdown' \| 'bottomSheet' \| 'modal'` | `'auto'` | Where the picker opens: a dropdown on the web, a bottom sheet on native. See [Dropdown, bottom sheet or modal](#dropdown-bottom-sheet-or-modal). |
| `onOpen` / `onClose` | `() => void` | — | The picker opened / closed (Done, Cancel or dismiss). |
| `style` | `StyleProp` | — | Wrapper style (label + field). |
| `styles` | `SlotStyles` | — | Field slots plus the picker's slots. |
| `theme` | `DeepPartial` | — | Theme for the field and the sheet. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). Passed on to the sheet it opens. |
| `testID` | `string` | — | Test ID on the field box. |
`EmojiPickerField` also takes `value?: string`, the emoji to show. `PickerField` takes the props above (no sheet props), `children: (sheet: { visible, onClose }) => ReactNode`, and `icon`, a node shown before the value such as a colour swatch or a flag.
#### Types
```ts
import type {
DatePickerFieldProps,
TimePickerFieldProps,
DateTimePickerFieldProps,
WheelPickerFieldProps,
EmojiPickerFieldProps,
PickerFieldProps,
PickerFieldBaseProps,
PickerFieldSlot,
PickerFieldTriggerInfo,
FieldPresentation,
WithPickerField,
} from '@elik56a/react-native-pickers';
```
`WithPickerField` turns a sheet's props into a field's, for wrapping your own sheet the same way.
### Accessibility
The field is a button labelled with `label` (or the placeholder), with the current value as its hint and an `expanded` state while the picker is open. The dropdown closes on a tap outside it and on the Android back button. The × button is labelled `Clear`.
### Related
- [Picker sheets](./picker-sheets): the sheets on their own, when you control `visible`
- [SelectPicker](./select-picker): the select field these match
- [Forms](../guides/forms): React Hook Form and Formik
---
## React Native Bottom Sheet Pickers
## Picker sheets
Every React Native picker in this library is also available inside a [`BottomSheet`](./bottom-sheet) with **Cancel / Done**: in the header beside the title on iOS, under the content with **OK** in Material, per the [design](../guides/designs). They all share the same "chrome" props and the same draft behaviour.
:::tip Opening a sheet from a form field?
You don't need your own button and `visible` state. [Picker fields](./picker-fields) (`DatePickerField`, `TimePickerField`, `DateTimePickerField`, `DurationPickerField`, `MonthPickerField`, `ColorPickerField`, `WheelPickerField`, `EmojiPickerField`) take the same props without `visible`/`onClose`, render a field that opens the sheet, and show the confirmed value.
:::
### Inline, bottom sheet or modal
Every picker can be shown three ways:
- **Inline:** the plain component (`WheelPicker`, `TimePicker`, `DatePicker`, `SelectList`, `EmojiPicker`) renders in place.
- **Bottom sheet:** slides up from the bottom and can be dragged down to dismiss.
- **Modal:** a centered dialog.
Each sheet can also open from a [field](./picker-fields) that shows the picked value.
The sheets take `presentation`: `'auto'` (default), `'bottomSheet'` or `'modal'`. `auto` is a modal on web windows wider than the sheet, where a bottom sheet would look cut off, and a bottom sheet on phones and native.
```tsx
```
[`SelectPicker`](./select-picker#presentation) also supports `'dropdown'`, which opens under the field. It's `SelectPicker`'s default on the web.
```tsx
import { DatePickerSheet, TimePickerSheet, WheelPickerSheet, EmojiPickerSheet, PickerSheet, useDraft } from '@elik56a/react-native-pickers';
```
### Try the props
### How drafts work
1. When the sheet **opens**, it copies `value` into a draft.
2. The picker inside edits only the draft.
3. **Done** calls `onConfirm(draft)` and then `onClose()`.
4. **Cancel**, a backdrop tap, a swipe down or Android back calls only `onClose()`, and the draft is thrown away.
Your state changes only when the user confirms, which is how native iOS / Android pickers behave.
### Shared props
All sheets accept these (`SheetChromeProps`):
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `visible` | `boolean` | **required** | Show / hide. |
| `onClose` | `() => void` | **required** | Set `visible` to `false` here. |
| `title` | `ReactNode` | — | Header title. |
| `confirmLabel` | `string` | `'Done'` · `'OK'` Material | Confirm button text. |
| `cancelLabel` | `string` | `'Cancel'` | Cancel button text. |
| `showCancel` | `boolean` | `true` (`false` for emoji) | Show the Cancel button. |
| `presentation` | `'auto' \| 'bottomSheet' \| 'modal'` | `'auto'` | Bottom sheet or centered modal. `auto`: modal on wide web windows, bottom sheet elsewhere. |
| `sheetProps` | `Partial` | — | Passed to the [BottomSheet](./bottom-sheet#props) (`height`, `bottomInset`, `styles`, `enableSwipeToClose`…). |
| `theme` | `DeepPartial` | — | Theme for the sheet and the picker. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
---
### DatePickerSheet
A calendar (single, range, multiple) or date wheel in a sheet.
```tsx live
import { useState } from 'react';
import { Pressable, Text, View } from 'react-native';
import { DatePickerSheet, formatDate } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
function Field({
label,
value,
onPress,
}: {
label: string;
value: string;
onPress: () => void;
}) {
return (
{label}{value}
);
}
export default function Example() {
const [sheet, setSheet] = useState<'day' | 'range' | 'birthday' | null>(null);
const [day, setDay] = useState(null);
const [range, setRange] = useState({ start: null, end: null });
const [birthday, setBirthday] = useState(new Date(1995, 6, 12));
const close = () => setSheet(null);
return (
setSheet('day')}
/>
setSheet('range')}
/>
setSheet('birthday')}
/>
);
}
```
#### Behaviour
- **Done is disabled** until the selection is complete: a date in single mode, both `start` and `end` in range mode, no half-picked range in ranges mode. Multiple and ranges modes can confirm an empty array.
- **`confirmOnSelect`** (single + calendar only) hides Done and confirms as soon as a day is tapped.
- **`display="wheel"`** shows a [DateWheelPicker](./date-wheel-picker) (single mode). The draft starts at `value` or today. `locale`, `minDate` and `maxDate` are forwarded; pass other wheel props via `wheelProps`.
#### Props
Shared props, plus every [calendar prop](./date-picker#calendar) (`minDate`, `maxDate`, `isDateDisabled`, `markedDates`, `locale`, `firstDayOfWeek`, `styles`, `renderDay`…), plus:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | `'single' \| 'range' \| 'multiple' \| 'ranges'` | `'single'` | Selection mode. |
| `value` | `Date \| null` · `DateRange` · `Date[]` · `DateRange[]` | — | Current committed value; copied into the draft on open. |
| `onConfirm` | `(date: Date)` · `(range: DateRange)` · `(dates: Date[])` · `(ranges: DateRange[])` `=> void` | **required** | Called with the draft on Done. |
| `minRangeDays` / `maxRangeDays` | `number` | — | Range and ranges modes. |
| `maxSelections` | `number` | — | Multiple mode. |
| `maxRanges` | `number` | — | Ranges mode. |
| `display` | `'calendar' \| 'wheel'` | `'calendar'` | Wheel is single mode only. |
| `wheelProps` | `Partial` | — | Extra props for the wheel. |
| `confirmOnSelect` | `boolean` | `false` | Single calendar: confirm on tap, no Done button. |
---
### TimePickerSheet
```tsx live
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
import { TimePickerSheet } from '@elik56a/react-native-pickers';
export default function Example() {
const [open, setOpen] = useState(false);
const [time, setTime] = useState(new Date(2026, 0, 1, 7, 30));
return (
<>
setOpen(true)}
style={{ alignItems: 'center', padding: 12 }}
>
{time.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
Change setOpen(false)}
title="Wake up"
value={time}
onConfirm={setTime}
minuteInterval={5}
/>
>
);
}
```
Props: shared props, every [TimePicker prop](./time-picker#props) except `value`/`defaultValue`/`onChange`, plus:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Date` | now | Copied into the draft on open. |
| `onConfirm` | `(date: Date) => void` | **required** | Called with the draft on Done. |
---
### WheelPickerSheet
```tsx live
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
import { WheelPickerSheet } from '@elik56a/react-native-pickers';
const guests = Array.from({ length: 12 }, (_, i) => ({
label: `${i + 1} guest${i ? 's' : ''}`,
value: i + 1,
}));
export default function Example() {
const [open, setOpen] = useState(false);
const [count, setCount] = useState(2);
return (
<>
setOpen(true)}
style={{
borderWidth: 1,
borderColor: '#E8E4DC',
borderRadius: 12,
padding: 14,
}}
>
👥 {guests[count - 1].label} setOpen(false)}
title="Guests"
items={guests}
value={count}
onConfirm={setCount}
/>
>
);
}
```
Props: shared props, every [WheelPicker prop](./wheel-picker#props) except `value`/`defaultValue`/`onChange`, plus:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `T` | first item | Copied into the draft on open. |
| `onConfirm` | `(value: T) => void` | **required** | Called with the draft on Done. |
---
### EmojiPickerSheet
No Done button: each pick calls `onSelect`, and the sheet closes unless `closeOnSelect={false}`.
```tsx live
import { useState } from 'react';
import { Pressable, Text, View } from 'react-native';
import { EmojiPickerSheet } from '@elik56a/react-native-pickers';
export default function Example() {
const [open, setOpen] = useState(false);
const [reactions, setReactions] = useState(['👍', '🎉']);
return (
{reactions.map((r, i) => (
{r}
))}
setOpen(true)}
style={{
borderRadius: 999,
borderWidth: 1,
borderColor: '#E8E4DC',
paddingHorizontal: 12,
justifyContent: 'center',
}}
>
+ setOpen(false)}
title="React"
onSelect={(e) => setReactions((r) => [...r, e])}
/>
);
}
```
Props: shared props, every [EmojiPicker prop](./emoji-picker#props), plus:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `closeOnSelect` | `boolean` | `true` | Close after a pick. |
| `height` | `number` | `420` | Picker height inside the sheet. |
| `showCancel` | `boolean` | `false` | Show a Cancel button. |
---
### PickerSheet
The building block: a `BottomSheet` with Cancel on the left and (when you pass `onConfirm`) Done on the right. Use it to build sheets for your own pickers.
```tsx live
import { useState } from 'react';
import { Pressable, Text, Switch, View } from 'react-native';
import { PickerSheet, useDraft } from '@elik56a/react-native-pickers';
type Prefs = { push: boolean; email: boolean };
export default function Example() {
const [open, setOpen] = useState(false);
const [prefs, setPrefs] = useState({ push: true, email: false });
const [draft, setDraft] = useDraft(prefs, open);
return (
<>
setOpen(true)}
style={{ padding: 14, borderRadius: 12, backgroundColor: '#F4F2EE' }}
>
Notifications:{' '}
{[prefs.push && 'push', prefs.email && 'email']
.filter(Boolean)
.join(' + ') || 'off'}
setOpen(false)}
title="Notifications"
confirmLabel="Save"
onConfirm={() => {
setPrefs(draft);
setOpen(false);
}}
>
{(['push', 'email'] as const).map((k) => (
{k}
setDraft({ ...draft, [k]: v })}
/>
))}
>
);
}
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | **required** | Sheet content. |
| `onConfirm` | `() => void` | — | Shows the confirm button. **You** close the sheet (call `onClose` / set `visible`). |
| `confirmDisabled` | `boolean` | `false` | Disable the confirm button. |
| …shared props | | | `visible`, `onClose`, `title`, `confirmLabel`, `cancelLabel`, `showCancel`, `sheetProps`, `theme`. |
### `useDraft`
```ts
function useDraft(value: V, visible: boolean): [draft: V, setDraft: (v: V) => void];
```
Returns a draft copy of `value` that **resets to `value` each time `visible` turns true**. It's the hook the built-in sheets use; use it for your own `PickerSheet`s as shown above.
---
## React Native Searchable Select List
## SelectList
`SelectList` is the searchable React Native list inside [`SelectPicker`](./select-picker), as a standalone inline component. Use it in settings screens, onboarding steps, or your own sheets and modals.
```tsx
import { SelectList } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { View } from 'react-native';
import { SelectList } from '@elik56a/react-native-pickers';
const themes = [
{
label: 'System',
value: 'system',
description: 'Follow the device setting',
},
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
];
export default function Example() {
const [mode, setMode] = useState('system');
return (
);
}
```
### Try the props
### Multiple, with search
```tsx live
import { useState } from 'react';
import { SelectList } from '@elik56a/react-native-pickers';
const toppings = [
'Mozzarella',
'Basil',
'Mushrooms',
'Olives',
'Pepperoni',
'Jalapeños',
'Pineapple',
'Onion',
'Ham',
'Rocket',
].map((t) => ({
label: t,
value: t,
group: ['Pepperoni', 'Ham'].includes(t) ? 'Meat' : 'Veggie',
}));
export default function Example() {
const [picked, setPicked] = useState(['Mozzarella', 'Basil']);
return (
);
}
```
### Searching a server
`loadOptions` loads the options for what the user types, with the loader showing from the keystroke until they arrive. Here the list starts with recent picks and searches a fake API:
```tsx live
import { useState } from 'react';
import { SelectList } from '@elik56a/react-native-pickers';
const RECENT = ['react', 'react-native', 'expo'].map((name) => ({ label: name, value: name, description: 'Recent' }));
const PACKAGES = ['react', 'react-dom', 'react-native', 'react-native-web', 'react-query', 'redux', 'expo', 'expo-router', 'zod', 'zustand', 'date-fns', 'dayjs'];
// Stands in for a package registry search
const searchPackages = (query: string) =>
new Promise<{ label: string; value: string }[]>((resolve) =>
setTimeout(() => resolve(PACKAGES.filter((p) => p.includes(query.toLowerCase())).map((p) => ({ label: p, value: p }))), 500)
);
export default function Example() {
const [packages, setPackages] = useState([]);
return (
);
}
```
### Loading more as you scroll
`onEndReached` fires near the end of the list; append the next page to `options` and set `loading` while it's on its way. The spinner shows under the options.
```tsx live
import { useEffect, useState } from 'react';
import { SelectList } from '@elik56a/react-native-pickers';
const PAGE = 15;
const TOTAL = 60;
// Stands in for your API
const fetchPage = (offset: number) =>
new Promise<{ label: string; value: number }[]>((resolve) =>
setTimeout(
() =>
resolve(
Array.from({ length: Math.min(PAGE, TOTAL - offset) }, (_, i) => ({
label: `Contact ${offset + i + 1}`,
value: offset + i + 1,
}))
),
800
)
);
export default function Example() {
const [contact, setContact] = useState(null);
const [options, setOptions] = useState<{ label: string; value: number }[]>([]);
const [loading, setLoading] = useState(false);
const loadMore = () => {
if (loading || options.length >= TOTAL) return;
setLoading(true);
fetchPage(options.length).then((page) => {
setOptions((current) => [...current, ...page]);
setLoading(false);
});
};
useEffect(loadMore, []);
return (
);
}
```
### Behaviour
- **Controlled only**: pass `value` and update it in `onChange`.
- In single mode, tapping an option calls `onChange(value, option)`, including when it's already selected (there's no deselect).
- In multiple mode, taps toggle. `onChange(values, options)` keeps selection order (appended at the end).
- With `maxSelected` reached, unselected options become disabled until something is deselected.
- Without `listHeight`, the list grows with its content. Inside a scroll view or sheet, give it a `listHeight` so it scrolls on its own.
- Search state is internal and resets when the list unmounts; with `onSearchChange`, that reset calls `onSearchChange('')` so a remote list can drop its results.
- For options from a server, see [Search on a server](./select-picker#search-on-a-server): `loadOptions`, `onSearchChange`, `loading` and `onEndReached` work the same here.
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `ReadonlyArray>` | `[]` | See [options](./select-picker#options). With `loadOptions`, the ones shown before the user types. |
| `multiple` | `boolean` | `false` | Multi-select. |
| `value` | `T \| null` · `T[]` | — | Selection. |
| `onChange` | `(value: T, option) => void` · `(values: T[], options) => void` | — | Single · multiple. |
| `maxSelected` | `number` | — | Multiple only. |
| `searchable` | `boolean` | `options.length > 8` | Show the search field. |
| `query` | `string` | — | Controlled search query, for filtering from your own input. Hides the built-in search field. |
| `searchPlaceholder` | `string` | `'Search'` | Search hint and its accessibility label. |
| `filterOption` | `(option: SelectOption, query: string) => boolean` | `defaultFilter`; none with `loadOptions` / `onSearchChange` | Custom filter. |
| `loadOptions` | `(query: string) => Promise[]>` | — | [Search a server](./select-picker#search-on-a-server): loader, debounce and stale answers handled. |
| `onSearchChange` | `(query: string) => void` | — | Called once typing pauses, to search yourself. |
| `searchPrompt` | `string` | `'Type to search'` | Shown before the user types, while a searched list has no options. |
| `searchDebounce` | `number` | `300` | ms after the last keystroke before `loadOptions` / `onSearchChange`. |
| `onEndReached` | `() => void` | — | Called near the end of the list, to load more. |
| `loading` | `boolean` | `false` | Spinner in place of an empty list, or under the options. |
| `loadingText` | `string` | `'Loading…'` | Text next to the spinner. |
| `renderLoading` | `(theme: PickerTheme) => ReactNode` | — | Custom loader. |
| `renderOption` | `(info: SelectRenderOptionInfo) => ReactNode` | — | Custom row content. |
| `renderGroupHeader` | `(group: string, theme: PickerTheme) => ReactNode` | — | Custom section header. |
| `emptyText` | `string` | `'No results'` | Empty-state text. |
| `showCheckmarks` | `boolean` | `true` | ✓ (single) / checkbox (multiple). |
| `isEqual` | `(a: T, b: T) => boolean` | `Object.is` | Value equality. |
| `listHeight` | `number` | — | Fixed height for the scrolling list. |
| `autoFocusSearch` | `boolean` | `false` | Focus the search on mount. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | Slots below. |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. |
#### Slots
| Slot | Element |
| --- | --- |
| `container` | Outer view |
| `search` | Search field wrapper |
| `searchInput` | The `TextInput` |
| `list` | The `FlatList` |
| `groupHeader` / `groupHeaderText` | Section header |
| `option` | Each row |
| `selectedOption` | Selected rows (merged after `option`) |
| `optionLabel` / `selectedOptionLabel` | Row label |
| `optionDescription` | Second line |
| `check` | ✓ or checkbox |
| `empty` / `emptyText` | Empty state, and the `searchPrompt` |
| `loading` / `loadingText` | Loader row / its text |
#### Types
```ts
interface SelectRenderOptionInfo { option: SelectOption; selected: boolean; disabled: boolean; theme: PickerTheme }
type SelectValueProps =
| { multiple?: false; value?: T | null; onChange?: (value: T, option: SelectOption) => void }
| { multiple: true; value?: T[]; onChange?: (value: T[], options: SelectOption[]) => void; maxSelected?: number };
```
---
## React Native Select & Dropdown Picker
## SelectPicker
`SelectPicker` is a React Native form field that opens a searchable list: in a bottom sheet on phones, a dropdown under the field on the web, or a centered modal (`presentation`). Single or multiple selection, with groups, icons, descriptions, chips, a clear button and [search on your server](../recipes/server-search). It follows the [iOS, Material or web design](../guides/designs): a ✓ or radios, rounded or edge-to-edge rows.
```tsx
import { SelectPicker } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';
const fruits = [
'Apple',
'Banana',
'Cherry',
'Mango',
'Orange',
'Peach',
'Pear',
'Plum',
'Strawberry',
'Watermelon',
].map((f) => ({
label: f,
value: f.toLowerCase(),
}));
export default function Example() {
const [fruit, setFruit] = useState(null);
return (
);
}
```
:::caution Always controlled
`SelectPicker` doesn't store the selection itself. Pass `value` and update it from `onChange`.
:::
### Try the props
### Options
```ts
interface SelectOption {
label: string; // row text, and what search matches first
value: T; // any type; compared with isEqual (default Object.is)
description?: string; // second line, also searchable
icon?: React.ReactNode; // leading element: flag, avatar, icon
group?: string; // section header
disabled?: boolean;
keywords?: string[]; // extra search terms, e.g. other languages or synonyms
}
```
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { SelectPicker } from '@elik56a/react-native-pickers';
const countries = [
{
label: 'Israel',
value: 'IL',
icon: 🇮🇱,
group: 'Middle East',
keywords: ['ישראל'],
},
{ label: 'Jordan', value: 'JO', icon: 🇯🇴, group: 'Middle East' },
{
label: 'France',
value: 'FR',
icon: 🇫🇷,
group: 'Europe',
description: 'République française',
},
{
label: 'Germany',
value: 'DE',
icon: 🇩🇪,
group: 'Europe',
keywords: ['deutschland'],
},
{
label: 'Spain',
value: 'ES',
icon: 🇪🇸,
group: 'Europe',
disabled: true,
description: 'Not shipping yet',
},
{
label: 'Japan',
value: 'JP',
icon: 🇯🇵,
group: 'Asia',
keywords: ['nippon'],
},
];
export default function Example() {
const [country, setCountry] = useState('FR');
return (
);
}
```
Groups appear in the order their first option appears in `options`. Options without a `group` are listed together without a header.
### Multiple selection
`multiple` switches `value` to an array and `onChange` to `(values, options)`. The sheet gets checkboxes and a **Done** button, and the field shows removable **chips** (tap a chip to remove it). `maxSelected` caps the selection; once reached, the remaining options are disabled.
```tsx live
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';
const skills = [
'React',
'React Native',
'TypeScript',
'Expo',
'Node.js',
'GraphQL',
'Swift',
'Kotlin',
].map((s) => ({ label: s, value: s }));
export default function Example() {
const [picked, setPicked] = useState([
'React Native',
'TypeScript',
]);
return (
);
}
```
Set `chips={false}` to show a comma-separated list instead of chips.
### Search
As a **dropdown** (the default on the web), the user types right in the field to filter the options; `searchable={false}` keeps the field read-only. In a **sheet**, the search field appears automatically when there are **more than 8 options**; force it with `searchable` / `searchable={false}`. The default filter:
- matches `label`, `description` and `keywords`
- is case-insensitive and **accent-insensitive** (`creme` finds `Crème`), and ignores Hebrew niqqud
- splits the query on spaces and requires **every** word to match (`new yo` finds `New York`)
Provide `filterOption` for your own logic, for example matching only from the start of the label:
```tsx
option.label.toLowerCase().startsWith(query.toLowerCase())}
/>
```
The default is exported as `defaultFilter`, so you can extend it:
```tsx
import { defaultFilter } from '@elik56a/react-native-pickers';
filterOption={(o, q) => defaultFilter(o, q) || o.value === q.toUpperCase()}
```
### Search on a server
Pass `loadOptions` and the picker runs the search for you. It waits for a pause in typing (`searchDebounce`, 300 ms by default), shows the loader from the keystroke until the results arrive, and ignores answers to older queries. Before the user types, it lists `options`; with none, like here, it shows `searchPrompt` (default *Type to search*). A search with no results shows `emptyText`.
```tsx live
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';
const CITIES = ['Amsterdam', 'Athens', 'Barcelona', 'Berlin', 'Lisbon', 'London', 'Madrid', 'Milan', 'Paris', 'Prague', 'Rome', 'Vienna'];
// Stands in for your API
const searchCities = (query: string) =>
new Promise<{ label: string; value: string }[]>((resolve) =>
setTimeout(
() =>
resolve(
CITIES.filter((c) => c.toLowerCase().includes(query.toLowerCase())).map((c) => ({
label: c,
value: c,
}))
),
700
)
);
export default function Example() {
const [city, setCity] = useState(null);
return (
);
}
```
- **Search field from the start**, even with no `options`.
- **No double filtering**: results are listed as `loadOptions` returns them. Pass `filterOption` to filter anyway.
- **The selected label stays** in the field after a new search drops it from the results.
- **A failed request** shows `emptyText`.
For a whole form built this way, see the [flight search form recipe](../recipes/flight-search-form).
#### Searching yourself
For full control (pagination, caching, your own state), use `onSearchChange` instead: it's called with the query once typing pauses, and you set `options` and `loading`. With options showing, `loading` puts the spinner under them, which suits `onEndReached` for loading the next page. See the [Search a server recipe](../recipes/server-search).
`renderLoading` replaces the spinner and text with your own element:
```tsx
Asking the server…}
/>
```
### Clearing
`clearable` shows a × in the field when something is selected.
- In **multiple** mode, × calls `onChange([], [])` and then `onClear`.
- In **single** mode, × calls **only** `onClear`. Reset your state there:
```tsx live
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [v, setV] = useState('b');
return (
setV(null)}
options={[
{ label: 'Low', value: 'a' },
{ label: 'Medium', value: 'b' },
{ label: 'High', value: 'c', description: 'Pages on-call' },
]}
value={v}
onChange={setV}
/>
);
}
```
### Closing behaviour
In single mode, the value changes as soon as an option is tapped and the sheet or dropdown closes a moment later, once the row's ripple has played (`closeOnSelect`, default `true`). With `closeOnSelect={false}` the sheet stays open and shows a **Done** button. Multiple mode always shows Done. `onOpen` and `onClose` fire when the sheet opens and closes.
### Hover and ripple
Rows tint while the pointer is over them and ripple from the touch point when picked; in single mode the list closes right after the ripple plays. The field tints on hover but doesn't ripple. Both colours come from the theme, so a brand tint is one prop:
```tsx live
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';
const frameworks = [
'Expo',
'React Native CLI',
'React Native Web',
'Next.js',
'Remix',
'Vite',
].map((f) => ({ label: f, value: f }));
export default function Example() {
const [framework, setFramework] = useState(null);
return (
);
}
```
See [Theming → Hover and ripple](../guides/theming#hover-and-ripple) to change them everywhere or turn them off.
### Custom trigger
`renderTrigger` replaces the field. Call `open()` to show the sheet:
```tsx live
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
import { SelectPicker } from '@elik56a/react-native-pickers';
const sorts = [
{ label: 'Most relevant', value: 'relevance' },
{ label: 'Newest first', value: 'newest' },
{ label: 'Price: low to high', value: 'price_asc' },
{ label: 'Price: high to low', value: 'price_desc' },
];
export default function Example() {
const [sort, setSort] = useState('relevance');
return (
(
↕ {selected[0]?.label ?? 'Sort'}
)}
/>
);
}
```
### Custom chevron
The field's chevron rotates when it opens and closes. `renderChevron` swaps the icon and keeps the rotation, so any icon works, an SVG included:
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { SelectPicker } from '@elik56a/react-native-pickers';
const sizes = ['Small', 'Medium', 'Large'].map((s) => ({ label: s, value: s }));
export default function Example() {
const [size, setSize] = useState(null);
return (
(
⌄
)}
/>
);
}
```
### Custom options
`renderOption` replaces each row's content (taps, selection background and accessibility are still handled). `renderGroupHeader` replaces section headers.
```tsx live
import { useState } from 'react';
import { View, Text } from 'react-native';
import { SelectPicker } from '@elik56a/react-native-pickers';
const people = [
{
label: 'Ada Lovelace',
value: 'ada',
description: 'Mathematician',
color: '#F59E0B',
},
{
label: 'Alan Turing',
value: 'alan',
description: 'Computer scientist',
color: '#10B981',
},
{
label: 'Grace Hopper',
value: 'grace',
description: 'Rear admiral',
color: '#6366F1',
},
];
export default function Example() {
const [who, setWho] = useState(null);
return (
{
const p = people.find((x) => x.value === option.value)!;
return (
{p.label[0]}
{p.label}
{p.description}
{selected && ●}
);
}}
/>
);
}
```
### Presentation
`presentation` picks how the options open:
- `'auto'` (default): a dropdown on the web, a bottom sheet on native.
- `'bottomSheet'`: always a bottom sheet.
- `'modal'`: always a centered modal.
- `'dropdown'`: a popover under the field, as wide as the field. It flips above the field when there's more room there, and a tap outside closes it.
```tsx live
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [value, setValue] = useState(null);
return (
({
label,
value: label,
}))}
/>
);
}
```
### Sheet options
Customise the sheet through `sheet`, which takes [`PickerSheet`](./picker-sheets#pickersheet) props such as `confirmLabel` and `sheetProps` (passed through to [`BottomSheet`](./bottom-sheet)):
```tsx
```
#### Expanding the sheet
Long lists open at their content height and can grow to full screen: drag the header up on iOS and Android, or scroll the wheel up over the sheet on the web. The list grows with the sheet. Set the expanded height through `sheet.sheetProps`:
```tsx live
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';
const countries = [
'Argentina',
'Australia',
'Austria',
'Belgium',
'Brazil',
'Canada',
'Chile',
'China',
'Denmark',
'Egypt',
'Finland',
'France',
'Germany',
'Greece',
'India',
'Ireland',
'Israel',
'Italy',
'Japan',
'Mexico',
'Netherlands',
'Norway',
'Poland',
'Portugal',
'Spain',
'Sweden',
'Switzerland',
'Turkey',
'United Kingdom',
'United States',
].map((c) => ({ label: c, value: c }));
export default function Example() {
const [country, setCountry] = useState(null);
return (
);
}
```
`expandable: false` keeps the sheet at its content height; `draggable: false` turns off dragging on native. See [BottomSheet](./bottom-sheet#expanding-to-full-screen) for all the options.
### Props
#### Value
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `ReadonlyArray>` | `[]` | The options; with `loadOptions`, the ones shown before the user types. |
| `multiple` | `boolean` | `false` | Multi-select mode. |
| `value` | `T \| null` · `T[]` | — | Selected value(s). |
| `onChange` | `(value: T, option: SelectOption) => void` · `(values: T[], options: SelectOption[]) => void` | — | Single · multiple. |
| `maxSelected` | `number` | — | Multiple only: maximum selected options. |
| `isEqual` | `(a: T, b: T) => boolean` | `Object.is` | Value equality. |
#### Field
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | — | Label above the field; also the default sheet title. |
| `placeholder` | `string` | `'Select…'` | Shown when nothing is selected. |
| `disabled` | `boolean` | `false` | Disable the field. |
| `clearable` | `boolean` | `false` | Show a × button when something is selected. |
| `onClear` | `() => void` | — | Called by ×. In single mode, reset `value` here. |
| `chips` | `boolean` | `true` | Multiple: show selections as removable chips. |
| `renderChevron` | `(info: { open, theme }) => ReactNode` | — | Custom chevron icon (an SVG, an icon font…). It still rotates on open and close. |
| `renderTrigger` | `(info: { open, selected, disabled }) => ReactNode` | — | Replace the field entirely. |
| `style` | `StyleProp` | — | Wrapper style (label + field). |
#### Sheet & list
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `presentation` | `'auto' \| 'bottomSheet' \| 'modal' \| 'dropdown'` | `'auto'` | How the options open. `auto`: dropdown on the web, bottom sheet on native. |
| `sheetTitle` | `string` | `label` | Sheet header title. |
| `closeOnSelect` | `boolean` | `true` | Single: close on tap. `false` shows a Done button. |
| `sheet` | `Partial` | — | `confirmLabel`, `cancelLabel`, `showCancel`, `title`, `sheetProps`. |
| `onOpen` / `onClose` | `() => void` | — | Sheet open / close. |
| `searchable` | `boolean` | dropdown: `true` · sheet: `options.length > 8` | Dropdown: type in the field to filter, or in a search field atop the list with `renderTrigger`. Sheet: show the search field. |
| `searchPlaceholder` | `string` | `'Search'` | Search hint. |
| `filterOption` | `(option, query) => boolean` | `defaultFilter`; none with `loadOptions` / `onSearchChange` | Custom search. |
| `loadOptions` | `(query: string) => Promise[]>` | — | [Search a server](#search-on-a-server): loader, debounce and stale answers handled. An empty query shows `options`. |
| `onSearchChange` | `(query: string) => void` | — | Called once typing pauses, to [search yourself](#searching-yourself). |
| `searchPrompt` | `string` | `'Type to search'` | Shown before the user types, while a searched list has no options. |
| `searchDebounce` | `number` | `300` | ms after the last keystroke before `loadOptions` / `onSearchChange`. |
| `onEndReached` | `() => void` | — | Called near the end of the list, to load more. |
| `loading` | `boolean` | `false` | Spinner in place of an empty list, or under the options. |
| `loadingText` | `string` | `'Loading…'` | Text next to the spinner. |
| `renderLoading` | `(theme) => ReactNode` | — | Custom loader. |
| `emptyText` | `string` | `'No results'` | Shown when nothing matches. |
| `showCheckmarks` | `boolean` | `true` | Check marks / checkboxes on rows. |
| `renderOption` | `(info: { option, selected, disabled, theme }) => ReactNode` | — | Custom row content. |
| `renderGroupHeader` | `(group: string, theme) => ReactNode` | — | Custom section header. |
| `listHeight` | `number` | grows with content | Fixed list height. |
| `autoFocusSearch` | `boolean` | `false` | Focus search when the sheet opens. |
| `styles` | `SlotStyles` | — | Slots for both the field and the list. |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID on the list. |
#### Slots
**Field** (`SelectTriggerSlot`):
| Slot | Element |
| --- | --- |
| `label` | Label text above the field |
| `trigger` | The field box |
| `triggerText` | Selected label(s) |
| `placeholder` | Placeholder text |
| `chip` / `chipText` | Multi-select chips |
| `chevron` | The drawn chevron (a bordered `View`: `borderColor` sets its color, `width` / `height` its size) |
| `clear` | × button text |
**List**: see [SelectList slots](./select-list#slots).
### Accessibility
The field is a button labelled with `label` (or the placeholder), with the current selection as its hint and an `expanded` state. Options are `button`s (single) or `checkbox`es (multiple) with `selected` / `checked` / `disabled` states, and chips announce `Remove {label}`.
### Related
- [SelectList](./select-list): the list without the field and sheet
- [Picker fields](./picker-fields): the same field for dates, times, wheels and emoji
- [`CountryPicker`](./country-picker): this field with about 250 countries built in, and the [country and phone number recipe](../recipes/country-select)
- [Tag picker recipe](../recipes/tags)
- [Search a server recipe](../recipes/server-search): loading, debounced search and load-more
- [Flight search form recipe](../recipes/flight-search-form): a full form with airports searched on a server
---
## React Native Time Picker
## TimePicker
`TimePicker` is a React Native time picker for hours, minutes and optionally seconds and AM/PM. It looks native on each platform: [wheels on iOS, a Material clock dial on Android, columns to click on the web](#wheels-a-dial-or-a-list), or the one you pick with `display`. The 12h or 24h clock and the AM/PM labels come from the locale automatically.
Picking a length of time, such as a timer or a workout, rather than a time of day? Use [`DurationPicker`](./duration-picker).
```tsx
import { TimePicker } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { TimePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [time, setTime] = useState(new Date());
return (
<>
{time.toLocaleTimeString()}
>
);
}
```
### Try the props
### The value is a `Date`
`value` and `onChange` use a JavaScript `Date`. Only the hours, minutes and seconds are changed; **the date part is preserved**, so you can pass a full appointment `Date` and get the same day back with a new time. When `showSeconds` is off, seconds and milliseconds are set to `0`.
### 12h / 24h
`format` is `'auto'` by default and is resolved from `locale` (or the device locale). Force it with `'12h'` or `'24h'`.
```tsx live
import { useState } from 'react';
import { View, Text } from 'react-native';
import { TimePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [time, setTime] = useState(new Date(2026, 0, 1, 14, 45));
return (
en-US (12h)de-DE (24h)
);
}
```
AM/PM labels come from the locale too (`AM`/`PM`, `a.m.`/`p.m.`, `午前`/`午後`…). Override them with `dayPeriodLabels`, and use `dayPeriodFirst` for languages that put the period before the hour (Chinese, Korean, Japanese):
```tsx live
import { useState } from 'react';
import { TimePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [time, setTime] = useState(new Date(2026, 0, 1, 9, 30));
return (
);
}
```
Hours are zero-padded in 24h mode (`09`) and not in 12h mode (`9`); change that with `padHours`.
### Intervals
`minuteInterval` shows only every n-th minute (5, 10, 15, 30…). `showSeconds` adds a seconds wheel with its own `secondInterval`. A value whose minutes don't fall on the interval is shown rounded **down** (10:07 with a 5-minute step shows 10:05). `TimePickerSheet` and `DateTimePickerSheet` confirm the time as shown, so pressing **Done** without scrolling returns 10:05.
```tsx live
import { useState } from 'react';
import { TimePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [time, setTime] = useState(new Date(2026, 0, 1, 0, 2, 30));
return (
);
}
```
### Min and max time
`minTime` and `maxTime` limit the selectable **time of day** (their date part is ignored). Hours and minutes outside the range are disabled, and a combination that ends up out of range (for example, changing the hour while sitting on a late minute) is clamped into the range when committed.
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { TimePicker } from '@elik56a/react-native-pickers';
const at = (h: number, m = 0) => new Date(2026, 0, 1, h, m);
export default function Example() {
const [time, setTime] = useState(at(12));
return (
<>
Opening hours: 09:00 – 17:30
>
);
}
```
### Wheels, a dial or a list
Each [design](../guides/designs) has its own default:
| `display` | Default in | What it is |
| --- | --- | --- |
| `'wheel'` | iOS | Hour, minute and AM/PM wheels |
| `'dial'` | Material | The time in large type above a clock dial. Tap or drag to the hour, lift, then the minutes; tap the hours or minutes above to switch. 24h dials have 1–12 outside and 00, 13–23 on an inner ring. The keyboard button under the dial switches to typing the hours and minutes |
| `'input'` | | Material's time input: the hour and minute fields to type into, without the dial. Compact, so `DateTimePicker` uses it under a Material calendar |
| `'list'` | web | Hours, minutes (and seconds) and AM/PM side by side in a bordered panel, each a column of cells to click, the chosen one kept at the top |
```tsx
```
A dial or time input has no seconds, so with `showSeconds` they show wheels instead. Screen readers adjust the dial up and down by an hour, or by `minuteInterval`.
### Looping
Hour, minute and second wheels loop by default. Set `loop={false}` for wheels that stop at the ends. The AM/PM wheel never loops.
### Uncontrolled
```tsx
save(d)} />
```
Without `value` or `defaultValue`, the picker starts at the current time.
### In a sheet
```tsx
import { TimePickerSheet } from '@elik56a/react-native-pickers';
setOpen(false)} title="Alarm" value={time} onConfirm={setTime} minuteInterval={5} />
```
→ [TimePickerSheet](./picker-sheets#timepickersheet)
### In a form field
`TimePickerField` is a field that opens the sheet and shows the confirmed time (`9:30 AM` or `09:30`, from `format` and `locale`). Switch `presentation` in the panel: `auto` opens a dropdown under the field on the web and a bottom sheet on iOS and Android.
→ [Picker fields](./picker-fields)
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Date` | — | Selected time (controlled). The date part is preserved. |
| `defaultValue` | `Date` | now | Initial time when uncontrolled. |
| `onChange` | `(date: Date) => void` | — | Fires when any wheel settles. Receives a **new** `Date`. |
| `onValueChanging` | `(date: Date) => void` | — | Fires on every tick while scrolling (haptics). |
| `format` | `'12h' \| '24h' \| 'auto'` | `'auto'` | Clock format; `'auto'` reads it from `locale`. |
| `locale` | `string` | device | BCP 47 locale, e.g. `'en-US'`, `'he-IL'`. |
| `display` | `'wheel' \| 'dial' \| 'input' \| 'list'` | from the design | Wheels (iOS), a clock dial (Material), typed fields, or columns to click (web). |
| `minuteInterval` | `number` | `1` | Minute step: 1, 5, 10, 15, 30… |
| `showSeconds` | `boolean` | `false` | Add a seconds wheel. |
| `secondInterval` | `number` | `1` | Second step. |
| `minTime` | `Date` | — | Earliest selectable time of day. |
| `maxTime` | `Date` | — | Latest selectable time of day. |
| `loop` | `boolean` | `true` | Loop the hour / minute / second wheels. |
| `padHours` | `boolean` | `true` for 24h, `false` for 12h | Zero-pad the hour labels. |
| `dayPeriodLabels` | `[string, string]` | from locale | `[am, pm]` labels. |
| `dayPeriodFirst` | `boolean` | `false` | Put the AM/PM wheel before the hours. |
| `separator` | `ReactNode` | `':'` | Shown between hours, minutes and seconds (not next to AM/PM). |
| `itemHeight` | `number` | `40` | Row height. |
| `visibleItems` | `number` | `5` | Visible rows. |
| `variant` | `'3d' \| 'flat'` | `'3d'` | Wheel style. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | [MultiWheelPicker slots](./multi-wheel-picker#slots). |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. |
### Related
- [Alarm recipe](../recipes/alarm-time)
- [TimePickerField](./picker-fields): the picker behind a form field
- [DurationPicker](./duration-picker): for a length of time rather than a time of day
---
## React Native Time Zone Picker
## TimeZonePicker
`TimeZonePicker` is a React Native form field for choosing a time zone: all 418 IANA zones, each with its city, its offset from GMT and, where the JavaScript engine has it, its name, such as *Central European Summer Time*. The list is sorted by offset, with the device's own zone on top. It's a [`SelectPicker`](./select-picker) with the list built in, so it takes the same props: a bottom sheet on phones, a dropdown on the web, multiple selection, a clear button and the [iOS, Material or web design](../guides/designs).
The value is the IANA name, such as `'Europe/Lisbon'`, ready to store or pass to `Intl.DateTimeFormat`.
```tsx
import { TimeZonePicker } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { TimeZonePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [timeZone, setTimeZone] = useState('Europe/Lisbon');
return ;
}
```
Try searching `tokyo`, `utc+5:30`, `pacific` or `America/New_York`.
### Try the props
### Offsets and daylight saving
Offsets are worked out for the current moment, so a zone with daylight saving shows its summer offset in summer and its winter one in winter. To show them for another moment, such as the date of a meeting being scheduled, pass `date`.
```tsx live
import { useState } from 'react';
import { TimeZonePicker } from '@elik56a/react-native-pickers';
const MEETING = new Date(2027, 0, 15, 15, 0);
export default function Example() {
const [timeZone, setTimeZone] = useState(null);
return ;
}
```
### The device's zone and preferred zones
The device's zone is shown first, under *Your time zone*. Pass `preferred` to pin others instead, in the order you give, or an empty array to pin none. `timeZones` limits the list to the zones you support.
```tsx live
import { useState } from 'react';
import { TimeZonePicker } from '@elik56a/react-native-pickers';
const OFFICES = ['America/New_York', 'Europe/London', 'Asia/Singapore'];
export default function Example() {
const [timeZone, setTimeZone] = useState(null);
return (
);
}
```
### Old and new zone names
Some zones were renamed: `Asia/Calcutta` is now `Asia/Kolkata`, `Europe/Kiev` is `Europe/Kyiv`. Phones report the new names, while some browsers still report the old ones. The picker accepts either as `value` and always reports the new one, so a value saved on one device shows on another. [`normalizeTimeZone`](#utilities) does the same for your own data.
### On Hermes
Hermes, React Native's default engine, can't list time zones, so the list ships with the library (8 KB, read on first use). Offsets come from `Intl.DateTimeFormat`, which Hermes has. Zone names such as *India Standard Time* show where the engine provides them; where it doesn't, each zone shows its region instead, as *GMT+5:30 · Asia*.
### Utilities
| Export | Description |
| --- | --- |
| `getDeviceTimeZone()` | The device's zone, e.g. `'Europe/Lisbon'`, or `undefined` |
| `getTimeZoneOffset(id, date?)` | Minutes ahead of UTC at `date` (default: now), e.g. `330` for `'Asia/Kolkata'` |
| `formatGmtOffset(minutes)` | `330` → `'GMT+5:30'`, `0` → `'GMT'` |
| `getTimeZoneName(id, locale?, date?)` | `'Central European Summer Time'`, where the engine has names |
| `normalizeTimeZone(id)` | `'Asia/Calcutta'` → `'Asia/Kolkata'` |
| `getTimeZone(id)` | `{ id, alias?, city, region }` |
| `getAllTimeZones()` | Every zone, sorted by name |
```ts
const zone = getDeviceTimeZone() ?? 'UTC';
const label = `${getTimeZone(zone)?.city} (${formatGmtOffset(getTimeZoneOffset(zone) ?? 0)})`; // 'Lisbon (GMT+1)'
```
### Props
Every [`SelectPicker` prop](./select-picker#props) works, apart from the server search ones (`options`, `loadOptions`, `onSearchChange`, `loading` and the rest): the list is built in. `value` is an IANA name, or an array of them with `multiple`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `timeZones` | `string[]` | all | Only these zones. |
| `preferred` | `string[]` | the device's zone | Shown first, in this order, under their own header. `[]` pins none. |
| `preferredLabel` | `string` | `'Your time zone'` | Header above `preferred`. |
| `allTimeZonesLabel` | `string` | `'All time zones'` | Header above the rest. |
| `date` | `Date` | now | The moment offsets are worked out for. |
| `locale` | `string` | device locale | Language of the zone names, where the engine has them. |
| `placeholder` | `string` | `'Select a time zone'` | Shown while nothing is selected. |
| `searchPlaceholder` | `string` | `'Search time zones'` | Search field placeholder. |
| `sheet` | `SheetChromeProps` | `sheetProps.height: 0.8` | Sheet options. The sheet opens at 80% of the screen so it doesn't jump as search narrows the list. |
---
## React Native Week Strip
## WeekStrip
`WeekStrip` is one week of days in a row, the compact date picker at the top of booking, schedule, habit and fitness screens. Tap a day to pick it; swipe or use the arrows for the next or previous week. The month and year most of the week falls in sit above it.
It's the [`Calendar`](./date-picker)'s day cells in a single week, so it follows the [design](../guides/designs) the same way and takes the same `markedDates`, `minDate`, `maxDate`, `isDateDisabled`, `renderDay` and styles.
```tsx
import { WeekStrip } from '@elik56a/react-native-pickers';
```
```tsx live
import { useState } from 'react';
import { Text } from 'react-native';
import { WeekStrip } from '@elik56a/react-native-pickers';
export default function Example() {
const [day, setDay] = useState(new Date());
return (
<>
{day.toDateString()}
>
);
}
```
### Try the props
### Marked and disabled days
`markedDates` adds dots, a label or a disabled flag to days, keyed by `YYYY-MM-DD` ([`toDateKey`](../api/date-utils#todatekey)), as on the calendar. `minDate`, `maxDate` and `isDateDisabled` disable days, and the arrows stop at the weeks holding `minDate` and `maxDate`.
```tsx live
import { useState } from 'react';
import { WeekStrip, addDays, toDateKey } from '@elik56a/react-native-pickers';
const today = new Date();
const CLASSES = {
[toDateKey(today)]: { dots: ['#E4572E'] },
[toDateKey(addDays(today, 2))]: { dots: ['#E4572E', '#2E86AB'] },
[toDateKey(addDays(today, 4))]: { dots: ['#2E86AB'] },
};
export default function Schedule() {
const [day, setDay] = useState(today);
return (
date.getDay() === 0}
/>
);
}
```
### Weeks
The first day of the week comes from `locale` (Sunday in the US, Monday in most of Europe), or set `firstDayOfWeek`: `0` for Sunday to `6` for Saturday. The strip opens on the week of `value`, or of today, or of `initialWeek`, and moves to a new `value`'s week when you set one from outside.
`onWeekChange` fires with the first day of the week after an arrow or a swipe, to load that week's data:
```tsx
loadWeek(start)} />
```
`showHeader={false}` hides the title and arrows, leaving the days and the swipe. `formatTitle(weekStart, locale)` changes the title.
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Date \| null` | — | Selected day (controlled). |
| `defaultValue` | `Date \| null` | — | Initial day when uncontrolled. |
| `onChange` | `(date: Date) => void` | — | Fires when a day is tapped. |
| `initialWeek` | `Date` | the selected day or today | A day in the week shown first. |
| `onWeekChange` | `(weekStart: Date) => void` | — | Fires with the first day of the week after paging. |
| `firstDayOfWeek` | `0`–`6` | from `locale`, else `0` | `0` = Sunday. |
| `locale` | `string` | device | Weekday and month names. |
| `minDate` / `maxDate` | `Date` | — | Days outside are disabled, and paging stops at their weeks. |
| `isDateDisabled` | `(date: Date) => boolean` | — | Disable a day. |
| `markedDates` | `Record` | — | Dots, labels or disabled days, keyed by `YYYY-MM-DD`. |
| `showHeader` | `boolean` | `true` | Show the month title and the week arrows. |
| `formatTitle` | `(weekStart: Date, locale?: string) => string` | the month of the week's middle day | Title text. |
| `showWeekdays` | `boolean` | `true` | Show the weekday names. |
| `weekdayFormat` | `'short' \| 'narrow'` | from the design | Weekday names: `Mon` or `M`. |
| `enableSwipe` | `boolean` | `true` | Swipe left or right to change weeks. |
| `renderDay` | `(state: DayState, theme: PickerTheme) => ReactNode` | — | Fully custom day cell, as on the [calendar](./date-picker#custom-days-and-arrows). |
| `renderArrow` | `(direction, disabled) => ReactNode` | — | Custom arrows. |
| `dayHeight` | `number` | `44` | Day cell height. |
| `style` | `StyleProp` | — | Container style. |
| `styles` | `SlotStyles` | — | The [calendar slots](./date-picker#slots). |
| `theme` | `DeepPartial` | — | Theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `testID` | `string` | — | Test ID. |
### Related
- [DatePicker](./date-picker): a whole month
- [MonthPicker](./month-picker): a month and year
---
## React Native Wheel Picker
## WheelPicker
`WheelPicker` is an iOS-style scrolling wheel for React Native. Rows curve away on a 3D cylinder (or stay flat), snap into the selection band, and can loop forever. Works the same on iOS, Android and web.
```tsx
import { WheelPicker } from '@elik56a/react-native-pickers';
```
```tsx live noplatforms
import { useState } from 'react';
import { WheelPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [size, setSize] = useState('M');
return (
);
}
```
### Try the props
### Items
`items` accepts **primitives** (strings or numbers) or **objects**. Mix freely, but objects are the way to add labels, disabled rows or non-primitive values.
```ts
type WheelItem = {
label: string; // text shown on the row
value: T; // what onChange gives you
disabled?: boolean; // rendered dimmed; the wheel never settles on it
key?: string;
};
items: ReadonlyArray | T> // T must be string | number for the shorthand
```
```tsx live noplatforms
import { useState } from 'react';
import { Text } from 'react-native';
import { WheelPicker } from '@elik56a/react-native-pickers';
const plans = [
{ label: 'Free', value: { id: 'free', price: 0 } },
{ label: 'Pro — $9', value: { id: 'pro', price: 9 } },
{ label: 'Team — $29', value: { id: 'team', price: 29 } },
{
label: 'Enterprise (contact us)',
value: { id: 'ent', price: -1 },
disabled: true,
},
];
export default function Example() {
const [plan, setPlan] = useState(plans[1].value);
return (
<>
a.id === b.id}
/>
Selected: {plan.id}
>
);
}
```
:::info Object values need `isEqual`
Selection is matched with `Object.is` by default. If your values are objects that may be re-created between renders, pass `isEqual` so the wheel can find the selected row.
:::
### Infinite loop
`loop` makes the wheel wrap around: scroll past the last item to reach the first. Internally the list is repeated and silently re-centred when it settles, so the user can never hit an end.
```tsx live noplatforms
import { useState } from 'react';
import { WheelPicker } from '@elik56a/react-native-pickers';
const minutes = Array.from({ length: 60 }, (_, i) => ({
label: String(i).padStart(2, '0'),
value: i,
}));
export default function Example() {
const [minute, setMinute] = useState(30);
return (
);
}
```
### Disabled items
Disabled rows are shown dimmed and are never selected. If the user lets go on one, the wheel springs to the nearest enabled row. If the initial `value` points at a disabled row, the nearest enabled row is shown.
```tsx live noplatforms
import { useState } from 'react';
import { WheelPicker } from '@elik56a/react-native-pickers';
const slots = [
'09:00',
'09:30',
'10:00',
'10:30',
'11:00',
'11:30',
'12:00',
].map((t, i) => ({
label: i % 3 === 1 ? `${t} · booked` : t,
value: t,
disabled: i % 3 === 1,
}));
export default function Example() {
const [slot, setSlot] = useState('10:00');
return ;
}
```
### Flat variant and the 3D look
`variant="flat"` turns off the cylinder; rows just fade and scale slightly. For the 3D wheel, `rotation` sets how sharply it curves (degrees between rows) and `fadeTo` sets the opacity of the outermost rows.
```tsx live noplatforms
import { useState } from 'react';
import { View } from 'react-native';
import { WheelPicker } from '@elik56a/react-native-pickers';
const items = Array.from({ length: 20 }, (_, i) => i + 1);
export default function Example() {
const [a, setA] = useState(5);
const [b, setB] = useState(5);
const [c, setC] = useState(5);
return (
);
}
```
### Size: `itemHeight` and `visibleItems`
The wheel is exactly `itemHeight × visibleItems` tall. `visibleItems` should be odd so one row sits in the middle; an even number is rounded up.
```tsx live noplatforms
import { useState } from 'react';
import { WheelPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [v, setV] = useState('Medium');
return (
);
}
```
### Tap-to-type
With `keyboardInput` (on by default), tapping the **selected** row opens the keyboard. As the user types, the wheel jumps to the first matching label, and the value is committed when the wheel settles, exactly as if they had scrolled.
- **Numeric wheels** (every label is digits) match by number, so typing `5` finds `05`, and the numeric keyboard is shown.
- **Text wheels** match by case-insensitive prefix (`ju` → `June`).
- Typing continues while more characters could still narrow the match, then the keyboard closes.
Tap the selected row below, then type:
```tsx live noplatforms
import { useState } from 'react';
import { WheelPicker } from '@elik56a/react-native-pickers';
const years = Array.from({ length: 101 }, (_, i) => 1930 + i);
export default function Example() {
const [year, setYear] = useState(1990);
return ;
}
```
Set `keyboardInput={false}` to disable it. Tapping a non-selected row always scrolls to that row.
### Custom rows
`renderItem` replaces the row content. You get the item, its index, whether it's selected and the active theme.
```tsx live noplatforms
import { useState } from 'react';
import { View, Text } from 'react-native';
import { WheelPicker } from '@elik56a/react-native-pickers';
const currencies = [
{ label: 'USD', value: 'USD', flag: '🇺🇸', name: 'US Dollar' },
{ label: 'EUR', value: 'EUR', flag: '🇪🇺', name: 'Euro' },
{ label: 'GBP', value: 'GBP', flag: '🇬🇧', name: 'Pound' },
{ label: 'JPY', value: 'JPY', flag: '🇯🇵', name: 'Yen' },
{ label: 'ILS', value: 'ILS', flag: '🇮🇱', name: 'Shekel' },
];
export default function Example() {
const [cur, setCur] = useState('EUR');
return (
{
const c = currencies.find((x) => x.value === item.value)!;
return (
{c.flag}
{c.label}
{selected && (
{c.name}
)}
);
}}
/>
);
}
```
`renderIndicator` replaces the selection band (the grey pill behind the selected row), and `showIndicator={false}` removes it:
```tsx live noplatforms
import { useState } from 'react';
import { View } from 'react-native';
import { WheelPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [v, setV] = useState(3);
return (
(
)}
/>
);
}
```
### Haptics
`onValueChanging` fires on every row crossing, the perfect hook for a selection tick. The library imposes no haptics dependency; use whichever you like:
```tsx
import * as Haptics from 'expo-haptics';
Haptics.selectionAsync()} />
```
→ [Haptics guide](../guides/haptics)
### In a form field
`WheelPickerField` opens the wheel in a sheet from a field, and shows the confirmed item's `label`. Switch `presentation` in the panel: `auto` opens a dropdown under the field on the web and a bottom sheet on iOS and Android.
→ [Picker fields](./picker-fields)
### Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `ReadonlyArray \| T>` | **required** | Rows. Primitives (`string \| number`) or `{ label, value, disabled?, key? }`. |
| `value` | `T` | — | Selected value (controlled). Changing it scrolls the wheel without firing `onChange`. |
| `defaultValue` | `T` | first enabled item | Initial value when uncontrolled. |
| `onChange` | `(value: T, index: number) => void` | — | Fires once the wheel settles on a **new** value. `index` is the position in `items`. |
| `onValueChanging` | `(value: T, index: number) => void` | — | Fires on every row crossing while scrolling (skips disabled rows). Use for haptics. |
| `itemHeight` | `number` | `40` | Row height in px. |
| `visibleItems` | `number` | `5` | Visible rows. Odd numbers; even values are rounded up. |
| `loop` | `boolean` | `false` | Infinite wheel that wraps around. |
| `keyboardInput` | `boolean` | `true` | Tap the selected row to type a value. |
| `variant` | `'3d' \| 'flat'` | `'3d'` | Cylinder or flat list. |
| `rotation` | `number` | `20` | Degrees between rows on the 3D cylinder. Higher = sharper curve. |
| `fadeTo` | `number` | `0.3` | Opacity of the farthest visible rows (0–1). |
| `renderItem` | `(info: WheelRenderItemInfo) => ReactNode` | — | Custom row content. `info = { item, index, selected, theme }`. |
| `showIndicator` | `boolean` | `true` | Show the selection band. |
| `renderIndicator` | `(theme: PickerTheme) => ReactNode` | — | Custom selection band, vertically centred. |
| `textAlign` | `TextStyle['textAlign']` | `'center'` | Alignment of the default row label. |
| `disabled` | `boolean` | `false` | Disable scrolling and taps. |
| `isEqual` | `(a: T, b: T) => boolean` | `Object.is` | Equality used to find the selected row. |
| `style` | `StyleProp` | — | Outer container style (e.g. `flex`, `width`). |
| `styles` | `SlotStyles` | — | Per-slot styles; see below. |
| `theme` | `DeepPartial` | — | Component-level theme override. |
| `design` | `'ios' \| 'material' \| 'web'` | from `PickersProvider` | The look to follow, over the provider's: [iOS, Material or web](../guides/designs). |
| `accessibilityLabel` | `string` | — | Label announced by screen readers. |
| `testID` | `string` | — | Test ID on the container. |
#### Slots
| Slot | Element |
| --- | --- |
| `container` | Outer view (fixed height `itemHeight × visibleItems`) |
| `item` | Each row's animated wrapper |
| `itemText` | Every row label |
| `selectedItemText` | Label of the row in the band (merged after `itemText`) |
| `disabledItemText` | Labels of disabled rows |
| `indicator` | The selection band |
#### Types
```ts
interface WheelItem { label: string; value: T; disabled?: boolean; key?: string }
type WheelItemInput = WheelItem | (T extends string | number ? T : never);
type WheelPickerSlot = 'container' | 'item' | 'itemText' | 'selectedItemText' | 'disabledItemText' | 'indicator';
interface WheelRenderItemInfo { item: WheelItem; index: number; selected: boolean; theme: PickerTheme }
```
### Behaviour notes
- `onChange` fires only when the settled value **differs** from the last committed one. Scrolling away and back doesn't fire it.
- Scrolling with momentum fires `onChange` once, after momentum ends.
- A wheel with `loop` and very few items repeats the list enough times (at least 7 copies, about 240 rows) for smooth flinging.
- Rows are memoized; while scrolling only the two rows whose selected state changes re-render.
### Accessibility
The wheel is a single `adjustable` element. VoiceOver and TalkBack users swipe up / down to move one row (the `increment` / `decrement` actions), and the current label is announced as the value. Pass `accessibilityLabel` to name it ("Size").
### Related
- [MultiWheelPicker](./multi-wheel-picker): several wheels in one band
- [WheelPickerSheet](./picker-sheets#wheelpickersheet): the wheel in a bottom sheet
- [WheelPickerField](./picker-fields): the wheel behind a form field
---
## Date utilities
The helpers the pickers use internally, exported for your own code. All of them:
- work in **local time** (no UTC surprises)
- **never mutate** their inputs
- fall back to English names if `Intl` is missing or a locale is invalid
```ts
import { toDateKey, addDays, isSameDay, formatDate } from '@elik56a/react-native-pickers';
```
### Arithmetic
#### `startOfDay`
```ts
startOfDay(d: Date): Date
```
Same day at 00:00:00.000.
#### `startOfMonth`
```ts
startOfMonth(d: Date): Date
```
First day of the month at 00:00.
#### `daysInMonth`
```ts
daysInMonth(year: number, month: number): number // month is 0-based
daysInMonth(2028, 1) // 29
```
#### `addDays`
```ts
addDays(d: Date, days: number): Date
```
Adds calendar days (negative to subtract), keeping hours and minutes (seconds and milliseconds are reset). DST-safe, because it works on date parts rather than milliseconds.
#### `addMonths`
```ts
addMonths(d: Date, months: number): Date
addMonths(new Date(2026, 0, 31), 1) // Feb 28 2026 (clamped)
```
Clamps the day to the target month's length and keeps the time.
#### `clampDate`
```ts
clampDate(d: Date, min?: Date | null, max?: Date | null): Date
```
Returns a copy of `min` / `max` when `d` is outside them (by exact time), else `d`.
### Comparison
#### `isSameDay`
```ts
isSameDay(a?: Date | null, b?: Date | null): boolean
```
Same calendar day; `false` if either is null.
#### `isSameMonth`
```ts
isSameMonth(a: Date, b: Date): boolean
```
#### `compareDay`
```ts
compareDay(a: Date, b: Date): number // < 0, 0, > 0
```
Compares calendar days, ignoring time. Use it as a sort comparator: `dates.sort(compareDay)`.
#### `isBetweenDays`
```ts
isBetweenDays(d: Date, start?: Date | null, end?: Date | null): boolean
```
`true` when `d` is **strictly** between `start` and `end` (exclusive), by day.
#### `isOutOfRange`
```ts
isOutOfRange(d: Date, min?: Date | null, max?: Date | null): boolean
```
`true` when `d`'s day is before `min`'s day or after `max`'s day.
### Keys and grids
#### `toDateKey`
```ts
toDateKey(d: Date): string // 'YYYY-MM-DD' in local time
```
The key format of `markedDates`. Use it instead of `toISOString().slice(0, 10)`, which converts to UTC and can be off by a day.
#### `buildMonthGrid`
```ts
buildMonthGrid(year: number, month: number, firstDayOfWeek = 0, fixedWeeks = true): Date[][]
```
Weeks × 7 days for a month view, including leading and trailing days of neighbouring months. 6 weeks when `fixedWeeks`, else only as many as needed. Handy for custom calendars and heatmaps.
### Locale
#### `getMonthNames`
```ts
getMonthNames(locale?: string, width: 'long' | 'short' = 'long'): string[] // 12 names
```
#### `getWeekdayNames`
```ts
getWeekdayNames(locale?: string, width: 'long' | 'short' | 'narrow' = 'short'): string[]
```
7 names indexed by JS day: **0 = Sunday**.
#### `formatMonthYear`
```ts
formatMonthYear(d: Date, locale?: string): string // 'October 2026'
```
#### `formatDate`
```ts
formatDate(d: Date, locale?: string, opts: Intl.DateTimeFormatOptions = { dateStyle: 'medium' }): string
formatDate(new Date(2026, 9, 14)) // 'Oct 14, 2026'
formatDate(new Date(2026, 9, 14), 'de-DE', { dateStyle: 'full' }) // 'Mittwoch, 14. Oktober 2026'
```
Cached `Intl.DateTimeFormat`, falling back to `YYYY-MM-DD`.
#### `uses12HourClock`
```ts
uses12HourClock(locale?: string): boolean
```
`true` when the locale formats hours with AM/PM.
#### `getFirstDayOfWeek`
```ts
getFirstDayOfWeek(locale?: string): number // 0 = Sunday … 6 = Saturday
```
From `Intl.Locale` week info when the runtime supports it; otherwise `0`. Returns `0` when no `locale` is given.
#### `getDayPeriods`
```ts
getDayPeriods(locale?: string): [string, string] // ['AM', 'PM'], ['午前', '午後'], …
```
---
## Emoji utilities
The data and search behind `EmojiPicker`, for your own UIs.
```ts
import { getAllEmojis, searchEmojis, findEmoji, withSkinTone } from '@elik56a/react-native-pickers';
```
### `getAllEmojis`
```ts
getAllEmojis(): Emoji[]
```
All 1,923 emojis in display order. Parsed on the first call, then cached.
```ts
interface Emoji {
emoji: string;
name: string;
keywords: string[];
category: EmojiCategory;
version: number; // Emoji version, e.g. 0.6, 11, 15.1
skins?: string[]; // 5 variants, light → dark
}
```
### `searchEmojis`
```ts
searchEmojis(query: string, list: Emoji[] = getAllEmojis()): Emoji[]
```
Every word of the query must match. Results are ranked by:
1. name starts with the word (3 points)
2. a word of the name starts with it (2)
3. a keyword starts with it (1)
4. the name contains it (0.5)
Returns `[]` for an empty query.
```tsx live
import { useState } from 'react';
import { Text, TextInput, View } from 'react-native';
import { searchEmojis } from '@elik56a/react-native-pickers';
export default function Example() {
const [q, setQ] = useState('heart');
const results = searchEmojis(q).slice(0, 24);
return (
{results.map((e) => e.emoji).join(' ')}
);
}
```
### `findEmoji`
```ts
findEmoji(char: string): Emoji | undefined
```
Looks up an emoji by character. Matches skin-tone variants too (`findEmoji('👍🏽')` returns 👍's data) and ignores the U+FE0F variation selector.
### `withSkinTone`
```ts
withSkinTone(e: Emoji, tone: SkinTone): string
withSkinTone(findEmoji('👋')!, 3) // '👋🏽'
```
Returns the variant for tone 1–5, or the base emoji for tone 0 or emojis without skins.
### Constants
```ts
EMOJI_CATEGORIES: EmojiCategory[]
// ['smileys', 'people', 'animals', 'food', 'travel', 'activities', 'objects', 'symbols', 'flags']
DEFAULT_CATEGORY_LABELS: Record
// { recent: 'Recently used', search: 'Search results', smileys: 'Smileys & Emotion', … }
DEFAULT_CATEGORY_ICONS: Record
// { recent: '🕘', smileys: '😀', people: '👋', animals: '🐻', food: '🍔', travel: '✈️', activities: '⚽', objects: '💡', symbols: '❤️', flags: '🏳️' }
```
### Example: quick reactions
```tsx
const QUICK = ['👍', '❤️', '😂', '😮', '😢', '🙏'];
const [tone] = useState(2);
{QUICK.map((ch) => {
const e = findEmoji(ch)!;
const toned = withSkinTone(e, tone);
return react(toned)}>{toned};
})}
```
---
## API reference
Every export from `@elik56a/react-native-pickers`, grouped. Each name links to its full documentation.
### Components
| Export | Description |
| --- | --- |
| [`WheelPicker`](../components/wheel-picker) | Single scrolling wheel |
| [`MultiWheelPicker`](../components/multi-wheel-picker) | Several wheels in one band |
| [`TimePicker`](../components/time-picker) | Hour / minute / second / AM-PM wheels |
| [`DurationPicker`](../components/duration-picker) | A length of time in seconds: wheels, number boxes or columns |
| [`DatePicker`](../components/date-picker) | Calendar (`display="calendar"`) or wheel (`display="wheel"`) |
| [`Calendar`](../components/date-picker) | Month calendar: single / range / multiple / ranges |
| [`DateWheelPicker`](../components/date-wheel-picker) | Day / month / year wheels |
| [`MonthPicker`](../components/month-picker) | Month and year: wheels (iOS) or a grid of months (Material, web) |
| [`WeekStrip`](../components/week-strip) | One week of days in a row, swipe or arrows to page |
| [`DateTimePicker`](../components/date-time-picker) | Day + time wheels (`display="wheel"`) or calendar + time (`display="calendar"`) |
| [`SelectPicker`](../components/select-picker) | Field + searchable sheet |
| [`SelectList`](../components/select-list) | Inline searchable list |
| [`CountryPicker`](../components/country-picker) | Country field: flags, dialing codes, localized names |
| [`PhoneField`](../components/phone-field) | Phone number input: country code button, formats as you type, E.164 value |
| [`TimeZonePicker`](../components/time-zone-picker) | Time zone field: GMT offsets, sorted by offset, the device's zone on top |
| [`HorizontalList`](../components/horizontal-list) | Segmented control or chips that scroll sideways |
| [`EmojiPicker`](../components/emoji-picker) | Emoji keyboard |
| [`ColorPicker`](../components/color-picker) | iOS's tabs, Google's swatch circles or the browser's spectrum; hex with opacity |
| [`BottomSheet`](../components/bottom-sheet) | Animated sheet |
### Sheets
| Export | Description |
| --- | --- |
| [`PickerSheet`](../components/picker-sheets#pickersheet) | BottomSheet with Cancel / Done |
| [`DatePickerSheet`](../components/picker-sheets#datepickersheet) | Calendar or date wheel in a sheet |
| [`TimePickerSheet`](../components/picker-sheets#timepickersheet) | TimePicker in a sheet |
| [`DateTimePickerSheet`](../components/date-time-picker#in-a-sheet) | DateTimePicker in a sheet |
| [`DurationPickerSheet`](../components/duration-picker#in-a-sheet) | DurationPicker in a sheet |
| [`MonthPickerSheet`](../components/month-picker#in-a-sheet) | MonthPicker in a sheet |
| [`WheelPickerSheet`](../components/picker-sheets#wheelpickersheet) | WheelPicker in a sheet |
| [`EmojiPickerSheet`](../components/picker-sheets#emojipickersheet) | EmojiPicker in a sheet |
| [`ColorPickerSheet`](../components/color-picker#in-a-sheet) | ColorPicker in a sheet |
| [`useDraft(value, visible)`](../components/picker-sheets#usedraft) | Draft state that resets on open |
### Fields
| Export | Description |
| --- | --- |
| [`PickerField`](../components/picker-fields#your-own-sheet-pickerfield) | Labelled field that opens the sheet its child renders |
| [`DatePickerField`](../components/picker-fields) | Field + DatePickerSheet; shows the date, range or dates |
| [`TimePickerField`](../components/picker-fields) | Field + TimePickerSheet; shows the time |
| [`DateTimePickerField`](../components/picker-fields) | Field + DateTimePickerSheet; shows the date and time |
| [`DurationPickerField`](../components/duration-picker#in-a-form-field) | Field + DurationPickerSheet; shows `1 h 30 min` |
| [`MonthPickerField`](../components/month-picker#in-a-form-field) | Field + MonthPickerSheet; shows `October 2026` |
| [`WheelPickerField`](../components/picker-fields) | Field + WheelPickerSheet; shows the item's label |
| [`EmojiPickerField`](../components/picker-fields) | Field + EmojiPickerSheet; shows `value` |
| [`ColorPickerField`](../components/color-picker#in-a-form-field) | Field + ColorPickerSheet; shows a swatch and the hex code |
### Theme
| Export | Description |
| --- | --- |
| [`PickersProvider`](../guides/theming#pickersprovider) | App-wide theme, colour scheme, platform and design |
| [`usePickerTheme(override?)`](./theme#usepickertheme) | Resolved active theme |
| [`createTheme(overrides, base?)`](./theme#createtheme) | Full theme from overrides |
| [`mergeTheme(base, override?)`](./theme#mergetheme) | Deep merge |
| [`lightTheme`](./theme#lighttheme--darktheme) / [`darkTheme`](./theme#lighttheme--darktheme) | Built-in themes |
| [`DESIGN`](./theme#designs) / [`DESIGN_TOKENS`](./theme#designs) / [`resolveDesign(preference, platform)`](./theme#designs) | The iOS, Material and web designs |
| [`textStyle(theme, size, weight?)`](./theme#textstyle) | Text style from tokens |
### Date utilities
[`startOfDay`](./date-utils#startofday) · [`startOfMonth`](./date-utils#startofmonth) · [`daysInMonth`](./date-utils#daysinmonth) · [`addDays`](./date-utils#adddays) · [`addMonths`](./date-utils#addmonths) · [`isSameDay`](./date-utils#issameday) · [`isSameMonth`](./date-utils#issamemonth) · [`compareDay`](./date-utils#compareday) · [`isBetweenDays`](./date-utils#isbetweendays) · [`isOutOfRange`](./date-utils#isoutofrange) · [`clampDate`](./date-utils#clampdate) · [`toDateKey`](./date-utils#todatekey) · [`buildMonthGrid`](./date-utils#buildmonthgrid) · [`getMonthNames`](./date-utils#getmonthnames) · [`getWeekdayNames`](./date-utils#getweekdaynames) · [`formatMonthYear`](./date-utils#formatmonthyear) · [`formatDate`](./date-utils#formatdate) · [`uses12HourClock`](./date-utils#uses12hourclock) · [`getFirstDayOfWeek`](./date-utils#getfirstdayofweek) · [`getDayPeriods`](./date-utils#getdayperiods)
`CALENDAR_MODE`: the calendar selection modes as constants (`single`, `range`, `multiple`, `ranges`), e.g. `mode={CALENDAR_MODE.ranges}`.
### Emoji utilities
[`getAllEmojis`](./emoji-utils#getallemojis) · [`searchEmojis`](./emoji-utils#searchemojis) · [`findEmoji`](./emoji-utils#findemoji) · [`withSkinTone`](./emoji-utils#withskintone) · [`EMOJI_CATEGORIES`](./emoji-utils#constants) · [`DEFAULT_CATEGORY_LABELS`](./emoji-utils#constants) · [`DEFAULT_CATEGORY_ICONS`](./emoji-utils#constants)
### Color utilities
[`normalizeHex`](../components/color-picker#utilities) · [`hexToHsv`](../components/color-picker#utilities) · [`hsvToHex`](../components/color-picker#utilities) · [`withAlpha`](../components/color-picker#utilities) · [`DEFAULT_COLORS`](../components/color-picker#utilities)
### Country utilities
[`getCountry`](../components/country-picker#utilities) · [`getAllCountries`](../components/country-picker#utilities) · [`getCountryFlag`](../components/country-picker#utilities)
### Phone utilities
[`parsePhone`](../components/phone-field#utilities) · [`formatNationalNumber`](../components/phone-field#utilities) · [`isValidPhone`](../components/phone-field#utilities) · [`toE164`](../components/phone-field#utilities)
### Time zone utilities
[`getDeviceTimeZone`](../components/time-zone-picker#utilities) · [`getTimeZoneOffset`](../components/time-zone-picker#utilities) · [`formatGmtOffset`](../components/time-zone-picker#utilities) · [`getTimeZoneName`](../components/time-zone-picker#utilities) · [`normalizeTimeZone`](../components/time-zone-picker#utilities) · [`getTimeZone`](../components/time-zone-picker#utilities) · [`getAllTimeZones`](../components/time-zone-picker#utilities)
### Duration utilities
[`formatDuration(seconds, { showHours?, labels? })`](../components/duration-picker#formatduration): `5400` → `'1 h 30 min'`
### Select utilities
| Export | Description |
| --- | --- |
| [`defaultFilter(option, query)`](../components/select-picker#search) | The default search: case-, accent- and niqqud-insensitive; every word must match label, description or keywords |
### Types
See [TypeScript](../guides/typescript#exported-types) for the full list. The most used:
```ts
interface DateRange { start: Date | null; end: Date | null }
interface MarkedDate { dots?: string[]; disabled?: boolean; label?: string; textColor?: string }
interface SelectOption {
label: string; value: T; description?: string; disabled?: boolean;
icon?: React.ReactNode; group?: string; keywords?: string[];
}
interface WheelItem { label: string; value: T; disabled?: boolean; key?: string }
type SkinTone = 0 | 1 | 2 | 3 | 4 | 5;
/** Every sheet's `presentation`. */
type SheetPresentation = 'auto' | 'bottomSheet' | 'modal';
/** A field's `presentation`: a sheet presentation, or a dropdown under the field. Its `auto` (the default) is a dropdown on the web. */
type FieldPresentation = SheetPresentation | 'dropdown';
/** SelectPicker's `presentation`; its `auto` is a dropdown on the web. */
type SelectPresentation = FieldPresentation;
/** The field slots every `*PickerField` and `PickerField` takes in `styles`. */
type PickerFieldSlot = 'label' | 'trigger' | 'triggerText' | 'placeholder' | 'chevron' | 'clear';
type SlotStyles = Partial>>;
interface ThemableProps { theme?: DeepPartial }
```
### Default strings
| Where | Prop | Default |
| --- | --- | --- |
| Sheets | `confirmLabel` / `cancelLabel` | `Done` (iOS, web) · `OK` (Material) / `Cancel` |
| SelectPicker, fields | `placeholder` | `Select…` |
| SelectList | `searchPlaceholder` / `emptyText` / `loadingText` / `searchPrompt` | `Search` / `No results` / `Loading…` / `Type to search` |
| CountryPicker | `placeholder` / `searchPlaceholder` / `preferredLabel` / `allCountriesLabel` | `Select a country` / `Search countries` / `Suggested` / `All countries` |
| PhoneField | `label` / `countrySheetTitle` | none (read as `Phone number`) / `Country code` |
| TimeZonePicker | `placeholder` / `searchPlaceholder` / `preferredLabel` / `allTimeZonesLabel` | `Select a time zone` / `Search time zones` / `Your time zone` / `All time zones` |
| EmojiPicker | `searchPlaceholder` / `emptyText` | `Search emoji` / `No emoji found` |
| TimePicker | `dayPeriodLabels` | from locale (`AM` / `PM`) |
| ColorPicker | `labels` | `Grid`, `Spectrum`, `Sliders`, `Hue`, `Opacity`, `Red`, `Green`, `Blue`, `Hex`, `Copy`, `Copied`, `Custom colour`… |
| DurationPicker | `labels` | `{ hours: 'h', minutes: 'min', seconds: 's' }` |
### Default values at a glance
| Prop | Default | Components |
| --- | --- | --- |
| `itemHeight` | `40` | wheels |
| `visibleItems` | `5` | wheels |
| `variant` | `'3d'` | wheels |
| `rotation` / `fadeTo` | `20` / `0.3` | WheelPicker |
| `loop` | `false` WheelPicker · `true` TimePicker, DurationPicker, DateWheelPicker, MonthPicker | wheels |
| `keyboardInput` | `true` | WheelPicker |
| `minuteInterval` / `secondInterval` | `1` | TimePicker, DurationPicker |
| `format` | `'auto'` | TimePicker |
| `display` | `'wheel'` iOS · `'dial'` Material · `'list'` web | TimePicker |
| `display` | `'wheel'` iOS · `'input'` Material · `'list'` web | DurationPicker |
| `display` | `'wheel'` iOS · `'grid'` Material, web | MonthPicker |
| `display` | `'tabs'` iOS · `'swatches'` Material · `'spectrum'` web | ColorPicker |
| `display` / `timeDisplay` | `'wheel'` / `'wheel'` iOS · `'calendar'` / `'input'` Material · `'calendar'` / `'list'` web | DateTimePicker |
| `timePosition` | `'beside'` web · `'below'` other | DateTimePicker |
| `dayHeight` | `44` | Calendar |
| `fixedWeeks` / `showOutsideDays` | `true` | Calendar |
| `enableSwipe` / `enableQuickJump` | `true` | Calendar |
| `weekdayFormat` | `'short'` · `'narrow'` Material | Calendar |
| `monthFormat` | `'long'` | DateWheelPicker, MonthPicker |
| `searchable` | `options.length > 8` · `true` emoji | Select, Emoji |
| `closeOnSelect` | `true` | SelectPicker, EmojiPickerSheet |
| `chips` | `true` | SelectPicker |
| `clearable` / `disabled` | `false` | SelectPicker, fields |
| `columns` / `height` | `8` / `380` (`420` in sheet) | EmojiPicker |
| `maxRecent` | `24` | EmojiPicker |
| `maxHeightRatio` | `0.9` | BottomSheet |
| `bottomInset` | `24` iOS · `12` other | BottomSheet |
---
## Theme API
For concepts and the full token table, see the [Theming guide](../guides/theming).
### `PickersProvider`
```ts
function PickersProvider(props: {
children: React.ReactNode;
colorScheme?: 'auto' | 'light' | 'dark'; // default 'auto'
theme?: DeepPartial; // merged onto lightTheme (and onto darkTheme if `darkTheme` is omitted)
darkTheme?: DeepPartial; // merged onto darkTheme
platform?: PlatformOSType; // default Platform.OS
design?: 'auto' | 'ios' | 'material' | 'web'; // default 'auto'
}): JSX.Element;
```
Optional. Without a provider, components use `lightTheme` / `darkTheme` following `useColorScheme()`.
### `usePickerTheme`
```ts
function usePickerTheme(override?: DeepPartial): PickerTheme;
```
Returns the active theme: the design's tokens on the built-in light or dark theme, then the provider's overrides, then `override`. The design comes from `override.design`, the provider's `design`, or the platform. `theme.design` tells your own components which design to match.
### `createTheme`
```ts
function createTheme(overrides: DeepPartial, base?: PickerTheme): PickerTheme;
```
Builds a complete theme. Without `base`, starts from `darkTheme` if `overrides.dark` is `true`, else `lightTheme`.
### `mergeTheme`
```ts
function mergeTheme(base: T, override?: DeepPartial): T;
```
Deep merge without mutating. Plain objects merge recursively; arrays, functions and primitives in `override` replace the base value.
### `lightTheme` / `darkTheme`
```ts
const lightTheme: PickerTheme;
const darkTheme: PickerTheme;
```
The built-in themes. See [default values](../guides/theming#defaults).
### Designs
```ts
const DESIGN: { auto: 'auto'; ios: 'ios'; material: 'material'; web: 'web' };
const DESIGN_TOKENS: Record>; // what each design changes on the light or dark theme
function resolveDesign(preference: DesignPreference, platform: PlatformOSType): Design;
```
`resolveDesign('auto', platform)` gives `'ios'` on iOS, `'web'` on the web and `'material'` everywhere else. See [designs](../guides/designs).
### `textStyle`
```ts
function textStyle(
theme: PickerTheme,
size: 'xs' | 'sm' | 'md' | 'lg' | 'xl',
weight?: 'regular' | 'medium' | 'bold', // default 'regular'
): TextStyle; // { fontSize, fontWeight, fontFamily, color: theme.colors.text }
```
`medium` and `bold` use `typography.fontFamilyMedium`, falling back to `fontFamily`.
### Types
```ts
type DeepPartial = { [K in keyof T]?: T[K] extends object ? (T[K] extends Function ? T[K] : DeepPartial) : T[K] };
interface ThemableProps { theme?: DeepPartial; design?: Design }
type DesignPreference = 'auto' | 'ios' | 'material' | 'web';
type Design = 'ios' | 'material' | 'web';
```
---
## Accessibility
Accessibility is built in. Here's what each component exposes to VoiceOver (iOS), TalkBack (Android), screen readers on the web and the keyboard.
### Wheels
`WheelPicker` (and each column of `MultiWheelPicker`, `TimePicker` and `DateWheelPicker`) is a single element with:
| Property | Value |
| --- | --- |
| `accessibilityRole` | `adjustable` |
| `accessibilityValue` | the current label, e.g. `"45"` |
| `accessibilityActions` | `increment`, `decrement`: swipe up / down moves one row |
| `accessibilityLabel` | the `accessibilityLabel` prop |
Name your wheels, especially multi-column ones:
```tsx
```
### Calendar
- Each day is a `button` labelled with the **full date** in the calendar's locale ("Wednesday, 14 October 2026"), with `selected` and `disabled` states.
- The arrows are buttons labelled **Previous** and **Next**, with a `disabled` state at min / max.
- The month title is a button (quick jump) or a `header`.
- Quick-jump grid cells expose `selected` / `disabled`.
### Select
- The field is a `button` labelled with `label` (or the placeholder). Its hint lists the selected options, and it has an `expanded` state.
- Options are `button`s in single mode and `checkbox`es in multiple mode, with `selected`, `checked` and `disabled`.
- Chips are buttons labelled `Remove {label}`, and × is labelled **Clear**.
- The search input is labelled with `searchPlaceholder`.
- While `loading`, the loader is a `progressbar`.
### Horizontal list
The list is a `radiogroup` named by `accessibilityLabel`; each item is a `radio` labelled with its `label`, with `checked` and `disabled` states.
### Emoji
Every emoji cell is a button labelled with its name ("thumbs up"). Category tabs are `tab`s with a `selected` state, and the skin-tone button is labelled **Skin tone**.
### Sheets
The sheet is marked `accessibilityViewIsModal`, so focus stays inside it. The backdrop is a button labelled **Close**, string titles have the `header` role, and Android back closes the sheet.
### Keyboard
On the web, every picker works without a mouse:
| Key | What it does |
| --- | --- |
| Tab | Moves between fields, buttons, wheels and sliders |
| Enter / Space | Opens a field, picks the focused day, option or emoji |
| ↑↓ | On a wheel, steps one row. In a calendar, list or grid, moves to the cell above or below. From a search field, goes into the results, and back |
| ←→ | Moves to the next cell, wrapping to the next row, or to the next wheel of a date or time |
| Home / End | Jumps to the first or last cell, or to the ends of a wheel or slider |
| Esc | Closes a sheet or dropdown |
Arrows follow what's drawn, so they go the right way in RTL and in any custom layout. Colour sliders and the colour square move with the arrows, and the clock dial steps its hand. When a sheet or dropdown closes, focus goes back to the field that opened it.
### Your checklist
- Give every wheel an `accessibilityLabel`.
- Translate the English accessibility strings by passing translated `label` / `placeholder` / `searchPlaceholder` props.
- Keep custom `renderDay` / `renderOption` content as plain views and text. The library already provides the pressable and its accessibility props.
- Keep colour contrast when theming: `textSecondary` on `background` should meet WCAG AA (4.5:1) for body text.
---
## Forms (React Hook Form, Formik)
## Forms
Every picker is a controlled component, so it plugs into any form library. In a form you usually want a field that opens the picker: [`SelectPicker`](../components/select-picker) and the [picker fields](../components/picker-fields) (`DatePickerField`, `TimePickerField`, `DateTimePickerField`, `WheelPickerField`, `DurationPickerField`, `MonthPickerField`, `ColorPickerField`, `EmojiPickerField`) render one, and report the value through `onChange` (select) or `onConfirm` (fields). Their `onClose` is the natural place for `onBlur`.
### React Hook Form
```tsx
import { useForm, Controller } from 'react-hook-form';
import { Text, View, Button } from 'react-native';
import { SelectPicker, DatePickerField, TimePickerField } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
type FormValues = {
country: string | null;
tags: string[];
date: Date | null;
time: Date;
stay: DateRange;
};
export function BookingForm() {
const { control, handleSubmit, formState: { errors } } = useForm({
defaultValues: { country: null, tags: [], date: null, time: new Date(), stay: { start: null, end: null } },
});
return (
(
)}
/>
{errors.country && {errors.country.message}}
v.length > 0 || 'Pick at least one tag' }}
render={({ field }) => }
/>
(!!r.start && !!r.end) || 'Choose check-in and check-out' }}
render={({ field }) => (
)}
/>
{errors.stay && {errors.stay.message}}
}
/>
);
}
```
`field.onChange` receives the first argument, which is the value, so extra arguments such as the `option` from `SelectPicker` are ignored.
#### Options from your API
A field whose options come from a server takes `loadOptions` instead of a fixed list. It searches as the user types, with a loader, and the form only ever sees the picked value:
```tsx
(
api.searchUsers(query).then((users) => users.map((u) => ({ label: u.name, value: u.id })))}
value={field.value}
onChange={field.onChange}
onClose={field.onBlur}
/>
)}
/>
```
The field keeps showing the picked person's name after later searches stop returning them. For a full form with server search, see the [flight search form recipe](../recipes/flight-search-form).
#### Optional values
A field with `clearable` shows a × button while it has a value. Reset the form value in `onClear`:
```tsx
(
field.onChange(null)} />
)}
/>
```
#### Inline pickers
The inline pickers (`DatePicker`, `TimePicker`, `WheelPicker`…) take `value` + `onChange` the same way, for a picker that's always on screen:
```tsx
} />
```
### Formik
```tsx
{({ values, setFieldValue }) => (
<>
setFieldValue('birthday', d)} />
setFieldValue('size', v)} />
>
)}
```
### Sending dates to an API
Pickers work with local `Date` objects. Serialize them for your API deliberately:
```ts
import { toDateKey } from '@elik56a/react-native-pickers';
// A calendar day (no time zone): '2026-10-14'
const day = toDateKey(date);
// An instant: '2026-10-14T07:30:00.000Z'
const instant = time.toISOString();
// Parse a 'YYYY-MM-DD' back to a local date (don't use new Date('2026-10-14'), which parses as UTC)
const [y, m, d] = '2026-10-14'.split('-').map(Number);
const local = new Date(y, m - 1, d);
```
:::caution Day keys vs ISO strings
`date.toISOString().slice(0, 10)` converts to UTC first, and can give **yesterday or tomorrow** depending on the user's time zone. Use `toDateKey` for calendar days.
:::
### Zod schema
```ts
import { z } from 'zod';
const schema = z.object({
stay: z
.object({ start: z.date().nullable(), end: z.date().nullable() })
.refine((r) => r.start && r.end, 'Choose both dates'),
country: z.string({ required_error: 'Pick a country' }),
});
```
---
## Haptics
Native wheels tick under your finger. Get the same feel with `onValueChanging`, which fires on **every row crossing** while scrolling. The library has no haptics dependency, so bring the one you already use.
### expo-haptics
```bash
npx expo install expo-haptics
```
```tsx
import * as Haptics from 'expo-haptics';
import { WheelPicker, TimePicker, DatePicker } from '@elik56a/react-native-pickers';
const tick = () => Haptics.selectionAsync();
```
### react-native-haptic-feedback
```tsx
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
const tick = () => ReactNativeHapticFeedback.trigger('selection', { enableVibrateFallback: false });
```
### Confirm haptics
For a stronger buzz when a value is confirmed, use `onChange` or a sheet's `onConfirm`:
```tsx
{
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setRange(range);
}}
/>
```
### Which components support `onValueChanging`
| Component | Callback argument |
| --- | --- |
| `WheelPicker` | `(value, index)` |
| `MultiWheelPicker` columns | `(value, index)` per column |
| `TimePicker` / `TimePickerSheet` | `(date)` |
| `DateWheelPicker` / `DatePicker display="wheel"` | `(date)` |
| `WheelPickerSheet` | `(value, index)` |
Disabled rows don't trigger `onValueChanging`.
---
## i18n & RTL
## Internationalization & RTL
Every name the pickers display comes from the JavaScript `Intl` API for the `locale` you pass (or the device default), and every UI string is a prop.
### What `locale` controls
| Component | Derived from `locale` |
| --- | --- |
| `Calendar` / `DatePicker` | Month title, weekday names, first day of week, day accessibility labels |
| `DateWheelPicker` | Month names, column order (D/M/Y vs M/D/Y vs Y/M/D) |
| `TimePicker` | 12h vs 24h, AM/PM labels |
```tsx live
import { useState } from 'react';
import { View, Text } from 'react-native';
import { DatePicker, TimePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [d, setD] = useState(new Date());
const [t, setT] = useState(new Date());
return (
he-ILar-EG
);
}
```
Pass your app's language, for example from `expo-localization` or `i18next`:
```tsx
import { getLocales } from 'expo-localization';
const locale = getLocales()[0]?.languageTag; // 'fr-CA'
```
#### Hermes and `Intl`
Hermes (the default engine) supports `Intl.DateTimeFormat` on iOS and Android. If a locale or feature is missing at runtime, every helper falls back to English names instead of throwing. `firstDayOfWeek` uses `Intl.Locale` week info where available; otherwise it defaults to Sunday, so pass `firstDayOfWeek={1}` explicitly if you need Monday everywhere.
Hermes can't list or name countries and time zones (`Intl.DisplayNames`, `Intl.supportedValuesOf`), so [`CountryPicker`](../components/country-picker) and [`TimeZonePicker`](../components/time-zone-picker) ship their lists. Names show in the user's language where the engine provides them, and in English otherwise.
### Translating UI strings
| Component | String props (defaults) |
| --- | --- |
| Picker sheets | `confirmLabel` (`Done`, `OK` in Material), `cancelLabel` (`Cancel`), `title` |
| `SelectPicker` | `placeholder` (`Select…`), `label`, `sheetTitle`, `searchPlaceholder` (`Search`), `emptyText` (`No results`) |
| `SelectList` | `searchPlaceholder`, `emptyText` |
| `EmojiPicker` | `searchPlaceholder` (`Search emoji`), `emptyText` (`No emoji found`), `categoryLabels` |
| `TimePicker` | `dayPeriodLabels` (`[am, pm]`) |
| `DurationPicker` | `labels` (`h`, `min`, `s`) |
| `ColorPicker` | `labels`: tab, slider and button names (`Grid`, `Hue`, `Opacity`, `Copy`…) |
| `Calendar`, `WeekStrip` | `formatTitle` |
```tsx
const { t } = useTranslation();
```
### Right-to-left
All layouts use logical `start` / `end` properties, so they mirror automatically when `I18nManager.isRTL` is true:
- Calendar arrows swap direction, and swipe direction follows.
- The range band grows from the correct side.
- Select check marks, icons and chips sit on the logical end / start.
- Multi-wheel columns follow the row direction.
Enable RTL the standard React Native way:
```ts
import { I18nManager } from 'react-native';
I18nManager.allowRTL(true);
I18nManager.forceRTL(true); // requires an app reload
```
#### On the web
React Native Web has no RTL layout flag, and the browser aligns each label by its own script, so a list mixing Hebrew and English would zigzag. Select labels are therefore pinned to the left edge on the web. For an RTL web app, align them right through the slot styles:
```tsx
const rtl = { textAlign: 'right' } as const;
```
### Search in any language
The select search folds case and **diacritics** (`é` → `e`) and strips **Hebrew niqqud**, so users don't need to type accents. Use `keywords` to make options findable in other languages or by synonyms:
```ts
{ label: 'Germany', value: 'DE', keywords: ['Deutschland', 'Allemagne', 'גרמניה'] }
```
---
## Performance
### What the library does
- **Native-driver animations.** Wheel 3D transforms, fades, month slides and sheet transitions run on the UI thread on iOS and Android.
- **Virtualized lists.** Wheels and the emoji grid are `FlatList`s with `getItemLayout`, so only visible rows exist and scrolling never measures.
- **Memoized rows.** While a wheel scrolls, only the two rows whose selected state flips re-render. Calendar days and emoji cells are memoized too.
- **Lazy emoji data.** The 1,923-emoji table is parsed on first use and cached.
- **Formatter cache.** `Intl.DateTimeFormat` instances are cached per locale and options.
### What you can do
**Keep `items` / `options` stable.** Define them outside the component or `useMemo` them. New arrays every render make wheels re-normalize their items.
```tsx
// ✅ stable
const HOURS = Array.from({ length: 24 }, (_, i) => i);
function Example() {
return ;
}
```
**Memoize `theme` and `styles` objects**, especially on wheels:
```tsx
const theme = useMemo(() => ({ colors: { primary: brand } }), [brand]);
```
**Keep callbacks light.** `onValueChanging` fires on every row crossing; do haptics or cheap updates there, not network calls. Save in `onChange`.
**Keep `isDateDisabled` fast.** It runs for up to 42 days per render. Precompute a `Set` of `toDateKey`s.
**Mount sheets once.** Keep `` in the tree and toggle `visible`. It renders nothing while closed.
**Long select lists.** `SelectList` is virtualized. For thousands of options, give it a `listHeight` so the FlatList can window.
---
## Expo, bare RN & web
## Platforms
| Platform | Support | Notes |
| --- | --- | --- |
| **Expo Go** | ✅ | No native code, so nothing to prebuild |
| **Expo dev builds / EAS** | ✅ | |
| **Bare React Native** (iOS, Android) | ✅ | `react-native >= 0.72` |
| **New Architecture** (Fabric) | ✅ | Uses only core components |
| **Hermes** / JSC | ✅ | `Intl` names fall back to English where unsupported |
| **React Native Web** | ✅ | This site runs the library on RNW |
| **Expo Router web / Next.js** | ✅ | Via `react-native-web` |
| **react-native-windows / macos** | Untested | Core components only, so likely to work |
### Why zero native code matters
- **Native feel, your brand.** Each platform gets its own [design](./designs), in your colors and fonts. Native date pickers change with every OS version, and Android's calendar dialog can't be themed much.
- **No native build.** Add the package and reload. Works in Expo Go and in OTA updates.
- **One code path.** No platform-specific bugs to chase, and tests run in Jest without native mocks.
### Designs
Pickers look native on each platform: iOS on iOS, Material Design on Android, a web design on the web. Set `design="ios"` (or `"material"`, `"web"`) on `PickersProvider` to use one look everywhere. See [Designs](./designs) for what each one changes and how to customize it.
### How it's built
Only React Native core APIs: `FlatList`, `Animated` (native driver on iOS/Android), `PanResponder`, `Modal`, `Pressable`, `TextInput` and `Intl`. No Reanimated, Gesture Handler, SVG or vector icons.
### Web specifics
- The wheel snaps to the nearest row once scrolling stops. Mouse wheel, trackpad and touch all work.
- `Modal` (used by sheets) renders into a portal on the web.
- Everything works from the keyboard: arrow keys move through calendars, lists and grids and step wheels and sliders, Esc closes a sheet, and focus returns to the field. See [Accessibility](./accessibility.mdx#keyboard).
- Animations run on the JS driver on the web, because the native driver isn't available there.
#### Previewing other platforms
Pickers adapt to `Platform.OS`: fields open a dropdown on the web and a bottom sheet on iOS and Android, sheets keep room for the iOS home indicator and status bar, and headers drag on native. To see the native behavior in a browser (a design system, Storybook, these docs), set `platform` on the provider:
```tsx
```
The switch on every live example here previews iOS, Android and Web this way, with each platform's [design](./designs). Other platforms, such as Windows and macOS, behave like Android.
#### Next.js / SSR
Render pickers on the client. They measure layout and animate, so wrap them in a client component:
```tsx
'use client';
import { DatePicker } from '@elik56a/react-native-pickers';
```
and alias `react-native` to `react-native-web` in your bundler config (Expo's and Next's RNW guides cover this).
### Testing
Components render in Jest with the `react-native` preset. Use `testID` props to find them:
```tsx
render();
```
Wheels settle with timers, so use fake timers (`jest.useFakeTimers()` + `jest.runAllTimers()`) when asserting on `onChange` after a scroll event.
---
## Render props
When tokens and slots aren't enough, render props let you draw a part yourself. The component still handles touches, selection, scrolling and accessibility; you only render the visuals.
| Component | Prop | Signature |
| --- | --- | --- |
| `WheelPicker` | `renderItem` | `({ item, index, selected, theme }) => ReactNode` |
| `WheelPicker` | `renderIndicator` | `(theme) => ReactNode` |
| `Calendar` / `DatePicker` | `renderDay` | `(state: DayState, theme) => ReactNode` |
| `Calendar` / `DatePicker` | `renderArrow` | `(direction: 'prev' \| 'next', disabled: boolean) => ReactNode` |
| `Calendar` / `DatePicker` | `formatTitle` | `(month: Date, locale?: string) => string` |
| `SelectPicker` | `renderTrigger` | `({ open, selected, disabled }) => ReactNode` |
| `SelectPicker` | `renderChevron` | `({ open, theme }) => ReactNode` |
| `SelectPicker` / `SelectList` | `renderOption` | `({ option, selected, disabled, theme }) => ReactNode` |
| `SelectPicker` / `SelectList` | `renderGroupHeader` | `(group: string, theme) => ReactNode` |
| `SelectPicker` / `SelectList` | `renderLoading` | `(theme) => ReactNode` |
| `HorizontalList` | `renderItem` | `({ item, selected, disabled, theme }) => ReactNode` |
| `*PickerField` / `PickerField` | `renderTrigger` | `({ open, displayValue, disabled }) => ReactNode` |
| `*PickerField` / `PickerField` | `renderChevron` | `({ open, theme }) => ReactNode` |
| `EmojiPicker` | `renderEmoji` | `(emoji: string, data: Emoji, size: number) => ReactNode` |
| `MultiWheelPicker` | `separator` | `ReactNode \| (leftIndex) => ReactNode` |
| `BottomSheet` | `title`, `headerLeft`, `headerRight`, `footer` | `ReactNode` |
Every render prop receives the resolved `theme`, so custom parts stay correct in dark mode.
### Calendar with availability prices
```tsx live
import { useState } from 'react';
import { View, Text } from 'react-native';
import { DatePicker } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
const price = (d: Date) => 80 + ((d.getDate() * 37) % 90);
export default function Example() {
const [range, setRange] = useState({ start: null, end: null });
return (
{
const on = s.selected;
const p = price(s.date);
return (
{s.date.getDate()}
{!s.disabled && !s.outside && (
${p}
)}
);
}}
/>
);
}
```
### Wheel with icons
```tsx live
import { useState } from 'react';
import { View, Text } from 'react-native';
import { WheelPicker } from '@elik56a/react-native-pickers';
const moods = [
{ label: 'Awful', value: 1, icon: '😫' },
{ label: 'Bad', value: 2, icon: '😕' },
{ label: 'Okay', value: 3, icon: '😐' },
{ label: 'Good', value: 4, icon: '🙂' },
{ label: 'Great', value: 5, icon: '🤩' },
];
export default function Example() {
const [mood, setMood] = useState(4);
return (
(
{moods[item.value - 1].icon}
{item.label}
)}
/>
);
}
```
### Tips
- Keep render functions cheap. Wheels render a few rows, but they re-render as the selection moves.
- Don't add your own `Pressable` around the whole of `renderDay` / `renderOption` / `renderItem` content. The cell is already pressable, and a nested pressable swallows the tap.
- Read colours from the `theme` argument instead of hard-coding them, so dark mode keeps working.
---
## Styling with slots
Theme tokens set colours and sizes everywhere. **Slots** let you style one specific part of one component. Every component takes a `styles` prop: an object of slot name → style.
```tsx live
import { useState } from 'react';
import { WheelPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [v, setV] = useState('Wed');
return (
);
}
```
### Rules
- **Slot styles are merged last**, after the built-in and theme styles, so they always win.
- **State slots stack** on top of base slots: `selectedItemText` is applied after `itemText`, and `selectedDay` after `dayInner`.
- Slots accept any `StyleProp`: objects, arrays or `StyleSheet.create` entries.
- Types are exact. `SlotStyles` autocompletes valid names and flags typos.
- `style` (singular) styles the outer container; `styles` (plural) are the slots.
```ts
type SlotStyles = Partial>>;
```
### All slots
| Component | Slots |
| --- | --- |
| [WheelPicker](../components/wheel-picker#slots) | `container` `item` `itemText` `selectedItemText` `disabledItemText` `indicator` |
| [MultiWheelPicker / TimePicker / DateWheelPicker / DurationPicker](../components/multi-wheel-picker#slots) | `container` `indicator` `separator` `item` `itemText` `selectedItemText` `disabledItemText` |
| [Calendar / DatePicker](../components/date-picker#slots) | `container` `header` `headerTitle` `arrow` `arrowText` `weekdays` `weekdayText` `week` `day` `dayInner` `dayText` `selectedDay` `selectedDayText` `todayDay` `todayDayText` `disabledDayText` `outsideDayText` `rangeBand` `dot` `dayLabel` `gridCell` `gridCellText` |
| [MonthPicker](../components/month-picker#props) | the wheel slots, or the grid's `header` `headerTitle` `arrow` `arrowText` `gridCell` `gridCellText` |
| [WeekStrip](../components/week-strip#props) | the Calendar slots |
| [ColorPicker](../components/color-picker#props) | `container` `swatch` `pad` `slider` `sliderThumb` `sliderLabel` `hexInput` `preview` |
| [SelectList](../components/select-list#slots) | `container` `search` `searchInput` `list` `groupHeader` `groupHeaderText` `option` `selectedOption` `optionLabel` `selectedOptionLabel` `optionDescription` `check` `empty` `emptyText` `loading` `loadingText` |
| [SelectPicker](../components/select-picker#slots) | all SelectList slots + `trigger` `triggerText` `placeholder` `label` `chip` `chipText` `chevron` `clear` |
| [HorizontalList](../components/horizontal-list#slots) | `container` `content` `item` `selectedItem` `itemLabel` `selectedItemLabel` `thumb` `arrow` `arrowText` |
| [EmojiPicker](../components/emoji-picker#slots) | `container` `header` `searchInput` `skinToneButton` `skinToneMenu` `categoryBar` `categoryTab` `activeCategoryTab` `categoryIndicator` `sectionHeader` `sectionHeaderText` `emojiCell` `emojiText` `empty` `emptyText` |
| [BottomSheet](../components/bottom-sheet#slots) | `backdrop` `sheet` `handle` `header` `title` `content` `footer` |
Picker sheets style their `BottomSheet` through `sheetProps={{ styles: { … } }}` and their picker through the picker's own `styles`.
### Example: square calendar
```tsx live
import { useState } from 'react';
import { DatePicker, addDays } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';
const square = {
dayInner: { borderRadius: 6 },
selectedDay: { borderRadius: 6 },
todayDay: { borderRadius: 6, borderStyle: 'dashed' as const },
rangeBand: { borderRadius: 0 },
headerTitle: { fontWeight: '800' as const, fontSize: 18 },
arrow: { borderRadius: 6 },
};
export default function Example() {
const t = new Date();
const [r, setR] = useState({
start: addDays(t, 1),
end: addDays(t, 4),
});
return ;
}
```
### Example: outlined select field
```tsx live
import { useState } from 'react';
import { SelectPicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [v, setV] = useState(['design']);
return (
({
label: t,
value: t.toLowerCase(),
}))}
value={v}
onChange={setV}
styles={{
label: { fontSize: 11, letterSpacing: 1.2, fontWeight: '700' },
trigger: { borderWidth: 2, borderRadius: 4, minHeight: 56 },
chip: { borderRadius: 4, backgroundColor: '#111827' },
chipText: { color: '#FFFFFF' },
}}
/>
);
}
```
### Reusing styles
Define slot objects once and share them app-wide:
```ts title="pickerStyles.ts"
import type { SlotStyles, CalendarSlot, SelectSlot, SelectTriggerSlot } from '@elik56a/react-native-pickers';
export const calendarStyles: SlotStyles = { headerTitle: { fontFamily: 'Inter-Bold' } };
export const selectStyles: SlotStyles = { trigger: { minHeight: 52 } };
```
→ When a slot isn't enough, [replace the part with a render prop](./render-props).
---
## Theming
Pickers ship with a polished light and dark theme that follow the OS appearance automatically. Change any token for the whole app with `PickersProvider`, or for one component with its `theme` prop.
### PickersProvider
```tsx title="App.tsx"
import { PickersProvider } from '@elik56a/react-native-pickers';
export default function App() {
return (
);
}
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `colorScheme` | `'auto' \| 'light' \| 'dark'` | `'auto'` | `'auto'` follows `useColorScheme()`. |
| `theme` | `DeepPartial` | — | Overrides merged onto `lightTheme`. **Also applied to dark mode when `darkTheme` is not given.** |
| `darkTheme` | `DeepPartial` | — | Overrides merged onto `darkTheme`. |
| `platform` | `'ios' \| 'android' \| 'web' \| …` | `Platform.OS` | Pickers behave as on this platform. See [previewing other platforms](./platforms#previewing-other-platforms). |
| `design` | `'auto' \| 'ios' \| 'material' \| 'web'` | `'auto'` | The look pickers follow. `'auto'` matches the platform. See [designs](./designs). |
| `children` | `ReactNode` | **required** | |
:::caution Light overrides leak into dark mode
If you pass only `theme`, the same overrides are applied on top of the dark base too. That's right for brand colours and fonts, but wrong for backgrounds. Pass `darkTheme` whenever you override surface colours.
:::
Providers can be nested; the innermost one wins. That's an easy way to force one area dark:
```tsx live
import { useState } from 'react';
import { View } from 'react-native';
import { PickersProvider, DatePicker } from '@elik56a/react-native-pickers';
export default function Example() {
const [date, setDate] = useState(new Date());
return (