Slider
An interactive slider component for selecting a value within a range.
Web preview — components render natively on iOS & Android
import { Slider } from "@/components/ui/slider";
import { useState } from "react";
export function MyScreen() {
const [value, setValue] = useState(50);
return (
<Slider
value={value}
onValueChange={setValue}
min={0}
max={100}
/>
);
}Installation#
npx @aniui/cli add sliderUsage#
app/index.tsx
import { Slider } from "@/components/ui/slider";
import { useState } from "react";
export function MyScreen() {
const [value, setValue] = useState(50);
return (
<Slider
value={value}
onValueChange={setValue}
min={0}
max={100}
/>
);
}Default#
Web preview — components render natively on iOS & Android
import { Slider } from "@/components/ui/slider";
import { useState } from "react";
export function MyScreen() {
const [value, setValue] = useState(50);
return (
<Slider
value={value}
onValueChange={setValue}
min={0}
max={100}
/>
);
}Disabled#
Web preview — components render natively on iOS & Android
<Slider value={30} disabled />Props#
PropTypeDefault
valuenumber0minnumber0maxnumber100stepnumber1size"sm" | "md" | "lg""md"disabledbooleanfalseonValueChange(value: number) => void—classNamestring—Also accepts all View props from React Native.
Accessibility#
- Uses PanResponder for smooth gesture-based dragging
accessibilityRole="adjustable"with min/max/now values exposed to assistive technology.
Source#
components/ui/slider.tsx
import React, { useCallback, useEffect } from "react";
import { View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, { useSharedValue, useAnimatedStyle, runOnJS } from "react-native-reanimated";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { useThemeColors } from "@/components/ui/theme-provider";
const sliderVariants = cva("w-full justify-center", {
variants: {
size: {
sm: "h-8",
md: "h-10",
lg: "h-12",
},
},
defaultVariants: { size: "md" },
});
export interface SliderProps
extends React.ComponentPropsWithoutRef<typeof View>,
VariantProps<typeof sliderVariants> {
className?: string;
value?: number;
min?: number;
max?: number;
step?: number;
disabled?: boolean;
onValueChange?: (value: number) => void;
}
export function Slider({
value = 0, min = 0, max = 100, step = 1, disabled, size,
onValueChange, className, ...props
}: SliderProps) {
const trackWidth = useSharedValue(0);
// The thumb/fill follow this shared value so they track the finger even if
// the consumer doesn't echo `value` back synchronously.
const pct = useSharedValue(max > min ? ((value - min) / (max - min)) * 100 : 0);
const isDragging = useSharedValue(false);
const thumbSize = size === "lg" ? 24 : size === "sm" ? 16 : 20;
const colors = useThemeColors();
useEffect(() => {
if (!isDragging.value) {
pct.value = max > min ? ((value - min) / (max - min)) * 100 : 0;
}
}, [value, min, max, pct, isDragging]);
const setFromX = useCallback((locationX: number) => {
"worklet";
const w = trackWidth.value;
if (w <= 0) return;
const ratio = Math.max(0, Math.min(1, locationX / w));
const raw = min + ratio * (max - min);
const stepped = Math.round(raw / step) * step;
const clamped = Math.max(min, Math.min(max, stepped));
pct.value = max > min ? ((clamped - min) / (max - min)) * 100 : 0;
if (onValueChange) runOnJS(onValueChange)(clamped);
}, [min, max, step, onValueChange, trackWidth, pct]);
const gesture = Gesture.Pan()
.enabled(!disabled)
.onBegin((e) => { isDragging.value = true; setFromX(e.x); })
.onUpdate((e) => { setFromX(e.x); })
.onFinalize(() => { isDragging.value = false; })
.minDistance(0);
// transform: scaleX instead of width — the fill's box is always full-width,
// only its visual scale changes, so this never triggers a native layout
// pass the way animating `width` on every drag frame would.
const fillStyle = useAnimatedStyle(() => ({
transform: [{ scaleX: pct.value / 100 }],
transformOrigin: "left",
}));
const thumbStyle = useAnimatedStyle(() => ({
position: "absolute" as const,
left: `${pct.value}%`,
marginLeft: -(thumbSize / 2),
width: thumbSize,
height: thumbSize,
borderRadius: thumbSize / 2,
borderWidth: 2,
borderColor: colors.foreground,
backgroundColor: colors.background,
}));
return (
<GestureDetector gesture={gesture}>
<View
className={cn(sliderVariants({ size }), disabled && "opacity-50", className)}
accessible={true}
accessibilityRole="adjustable"
accessibilityValue={{ min, max, now: value }}
onLayout={(e) => { trackWidth.value = e.nativeEvent.layout.width; }}
{...props}
>
<View className="h-1.5 w-full rounded-full bg-secondary overflow-hidden">
<Animated.View className="h-full w-full rounded-full bg-primary" style={fillStyle} />
</View>
<Animated.View style={thumbStyle} />
</View>
</GestureDetector>
);
}

