The three options
| Option | Look | Expo Go | Best for | Trade-off |
|---|---|---|---|---|
| @react-native-community/slider | Platform native | ✅ Yes | Volume, brightness, single values | One thumb, limited styling |
| @ptomasroos/react-native-multi-slider | Styled by you | ✅ Yes | Price and age ranges | Slow maintenance, JS-thread drag |
| Custom (Gesture Handler + Reanimated) | Fully yours | ✅ Yes | Design-led controls, vertical sliders | You own accessibility |
Start with the community package. It wraps the real platform control, so it gets native feel, VoiceOver and TalkBack support, and correct behaviour inside scroll views for free. Reach for a custom slider only when you need two thumbs, a vertical layout, or a shape the native track cannot draw.
A working slider in Expo
npx expo install @react-native-community/sliderimport { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Slider from '@react-native-community/slider';
export default function TipSlider({ initial = 15, onSave }) {
// Display value only. The slider owns its own position while dragging.
const [shown, setShown] = useState(initial);
return (
<View style={styles.wrap}>
<Text style={styles.label}>Tip: {shown}%</Text>
<Slider
value={initial}
minimumValue={0}
maximumValue={30}
step={5}
onValueChange={setShown}
onSlidingComplete={onSave}
minimumTrackTintColor="#fb923c"
maximumTrackTintColor="#2a2a2a"
thumbTintColor="#ffffff" // Android, and iOS when no thumbImage
accessibilityLabel="Tip percentage"
style={styles.slider}
/>
</View>
);
}
const styles = StyleSheet.create({
wrap: { marginBottom: 24 },
label: { color: '#fff', fontSize: 15, marginBottom: 8 },
slider: { width: '100%', height: 40 },
});Notice what the slider is not given: the shown state. The label follows the finger through onValueChange, and the value is saved once, in onSlidingComplete, when the user lets go. That split is the whole trick to a smooth slider, and it also means you write to storage or the server once per gesture instead of sixty times a second.
Why a controlled slider stutters
// ❌ The thumb fights the finger, worst on slower Android phones
const [value, setValue] = useState(0.5);
<Slider value={value} onValueChange={setValue} />This looks like the normal React pattern, and it is the cause of most “my slider is jittery” reports. The native control moves the thumb, sends the new value across to JavaScript, React re-renders, and sends a value back down that is already a frame or two behind the finger. The native side then snaps the thumb backwards to match. On a fast iPhone you may never see it. On a mid-range Android phone with a busy screen, the thumb visibly drags and jumps.
The fix is the pattern above: treat value as the starting position only, and change it from outside only when something other than the user moves it, such as a reset button. If you need to reset, change a key on the slider or update value once, not on every drag frame.
The step bug: 0.30000000000000004
Set step={0.1} and the label will sooner or later show 0.30000000000000004. That is JavaScript floating-point arithmetic, not the slider, but the slider is where users see it. Round before you display or store:
onValueChange={(v) => setShown(Math.round(v * 10) / 10)}
// Or avoid fractions entirely: slide 0 to 100 and divide later
<Slider minimumValue={0} maximumValue={100} step={1} />Integer ranges are the cleaner option. Store a whole-number percentage and convert at the point of use, and the bug cannot happen.
A custom slider with Gesture Handler and Reanimated
When the design calls for a thick rounded track, a vertical control, or two thumbs, build it. The drag runs on the UI thread through Reanimated, so it stays smooth regardless of what React is doing, and you report the final value to JavaScript once. Your app root needs GestureHandlerRootView, which Expo Router templates already include.
import { StyleSheet, View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
const WIDTH = 280;
const THUMB = 28;
export function CustomSlider({ initial = 0.5, onChange }) {
const x = useSharedValue(initial * WIDTH);
const start = useSharedValue(0);
const pan = Gesture.Pan()
.hitSlop({ vertical: 16 })
.onBegin(() => {
start.value = x.value;
})
.onUpdate((e) => {
x.value = Math.min(WIDTH, Math.max(0, start.value + e.translationX));
})
.onEnd(() => {
runOnJS(onChange)(x.value / WIDTH);
});
const fill = useAnimatedStyle(() => ({ width: x.value }));
const thumb = useAnimatedStyle(() => ({ transform: [{ translateX: x.value - THUMB / 2 }] }));
return (
<GestureDetector gesture={pan}>
<View style={styles.track}>
<Animated.View style={[styles.fill, fill]} />
<Animated.View style={[styles.thumb, thumb]} />
</View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
track: { width: WIDTH, height: 8, borderRadius: 4, backgroundColor: '#2a2a2a', justifyContent: 'center' },
fill: { height: 8, borderRadius: 4, backgroundColor: '#fb923c' },
thumb: { position: 'absolute', left: 0, width: THUMB, height: THUMB, borderRadius: THUMB / 2, backgroundColor: '#fff' },
});The fixed WIDTH keeps the example short. In a real screen, measure the track with onLayout and store the width in a shared value. If you are on Reanimated 4, runOnJS still works but the newer API is scheduleOnRN from react-native-worklets. Background on how the pan gesture works is in the Gesture Handler guide.
What you give up is accessibility. The native slider announces itself and responds to swipe-up and swipe-down adjustments in VoiceOver. A custom one needs accessibilityRole="adjustable", accessibilityValue, and onAccessibilityAction handlers for increment and decrement, or screen reader users cannot use it at all.
When a slider is the wrong control
Sliders are good for values where the exact number matters less than the feel: volume, brightness, a rough budget. They are poor for precise values. Nobody wants to drag a thumb to exactly 37. If the user needs a specific number, a stepper with plus and minus buttons, or a numeric input, is faster and less frustrating. A slider with five or six steps is often better as a row of chips.
Drag feel is another thing you cannot judge from a screenshot. Describe the screen, “a settings page with a font size slider that previews the text live”, and ShipNative builds it as real React Native that runs on your phone, so you can check the drag under your own thumb before you commit to it.