Switches, Sliders and Pickers
Switch from core, slider and picker from the community, date and time from the OS, and the platform differences you cannot style away.
<Switch value={on} onValueChange={(next: boolean) => void} />Only one of these is built into React Native. Switch ships in core. Sliders, pickers and date selection come from community packages, installed with npx expo install.
Switch
Switch is controlled like a TextInput: pass value as a boolean, and onValueChange receives the new boolean directly. Colour props are trackColor, which takes { false, true }, thumbColor, and ios_backgroundColor. A switch applies instantly. There is no Save step, so do not put one in a form that has one.
Slider and picker
@react-native-community/slider gives Slider with minimumValue, maximumValue, step, onValueChange while dragging and onSlidingComplete on release. @react-native-picker/picker gives Picker with selectedValue and onValueChange(itemValue, itemIndex), holding Picker.Item children with label and value.
Dates
@react-native-community/datetimepicker wraps the system picker. onChange is called with (event, date), and event.type is 'set' or 'dismissed'. On Android the library recommends the imperative DateTimePickerAndroid.open(), because the picker is a dialog rather than a view.
What you cannot style
These are real OS controls. A picker is a spinning wheel on iOS and a dialog or dropdown on Android. mode and prompt are Android only; itemStyle is iOS only. If a design demands one identical control, build it yourself from a list in a modal.
Example
import { useState } from "react";
import { StyleSheet, Switch, Text, View } from "react-native";
import Slider from "@react-native-community/slider";
// On screen: a settings panel with a label and a toggle on one row, and a
// volume slider below it. The slider greys out and stops responding when the
// switch is off; the percentage beside it tracks the thumb as you drag.
export default function SoundSettings() {
const [enabled, setEnabled] = useState(true);
const [volume, setVolume] = useState(0.6);
return (
<View style={styles.container}>
<View style={styles.row}>
<Text style={styles.label}>Sound effects</Text>
<Switch
value={enabled}
onValueChange={setEnabled} // receives the new boolean, not an event
trackColor={{ false: "#c6c6c8", true: "#34c759" }}
ios_backgroundColor="#c6c6c8" // shown behind the track when off (iOS)
/>
</View>
<View style={styles.row}>
<Text style={styles.label}>Volume</Text>
<Text style={styles.value}>{Math.round(volume * 100)}%</Text>
</View>
<Slider
minimumValue={0}
maximumValue={1}
step={0.05}
value={volume}
disabled={!enabled}
onValueChange={setVolume} // fires continuously while dragging
minimumTrackTintColor="#0a84ff"
maximumTrackTintColor="#c6c6c8"
/>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 12 },
row: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
label: { fontSize: 16 },
value: { fontSize: 16, color: "#8a8a8e" },
});When to use it
- A settings screen of toggles that apply the moment they are flipped.
- Choosing a category or country from a fixed list of options.
- Picking a due date or a reminder time using the system picker the user already knows.
- Letting the user set a maximum price or a volume level by dragging.
More examples
Picker with typed options
Options are defined once as data and mapped to Picker.Item children, each with a key. The same component renders as an inline wheel on iOS and a tap-to-open dropdown on Android, which is exactly the difference you cannot style away. Note that mode and itemStyle each do nothing on the other platform.
import { useState } from "react";
import { Picker } from "@react-native-picker/picker";
import { Platform, StyleSheet, Text, View } from "react-native";
type Speed = "slow" | "normal" | "fast";
const OPTIONS: { label: string; value: Speed }[] = [
{ label: "Slow", value: "slow" },
{ label: "Normal", value: "normal" },
{ label: "Fast", value: "fast" },
];
export default function SpeedPicker() {
const [speed, setSpeed] = useState<Speed>("normal");
return (
<View style={styles.container}>
<Text style={styles.label}>Playback speed</Text>
<Picker
selectedValue={speed}
// onValueChange gets (itemValue, itemIndex); the value is typed as any,
// so narrow it yourself rather than trusting it.
onValueChange={(itemValue) => setSpeed(itemValue as Speed)}
mode="dropdown" // Android only; ignored on iOS
itemStyle={styles.item} // iOS only; ignored on Android
>
{OPTIONS.map((option) => (
<Picker.Item key={option.value} label={option.label} value={option.value} />
))}
</Picker>
<Text style={styles.hint}>
{Platform.OS === "ios" ? "A wheel, inline" : "A dropdown, on tap"}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 16 },
label: { fontSize: 13, fontWeight: "600" },
item: { fontSize: 16 },
hint: { fontSize: 12, color: "#8a8a8e" },
});Date selection on both platforms
One handler, two presentations. The second argument to onChange is optional, because a dismissed Android dialog calls back with event.type 'dismissed' and no date, so checking the type before storing is required rather than defensive. display accepts 'inline' and 'compact' on iOS 14 and later, and 'spinner' or 'calendar' on Android.
import { useState } from "react";
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
import DateTimePicker, {
DateTimePickerAndroid,
type DateTimePickerEvent,
} from "@react-native-community/datetimepicker";
export default function DueDateField() {
const [date, setDate] = useState(new Date(2026, 0, 15));
const [showIOS, setShowIOS] = useState(false);
function onChange(event: DateTimePickerEvent, selected?: Date) {
setShowIOS(false);
if (event.type === "set" && selected !== undefined) setDate(selected);
}
function open() {
if (Platform.OS === "android") {
// Imperative on Android: the picker is a dialog, not a view.
DateTimePickerAndroid.open({ value: date, mode: "date", onChange });
} else {
setShowIOS(true);
}
}
return (
<View style={styles.container}>
<Pressable onPress={open}>
<Text style={styles.value}>{date.toDateString()}</Text>
</Pressable>
{showIOS ? (
<DateTimePicker value={date} mode="date" display="inline" onChange={onChange} />
) : null}
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 8 },
value: { fontSize: 16, color: "#0a84ff" },
});Committing a slider on release
The label follows the thumb while dragging, but the costly work happens once on release. Splitting the two callbacks is the fix for a slider that stutters: onValueChange can fire dozens of times a second, so anything heavier than a setState belongs in onSlidingComplete.
import { useState } from "react";
import Slider from "@react-native-community/slider";
import { StyleSheet, Text, View } from "react-native";
type PriceFilterProps = {
onCommit: (maxPrice: number) => void; // run the expensive work once
};
export default function PriceFilter({ onCommit }: PriceFilterProps) {
const [preview, setPreview] = useState(50);
return (
<View style={styles.container}>
<Text style={styles.label}>Up to {preview}</Text>
<Slider
minimumValue={0}
maximumValue={200}
step={5}
value={preview}
onValueChange={setPreview} // cheap: just updates the label
onSlidingComplete={onCommit} // fires once, when the thumb is released
minimumTrackTintColor="#0a84ff"
/>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 8 },
label: { fontSize: 16, fontWeight: "600" },
});
Discussion