The four options
| Option | Look | Expo Go | Best for | Trade-off |
|---|---|---|---|---|
| @react-native-picker/picker | Platform native | ✅ Yes | Settings-style value fields | iOS renders a 216pt wheel |
| react-native-element-dropdown | Styled by you | ✅ Yes | Form fields, search, multi-select | Not a platform control |
| @react-native-menu/menu | True native menu | ❌ Dev build | Overflow and context menus | Menus, not form inputs |
| Custom Modal / sheet | Fully yours | ✅ Yes | Long lists, design-led UI | You own a11y and dismissal |
The decision comes down to one question: is this a platform value or a designed field? A unit, a timezone, a repeat interval — those are platform values, and the OS control is the right answer because users already know how it works. A category chip on your add-expense screen is a designed field, and a native wheel will look like it wandered in from a different app.
The platform control: Picker
npx expo install @react-native-picker/pickerimport { useState } from 'react';
import { Picker } from '@react-native-picker/picker';
const UNITS = [
{ label: 'Kilograms', value: 'kg' },
{ label: 'Pounds', value: 'lb' },
];
export default function UnitField() {
const [unit, setUnit] = useState('kg');
return (
<Picker
selectedValue={unit}
onValueChange={setUnit}
dropdownIconColor="#8a8a8a" // Android only
itemStyle={{ color: '#fff' }} // iOS only
>
{UNITS.map((u) => (
<Picker.Item key={u.value} label={u.label} value={u.value} />
))}
</Picker>
);
}Note the two platform-only props sitting next to each other. That is the honest summary of this component: it is two different widgets behind one name. On Android you get a tappable row that opens a dialog — a real dropdown. On iOS you get an inline spinning wheel that occupies roughly 216 points of vertical space in your layout, whether you wanted it to or not.
Most teams end up wrapping it: render a normal-looking row on iOS, and open the Picker inside a modal or an action sheet when it is tapped. If you are already writing that wrapper, you are most of the way to a custom dropdown, which is why so many projects skip straight to the next option.
The common case: element-dropdown
npm install react-native-element-dropdownPure JavaScript, so it works in Expo Go and needs no native rebuild. It renders a field you style yourself and an options list that looks the same on both platforms — which is usually the point.
import { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Dropdown } from 'react-native-element-dropdown';
const CATEGORIES = [
{ label: 'Groceries', value: 'groceries' },
{ label: 'Transport', value: 'transport' },
{ label: 'Rent', value: 'rent' },
{ label: 'Subscriptions', value: 'subs' },
];
export default function CategoryField({ value, onChange }) {
const [focused, setFocused] = useState(false);
return (
<View style={styles.wrap}>
<Text style={styles.label}>Category</Text>
<Dropdown
data={CATEGORIES}
labelField="label"
valueField="value"
value={value}
onChange={(item) => onChange(item.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="Choose one"
search
searchPlaceholder="Search categories"
style={[styles.field, focused && styles.fieldFocused]}
containerStyle={styles.menu}
placeholderStyle={styles.placeholder}
selectedTextStyle={styles.selected}
inputSearchStyle={styles.search}
activeColor="#232323"
/>
</View>
);
}
const styles = StyleSheet.create({
wrap: { marginBottom: 20 },
label: { color: '#8a8a8a', fontSize: 13, marginBottom: 8 },
field: {
height: 52,
paddingHorizontal: 16,
borderRadius: 12,
backgroundColor: '#141414',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
},
fieldFocused: { borderColor: '#fb923c' },
menu: {
borderRadius: 12,
backgroundColor: '#141414',
borderColor: 'rgba(255,255,255,0.08)',
},
placeholder: { color: '#5a5a5a', fontSize: 15 },
selected: { color: '#fff', fontSize: 15 },
search: { color: '#fff', borderRadius: 8, borderColor: 'rgba(255,255,255,0.08)' },
});The search prop is the reason this package wins most comparisons. The moment a list passes roughly fifteen options, scrolling becomes the worst part of the form, and adding search to a hand-rolled dropdown is an afternoon you did not plan for. Multi-select lives in the sibling MultiSelect export with the same API shape.
The Android bug behind most “my dropdown is cut off” reports
The classic hand-rolled dropdown is a field with an absolutely positioned list underneath it and a high zIndex. On iOS that works. On Android it gets clipped at the edge of the nearest parent, because Android does not render children outside their parent bounds and zIndex does not lift a view out of a clipping ancestor the way CSS stacking contexts do.
// ❌ Fine on iOS, cut off on Android inside any card or ScrollView
<View style={{ position: 'relative' }}>
<Pressable onPress={open}><Text>{value}</Text></Pressable>
{isOpen && (
<View style={{ position: 'absolute', top: 52, zIndex: 999, elevation: 10 }}>
{options.map(renderOption)}
</View>
)}
</View>elevation helps with paint order but not with clipping. The fix is to stop overflowing at all — render the list in a Modal, which mounts outside the view hierarchy and behaves identically on both platforms:
// ✅ Same on both platforms, and dismissal comes for free
<Modal visible={isOpen} transparent animationType="fade" onRequestClose={close}>
<Pressable style={styles.backdrop} onPress={close} />
<View style={styles.sheet}>
<FlatList
data={options}
keyExtractor={(o) => o.value}
renderItem={({ item }) => (
<Pressable
onPress={() => { onChange(item.value); close(); }}
style={styles.row}
accessibilityRole="button"
accessibilityState={{ selected: item.value === value }}
>
<Text style={styles.rowText}>{item.label}</Text>
</Pressable>
)}
/>
</View>
</Modal>Two things you now get for free that the absolute-positioned version never had: the hardware back button closes it on Android via onRequestClose, and the list can be taller than the screen without fighting the parent scroll view. The same reasoning is why a bottom sheet is often the better dropdown once you have more than a handful of options — it is the pattern phone users already expect for “pick one of these”.
Three smaller traps
- Dropdowns inside a FlatList row.Every open dropdown in a recycled row is a candidate for being unmounted mid-interaction as the list virtualises. Lift the open state to the list’s parent and render a single Modal for the whole screen, keyed by which row asked for it. The FlatList guide covers why row components should stay dumb.
- Keyboard still up when the list opens. If the previous field was a text input, the keyboard covers your options. Call
Keyboard.dismiss()before opening, or the user taps blind. - No accessible state. A
Pressablefull of Text is invisible to screen readers as a choice. SetaccessibilityRole="button"andaccessibilityState={{ selected }}on each option — two attributes, and the field stops being unusable with VoiceOver.
Most dropdowns should not be dropdowns
A dropdown is a desktop control designed for a mouse and a lot of screen. On a phone, four or five options read better as a row of chips or a segmented control — no open state, no dismissal logic, no Android clipping, and one fewer tap. Above roughly fifteen options, a searchable full-screen picker beats a floating list because the user can actually see what they are choosing from. The floating dropdown is genuinely the best answer only in the middle of that range.
If you want to feel the difference before committing, describe the form — “an add-expense screen with amount, a category selector with twelve options, and a date field” — and ShipNative builds it as real React Native running on your phone. Tapping through it for thirty seconds settles the chips-versus-dropdown argument faster than any comparison table.