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.

Syntax<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

Example · tsx
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.

Example · tsx
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.

Example · tsx
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.

Example · tsx
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

  • Be the first to comment on this lesson.