diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx
index 18c958a4bb..85ea0b6228 100644
--- a/example/src/ExampleList.tsx
+++ b/example/src/ExampleList.tsx
@@ -36,6 +36,7 @@ import SearchbarExample from './Examples/SearchbarExample';
import SegmentedButtonMultiselectRealCase from './Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase';
import SegmentedButtonRealCase from './Examples/SegmentedButtons/SegmentedButtonRealCase';
import SegmentedButtonExample from './Examples/SegmentedButtonsExample';
+import SliderExample from './Examples/SliderExample';
import SnackbarExample from './Examples/SnackbarExample';
import SurfaceExample from './Examples/SurfaceExample';
import SwitchExample from './Examples/SwitchExample';
@@ -79,6 +80,7 @@ export const mainExamples = {
RadioItem: RadioButtonItemExample,
Searchbar: SearchbarExample,
SegmentedButton: SegmentedButtonExample,
+ Slider: SliderExample,
Snackbar: SnackbarExample,
Surface: SurfaceExample,
Switch: SwitchExample,
diff --git a/example/src/Examples/SliderExample.tsx b/example/src/Examples/SliderExample.tsx
new file mode 100644
index 0000000000..76cb3d052d
--- /dev/null
+++ b/example/src/Examples/SliderExample.tsx
@@ -0,0 +1,240 @@
+import * as React from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import { Slider, Switch, Text, useTheme } from 'react-native-paper';
+
+import ScreenWrapper from '../ScreenWrapper';
+
+type SliderSize = 'xs' | 's' | 'm' | 'l' | 'xl';
+const SIZES: SliderSize[] = ['xs', 's', 'm', 'l', 'xl'];
+
+const Section = ({
+ title,
+ children,
+}: {
+ title: string;
+ children: React.ReactNode;
+}) => {
+ const theme = useTheme();
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+};
+
+const Row = ({
+ label,
+ children,
+}: {
+ label: string;
+ children: React.ReactNode;
+}) => (
+
+ {label}
+ {children}
+
+);
+
+const SliderExample = () => {
+ const theme = useTheme();
+
+ const [standardValue, setStandardValue] = React.useState(40);
+ const [centeredValue, setCenteredValue] = React.useState(0);
+ const [rangeValue, setRangeValue] = React.useState<[number, number]>([
+ 20, 75,
+ ]);
+
+ const [size, setSize] = React.useState('m');
+ const [vertical, setVertical] = React.useState(false);
+ const [showIcon, setShowIcon] = React.useState(false);
+ const [showStops, setShowStops] = React.useState(false);
+ const [showValueIndicator, setShowValueIndicator] = React.useState(false);
+ const [disabled, setDisabled] = React.useState(false);
+
+ const iconSource = showIcon ? 'volume-high' : undefined;
+ const orientation = vertical ? 'vertical' : 'horizontal';
+
+ return (
+
+ {/* Controls */}
+
+
+
+ {SIZES.map((s) => (
+ setSize(s)}
+ style={[
+ styles.sizeChip,
+ {
+ backgroundColor:
+ size === s
+ ? theme.colors.primaryContainer
+ : theme.colors.surfaceVariant,
+ color:
+ size === s
+ ? theme.colors.onPrimaryContainer
+ : theme.colors.onSurfaceVariant,
+ },
+ ]}
+ >
+ {s.toUpperCase()}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Sliders */}
+
+
+
+
+ Value: {standardValue}
+
+
+
+
+
+
+ Value: {centeredValue}
+
+
+
+
+
+
+
+ Range: {rangeValue[0]} - {rangeValue[1]}
+
+
+
+ );
+};
+
+SliderExample.title = 'Slider';
+
+const styles = StyleSheet.create({
+ container: {
+ paddingVertical: 8,
+ },
+ section: {
+ marginBottom: 24,
+ paddingHorizontal: 16,
+ },
+ sectionTitle: {
+ marginBottom: 12,
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ },
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 6,
+ },
+ label: {
+ flex: 1,
+ },
+ sizeRow: {
+ flexDirection: 'row',
+ gap: 6,
+ },
+ sizeChip: {
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ borderRadius: 8,
+ overflow: 'hidden',
+ fontSize: 12,
+ fontWeight: '600',
+ },
+ sliderArea: {
+ marginTop: 8,
+ marginBottom: 4,
+ },
+ sliderAreaVertical: {
+ height: 200,
+ alignItems: 'flex-start',
+ },
+ slider: {
+ flex: 1,
+ },
+ sliderVertical: {
+ height: '100%',
+ },
+ valueText: {
+ opacity: 0.6,
+ fontSize: 13,
+ marginTop: 4,
+ },
+});
+
+export default SliderExample;
diff --git a/src/components/Slider/Slider.tsx b/src/components/Slider/Slider.tsx
new file mode 100644
index 0000000000..fb2a7b1d9f
--- /dev/null
+++ b/src/components/Slider/Slider.tsx
@@ -0,0 +1,965 @@
+import * as React from 'react';
+import {
+ PanResponder,
+ StyleSheet,
+ View,
+ type ColorValue,
+ type StyleProp,
+ type ViewStyle,
+} from 'react-native';
+
+import Animated, {
+ ReduceMotion,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
+
+import {
+ BETWEEN_HANDLE_SPACE,
+ DISABLED_CONTENT_OPACITY,
+ DISABLED_INACTIVE_OPACITY,
+ INNER_CORNER_RADIUS,
+ SIZE_SPECS,
+ STOP_SIZE,
+ VALUE_INDICATOR_BOTTOM_SPACE,
+ VALUE_INDICATOR_SIZE,
+ SliderTokens,
+ type SliderSize,
+} from './tokens';
+import {
+ fractionToValue,
+ nearestHandle,
+ positionToFraction,
+ stopFractions,
+ valueToFraction,
+} from './utils';
+import { useLocale } from '../../core/locale';
+import { useInternalTheme } from '../../core/theming';
+import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
+import { cornerFull } from '../../theme/tokens/sys/shape';
+import type { ThemeProp } from '../../types';
+import useLayout from '../../utils/useLayout';
+import Icon, { type IconSource } from '../Icon';
+import Text from '../Typography/Text';
+
+type BaseProps = {
+ min?: number;
+ max?: number;
+ step?: number;
+ size?: SliderSize;
+ orientation?: 'horizontal' | 'vertical';
+ disabled?: boolean;
+ icon?: IconSource;
+ showStops?: boolean;
+ showValueIndicator?: boolean;
+ valueIndicatorLabel?: (v: number) => string;
+ color?: ColorValue;
+ style?: StyleProp;
+ testID?: string;
+ theme?: ThemeProp;
+ accessibilityLabel?: string;
+};
+
+type StandardProps = BaseProps & {
+ variant?: 'standard';
+ value: number;
+ onValueChange?: (v: number) => void;
+ onSlidingStart?: (v: number) => void;
+ onSlidingComplete?: (v: number) => void;
+};
+
+type CenteredProps = BaseProps & {
+ variant: 'centered';
+ value: number;
+ onValueChange?: (v: number) => void;
+ onSlidingStart?: (v: number) => void;
+ onSlidingComplete?: (v: number) => void;
+};
+
+type RangeProps = BaseProps & {
+ variant: 'range';
+ value: [number, number];
+ onValueChange?: (v: [number, number]) => void;
+ onSlidingStart?: (v: [number, number]) => void;
+ onSlidingComplete?: (v: [number, number]) => void;
+};
+
+export type Props = StandardProps | CenteredProps | RangeProps;
+
+/**
+ * Material 3 slider for selecting a value from a range.
+ * Supports standard, centered, and range variants.
+ *
+ * ## Usage
+ * ```js
+ * import * as React from 'react';
+ * import { Slider } from 'react-native-paper';
+ *
+ * const Example = () => {
+ * const [value, setValue] = React.useState(50);
+ * return ;
+ * };
+ * ```
+ */
+const Slider = (props: Props) => {
+ const {
+ min = 0,
+ max = 100,
+ step = 0,
+ size = 's',
+ orientation = 'horizontal',
+ disabled = false,
+ icon,
+ showStops = false,
+ showValueIndicator = false,
+ valueIndicatorLabel,
+ color,
+ style,
+ testID,
+ theme: themeOverrides,
+ accessibilityLabel,
+ } = props;
+
+ const variant = props.variant ?? 'standard';
+ const isRange = variant === 'range';
+
+ const theme = useInternalTheme(themeOverrides);
+ const reduceMotion = useReduceMotion();
+ const { direction } = useLocale();
+ const isRTL = direction === 'rtl';
+ const isVertical = orientation === 'vertical';
+
+ const spec = SIZE_SPECS[size];
+
+ const colors = React.useMemo(() => {
+ const t = SliderTokens.colors;
+ const c = theme.colors;
+ return {
+ activeTrack: color ?? c[t.activeTrack],
+ inactiveTrack: c[t.inactiveTrack],
+ handle: color ?? c[t.handle],
+ stopOnActive: c[t.stopOnActive],
+ stopOnInactive: c[t.stopOnInactive],
+ valueIndicatorBg: c[t.valueIndicatorBg],
+ valueIndicatorText: c[t.valueIndicatorText],
+ disabledContent: c[t.disabledContent],
+ };
+ }, [theme, color]);
+
+ const reanimatedReduceMotion = reduceMotion
+ ? ReduceMotion.Always
+ : ReduceMotion.Never;
+
+ const timingConfig = React.useMemo(
+ () => ({ duration: 100, reduceMotion: reanimatedReduceMotion }),
+ [reanimatedReduceMotion]
+ );
+
+ const initialEndValue = isRange
+ ? (props as RangeProps).value[1]
+ : (props as StandardProps | CenteredProps).value;
+ const initialStartValue = isRange ? (props as RangeProps).value[0] : min;
+
+ const [endValue, setEndValue] = React.useState(initialEndValue);
+ const [startValue, setStartValue] = React.useState(initialStartValue);
+ const [indicatorDisplayValue, setIndicatorDisplayValue] =
+ React.useState(initialEndValue);
+
+ React.useEffect(() => {
+ if (isRange) {
+ const [s, e] = (props as RangeProps).value;
+ setStartValue(s);
+ setEndValue(e);
+ } else {
+ const v = (props as StandardProps | CenteredProps).value;
+ setEndValue(v);
+ }
+ }, [props, isRange]);
+
+ const [layout, onLayout] = useLayout();
+ const trackLength = isVertical ? layout.height : layout.width;
+
+ // Extra space above the track for the value indicator bubble (horizontal only)
+ const verticalOffset =
+ showValueIndicator && !isVertical
+ ? VALUE_INDICATOR_SIZE + VALUE_INDICATOR_BOTTOM_SPACE
+ : 0;
+
+ const trackLengthSV = useSharedValue(0);
+ const endFractionSV = useSharedValue(valueToFraction(endValue, min, max));
+ const startFractionSV = useSharedValue(valueToFraction(startValue, min, max));
+ const endHandleWidthSV = useSharedValue(spec.handleWidth);
+ const startHandleWidthSV = useSharedValue(spec.handleWidth);
+ const valueIndicatorAlphaSV = useSharedValue(0);
+
+ React.useEffect(() => {
+ trackLengthSV.value = trackLength;
+ }, [trackLength, trackLengthSV]);
+
+ React.useEffect(() => {
+ if (!isRange) {
+ endFractionSV.value = valueToFraction(endValue, min, max);
+ }
+ }, [endValue, min, max, isRange, endFractionSV]);
+
+ React.useEffect(() => {
+ if (isRange) {
+ startFractionSV.value = valueToFraction(startValue, min, max);
+ endFractionSV.value = valueToFraction(endValue, min, max);
+ }
+ }, [startValue, endValue, min, max, isRange, startFractionSV, endFractionSV]);
+
+ // gap = half handle width + between-handle space
+ const trackHandleOffset = spec.handleWidth / 2 + BETWEEN_HANDLE_SPACE;
+
+ // Active track: gaps on handle sides, 2dp inner corners
+ const activeTrackAnimStyle = useAnimatedStyle(() => {
+ const vf = endFractionSV.value;
+ const sf = startFractionSV.value;
+ const len = trackLengthSV.value;
+ const gap = trackHandleOffset;
+
+ if (isVertical) {
+ let bottom: number;
+ let height: number;
+
+ if (variant === 'range') {
+ const lo = Math.min(sf, vf);
+ const hi = Math.max(sf, vf);
+ bottom = lo * len + gap;
+ height = Math.max(0, (hi - lo) * len - 2 * gap);
+ } else if (variant === 'centered') {
+ if (vf >= 0.5) {
+ bottom = 0.5 * len;
+ height = Math.max(0, (vf - 0.5) * len - gap);
+ } else {
+ bottom = vf * len + gap;
+ height = Math.max(0, (0.5 - vf) * len - gap);
+ }
+ } else {
+ bottom = 0;
+ height = Math.max(0, vf * len - gap);
+ }
+
+ return {
+ bottom,
+ height,
+ top: undefined,
+ left: 0,
+ right: 0,
+ width: undefined,
+ };
+ }
+
+ let left: number;
+ let width: number;
+
+ if (variant === 'range') {
+ const lo = Math.min(sf, vf);
+ const hi = Math.max(sf, vf);
+ left = lo * len + gap;
+ width = Math.max(0, (hi - lo) * len - 2 * gap);
+ } else if (variant === 'centered') {
+ if (vf >= 0.5) {
+ left = 0.5 * len;
+ width = Math.max(0, (vf - 0.5) * len - gap);
+ } else {
+ left = vf * len + gap;
+ width = Math.max(0, (0.5 - vf) * len - gap);
+ }
+ } else {
+ left = 0;
+ width = Math.max(0, vf * len - gap);
+ }
+
+ return {
+ left,
+ width,
+ top: 0,
+ bottom: 0,
+ right: undefined,
+ height: undefined,
+ };
+ });
+
+ // Right inactive track: from (handle + gap) to track end, 2dp inner left corners
+ const rightInactiveAnimStyle = useAnimatedStyle(() => {
+ const vf = endFractionSV.value;
+ const sf = startFractionSV.value;
+ const len = trackLengthSV.value;
+ const gap = trackHandleOffset;
+
+ if (isVertical) {
+ let height: number;
+ if (variant === 'range') {
+ const hi = Math.max(sf, vf);
+ height = Math.max(0, (1 - hi) * len - gap);
+ } else if (variant === 'centered') {
+ height =
+ vf >= 0.5
+ ? Math.max(0, (1 - vf) * len - gap)
+ : Math.max(0, 0.5 * len);
+ } else {
+ height = Math.max(0, (1 - vf) * len - gap);
+ }
+ return {
+ top: 0,
+ height,
+ bottom: undefined,
+ left: 0,
+ right: 0,
+ width: undefined,
+ };
+ }
+
+ let left: number;
+ let width: number;
+ if (variant === 'range') {
+ const hi = Math.max(sf, vf);
+ left = hi * len + gap;
+ width = Math.max(0, (1 - hi) * len - gap);
+ } else if (variant === 'centered') {
+ if (vf >= 0.5) {
+ left = vf * len + gap;
+ width = Math.max(0, (1 - vf) * len - gap);
+ } else {
+ left = 0.5 * len;
+ width = Math.max(0, 0.5 * len);
+ }
+ } else {
+ left = vf * len + gap;
+ width = Math.max(0, (1 - vf) * len - gap);
+ }
+ return {
+ left,
+ width,
+ top: 0,
+ bottom: 0,
+ right: undefined,
+ height: undefined,
+ };
+ });
+
+ // Left inactive track: from track start to (handle - gap), 2dp inner right corners
+ // Only needed for centered and range variants.
+ const leftInactiveAnimStyle = useAnimatedStyle(() => {
+ if (variant === 'standard') {
+ return { width: 0 };
+ }
+
+ const vf = endFractionSV.value;
+ const sf = startFractionSV.value;
+ const len = trackLengthSV.value;
+ const gap = trackHandleOffset;
+
+ if (isVertical) {
+ let height: number;
+ if (variant === 'range') {
+ const lo = Math.min(sf, vf);
+ height = Math.max(0, lo * len - gap);
+ } else {
+ // centered
+ height =
+ vf >= 0.5 ? Math.max(0, 0.5 * len) : Math.max(0, vf * len - gap);
+ }
+ return {
+ bottom: 0,
+ height,
+ top: undefined,
+ left: 0,
+ right: 0,
+ width: undefined,
+ };
+ }
+
+ let width: number;
+ if (variant === 'range') {
+ const lo = Math.min(sf, vf);
+ width = Math.max(0, lo * len - gap);
+ } else {
+ // centered
+ width = vf >= 0.5 ? Math.max(0, 0.5 * len) : Math.max(0, vf * len - gap);
+ }
+ return {
+ left: 0,
+ width,
+ top: 0,
+ bottom: 0,
+ right: undefined,
+ height: undefined,
+ };
+ });
+
+ // End handle animated position along the track axis
+ const endHandleAnimStyle = useAnimatedStyle(() => {
+ const len = trackLengthSV.value;
+ const w = endHandleWidthSV.value;
+ if (isVertical) {
+ const pos = (1 - endFractionSV.value) * len;
+ return { top: pos - w / 2, height: w };
+ }
+ const pos = endFractionSV.value * len;
+ return { left: pos - w / 2, width: w };
+ });
+
+ // Start handle animated position along the track axis
+ const startHandleAnimStyle = useAnimatedStyle(() => {
+ const len = trackLengthSV.value;
+ const w = startHandleWidthSV.value;
+ if (isVertical) {
+ const pos = (1 - startFractionSV.value) * len;
+ return { top: pos - w / 2, height: w };
+ }
+ const pos = startFractionSV.value * len;
+ return { left: pos - w / 2, width: w };
+ });
+
+ // Value indicator animated style
+ const valueIndicatorAnimStyle = useAnimatedStyle(() => {
+ const len = trackLengthSV.value;
+ if (isVertical) {
+ const pos = (1 - endFractionSV.value) * len;
+ return {
+ opacity: valueIndicatorAlphaSV.value,
+ top: pos - VALUE_INDICATOR_SIZE / 2,
+ left: -(VALUE_INDICATOR_SIZE + VALUE_INDICATOR_BOTTOM_SPACE),
+ right: undefined,
+ bottom: undefined,
+ transform: [],
+ };
+ }
+ return {
+ opacity: valueIndicatorAlphaSV.value,
+ transform: [
+ { translateX: endFractionSV.value * len - VALUE_INDICATOR_SIZE / 2 },
+ ],
+ };
+ });
+
+ // Gesture handling
+ const valuesRef = React.useRef({
+ min,
+ max,
+ step,
+ trackLength,
+ endValue,
+ startValue,
+ variant,
+ isRTL,
+ isVertical,
+ });
+ valuesRef.current = {
+ min,
+ max,
+ step,
+ trackLength,
+ endValue,
+ startValue,
+ variant,
+ isRTL,
+ isVertical,
+ };
+
+ const grantTouchRef = React.useRef(0);
+ const activeHandleRef = React.useRef<'start' | 'end'>('end');
+
+ const panResponder = React.useRef(
+ PanResponder.create({
+ onStartShouldSetPanResponder: () => !disabled,
+ onMoveShouldSetPanResponder: () => !disabled,
+
+ onPanResponderGrant: (evt) => {
+ const {
+ min: mn,
+ max: mx,
+ isVertical: iv,
+ isRTL: rtl,
+ trackLength: tl,
+ startValue: sv,
+ endValue: ev,
+ variant: vt,
+ } = valuesRef.current;
+ const touchPx = iv
+ ? evt.nativeEvent.locationY
+ : evt.nativeEvent.locationX;
+ grantTouchRef.current = touchPx;
+
+ if (vt === 'range') {
+ const f = positionToFraction(touchPx, tl, rtl, iv);
+ activeHandleRef.current = nearestHandle(
+ f,
+ valueToFraction(sv, mn, mx),
+ valueToFraction(ev, mn, mx)
+ );
+ } else {
+ activeHandleRef.current = 'end';
+ }
+
+ if (activeHandleRef.current === 'start') {
+ startHandleWidthSV.value = withTiming(
+ spec.handlePressWidth,
+ timingConfig
+ );
+ } else {
+ endHandleWidthSV.value = withTiming(
+ spec.handlePressWidth,
+ timingConfig
+ );
+ }
+
+ if (showValueIndicator) {
+ valueIndicatorAlphaSV.value = withTiming(1, timingConfig);
+ }
+
+ if (props.onSlidingStart) {
+ if (isRange) {
+ (props as RangeProps).onSlidingStart?.([sv, ev]);
+ } else {
+ (props as StandardProps | CenteredProps).onSlidingStart?.(ev);
+ }
+ }
+ },
+
+ onPanResponderMove: (_, gestureState) => {
+ const {
+ min: mn,
+ max: mx,
+ step: st,
+ trackLength: tl,
+ startValue: sv,
+ endValue: ev,
+ variant: vt,
+ isVertical: iv,
+ isRTL: rtl,
+ } = valuesRef.current;
+
+ const delta = iv ? gestureState.dy : gestureState.dx;
+ const touchPx = grantTouchRef.current + delta;
+ const fraction = positionToFraction(touchPx, tl, rtl, iv);
+ const snapped = fractionToValue(fraction, mn, mx, st);
+ // Snap the visual handle position for discrete mode
+ const displayFraction =
+ st > 0 ? valueToFraction(snapped, mn, mx) : fraction;
+
+ if (vt === 'range') {
+ if (activeHandleRef.current === 'start') {
+ const clamped = Math.min(snapped, ev);
+ startFractionSV.value = valueToFraction(clamped, mn, mx);
+ setStartValue(clamped);
+ setIndicatorDisplayValue(clamped);
+ (props as RangeProps).onValueChange?.([clamped, ev]);
+ } else {
+ const clamped = Math.max(snapped, sv);
+ endFractionSV.value = valueToFraction(clamped, mn, mx);
+ setEndValue(clamped);
+ setIndicatorDisplayValue(clamped);
+ (props as RangeProps).onValueChange?.([sv, clamped]);
+ }
+ } else {
+ endFractionSV.value = displayFraction;
+ setEndValue(snapped);
+ setIndicatorDisplayValue(snapped);
+ (props as StandardProps | CenteredProps).onValueChange?.(snapped);
+ }
+ },
+
+ onPanResponderRelease: () => {
+ const { endValue: ev, startValue: sv } = valuesRef.current;
+
+ if (activeHandleRef.current === 'start') {
+ startHandleWidthSV.value = withTiming(spec.handleWidth, timingConfig);
+ } else {
+ endHandleWidthSV.value = withTiming(spec.handleWidth, timingConfig);
+ }
+
+ if (showValueIndicator) {
+ valueIndicatorAlphaSV.value = withTiming(0, timingConfig);
+ }
+
+ if (props.onSlidingComplete) {
+ if (isRange) {
+ (props as RangeProps).onSlidingComplete?.([sv, ev]);
+ } else {
+ (props as StandardProps | CenteredProps).onSlidingComplete?.(ev);
+ }
+ }
+ },
+
+ onPanResponderTerminate: () => {
+ if (activeHandleRef.current === 'start') {
+ startHandleWidthSV.value = withTiming(spec.handleWidth, timingConfig);
+ } else {
+ endHandleWidthSV.value = withTiming(spec.handleWidth, timingConfig);
+ }
+ if (showValueIndicator) {
+ valueIndicatorAlphaSV.value = withTiming(0, timingConfig);
+ }
+ },
+ })
+ ).current;
+
+ // Accessibility
+ const handleAccessibilityIncrement = () => {
+ if (disabled) return;
+ const increment = step > 0 ? step : (max - min) / 100;
+ const next = Math.min(endValue + increment, max);
+ setEndValue(next);
+ endFractionSV.value = valueToFraction(next, min, max);
+ if (!isRange) {
+ (props as StandardProps | CenteredProps).onValueChange?.(next);
+ }
+ };
+
+ const handleAccessibilityDecrement = () => {
+ if (disabled) return;
+ const decrement = step > 0 ? step : (max - min) / 100;
+ const next = Math.max(endValue - decrement, min);
+ setEndValue(next);
+ endFractionSV.value = valueToFraction(next, min, max);
+ if (!isRange) {
+ (props as StandardProps | CenteredProps).onValueChange?.(next);
+ }
+ };
+
+ // End stops are always visible at the track's min/max positions.
+ // Intermediate step tick marks are only shown when showStops && step > 0,
+ // and skip fractions 0 and 1 since those are covered by the end stops.
+ const endStopFractions: number[] = trackLength > 0 ? [0, 1] : [];
+ const stepTickFractions: number[] =
+ showStops && step > 0 && trackLength > 0
+ ? stopFractions(min, max, step).filter((f) => f > 0.001 && f < 0.999)
+ : [];
+ const allStopFractions = [...endStopFractions, ...stepTickFractions];
+
+ // Active segment bounds for stop color determination
+ const endFraction = valueToFraction(endValue, min, max);
+ const startFraction = isRange ? valueToFraction(startValue, min, max) : 0;
+ const activeLead =
+ variant === 'centered'
+ ? Math.min(0.5, endFraction)
+ : variant === 'range'
+ ? Math.min(startFraction, endFraction)
+ : 0;
+ const activeTrail =
+ variant === 'centered'
+ ? Math.max(0.5, endFraction)
+ : variant === 'range'
+ ? Math.max(startFraction, endFraction)
+ : endFraction;
+
+ const indicatorText = valueIndicatorLabel
+ ? valueIndicatorLabel(indicatorDisplayValue)
+ : String(Math.round(indicatorDisplayValue));
+
+ const contentOpacity = disabled ? DISABLED_CONTENT_OPACITY : 1;
+ const inactiveOpacity = disabled ? DISABLED_INACTIVE_OPACITY : 1;
+
+ // Track container: positioned within the handle area, offset for indicator zone
+ const trackContainerStyle: ViewStyle = isVertical
+ ? {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: (spec.handleHeight - spec.trackThickness) / 2,
+ width: spec.trackThickness,
+ }
+ : {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: verticalOffset + (spec.handleHeight - spec.trackThickness) / 2,
+ height: spec.trackThickness,
+ };
+
+ // Corner radii for each track segment
+ // Outer corners use the size spec radii; inner corners (facing the handle gap) use 2dp.
+ const leftInactiveCorners: ViewStyle = isVertical
+ ? {
+ borderBottomLeftRadius: spec.activeLeadingRadius,
+ borderBottomRightRadius: spec.activeLeadingRadius,
+ borderTopLeftRadius: INNER_CORNER_RADIUS,
+ borderTopRightRadius: INNER_CORNER_RADIUS,
+ }
+ : {
+ borderTopLeftRadius: spec.activeLeadingRadius,
+ borderBottomLeftRadius: spec.activeLeadingRadius,
+ borderTopRightRadius: INNER_CORNER_RADIUS,
+ borderBottomRightRadius: INNER_CORNER_RADIUS,
+ };
+
+ const rightInactiveCorners: ViewStyle = isVertical
+ ? {
+ borderBottomLeftRadius: INNER_CORNER_RADIUS,
+ borderBottomRightRadius: INNER_CORNER_RADIUS,
+ borderTopLeftRadius: spec.inactiveTrailingRadius,
+ borderTopRightRadius: spec.inactiveTrailingRadius,
+ }
+ : {
+ borderTopLeftRadius: INNER_CORNER_RADIUS,
+ borderBottomLeftRadius: INNER_CORNER_RADIUS,
+ borderTopRightRadius: spec.inactiveTrailingRadius,
+ borderBottomRightRadius: spec.inactiveTrailingRadius,
+ };
+
+ // Active track: leading outer corner uses size spec (standard only), all others 2dp inner
+ const activeTrackCorners: ViewStyle = isVertical
+ ? {
+ borderBottomLeftRadius:
+ variant === 'standard'
+ ? spec.activeLeadingRadius
+ : INNER_CORNER_RADIUS,
+ borderBottomRightRadius:
+ variant === 'standard'
+ ? spec.activeLeadingRadius
+ : INNER_CORNER_RADIUS,
+ borderTopLeftRadius: INNER_CORNER_RADIUS,
+ borderTopRightRadius: INNER_CORNER_RADIUS,
+ }
+ : {
+ borderTopLeftRadius:
+ variant === 'standard'
+ ? spec.activeLeadingRadius
+ : INNER_CORNER_RADIUS,
+ borderBottomLeftRadius:
+ variant === 'standard'
+ ? spec.activeLeadingRadius
+ : INNER_CORNER_RADIUS,
+ borderTopRightRadius: INNER_CORNER_RADIUS,
+ borderBottomRightRadius: INNER_CORNER_RADIUS,
+ };
+
+ // Outer wrapper: extends for indicator space above the track (horizontal only)
+ const outerStyle: ViewStyle = isVertical
+ ? { width: spec.handleHeight, overflow: 'visible' }
+ : { height: spec.handleHeight + verticalOffset, overflow: 'visible' };
+
+ // Static handle positioning perpendicular to the track axis
+ const endHandleStaticStyle: ViewStyle = isVertical
+ ? { position: 'absolute', left: 0, right: 0 }
+ : { position: 'absolute', top: verticalOffset, bottom: 0 };
+
+ const startHandleStaticStyle: ViewStyle = isVertical
+ ? { position: 'absolute', left: 0, right: 0 }
+ : { position: 'absolute', top: verticalOffset, bottom: 0 };
+
+ // Icon: center it at the leading corner's inscribed-square focal point
+ const iconLeadingOffset = Math.max(
+ 0,
+ spec.activeLeadingRadius - spec.iconSize / 2
+ );
+
+ const inactiveColor = disabled
+ ? colors.disabledContent
+ : colors.inactiveTrack;
+ const activeColor = disabled ? colors.disabledContent : colors.activeTrack;
+
+ return (
+
+ {/* Gesture and accessibility wrapper */}
+ {
+ if (evt.nativeEvent.actionName === 'increment') {
+ handleAccessibilityIncrement();
+ } else if (evt.nativeEvent.actionName === 'decrement') {
+ handleAccessibilityDecrement();
+ }
+ }}
+ accessibilityLabel={accessibilityLabel}
+ accessibilityState={{ disabled }}
+ />
+
+ {/* Track container */}
+
+ {/* Left inactive track segment (centered/range only) */}
+ {variant !== 'standard' && (
+
+ )}
+
+ {/* Right inactive track segment */}
+
+
+ {/* Active track segment */}
+
+
+ {/* Stop indicators: always-on end stops + optional intermediate step ticks */}
+ {allStopFractions.map((f) => {
+ const isActive = f >= activeLead && f <= activeTrail;
+ // Stops are centered at the corner arc center:
+ // f=0 -> spec.activeLeadingRadius from leading edge
+ // f=1 -> spec.inactiveTrailingRadius from trailing edge
+ // intermediate -> lerp between the two
+ const innerStart = spec.activeLeadingRadius;
+ const innerEnd = trackLength - spec.inactiveTrailingRadius;
+ if (innerEnd <= innerStart) return null;
+ const pixelCenter = innerStart + f * (innerEnd - innerStart);
+ const dotStyle: ViewStyle = isVertical
+ ? {
+ position: 'absolute',
+ left: (spec.trackThickness - STOP_SIZE) / 2,
+ bottom: pixelCenter - STOP_SIZE / 2,
+ }
+ : {
+ position: 'absolute',
+ top: (spec.trackThickness - STOP_SIZE) / 2,
+ left: pixelCenter - STOP_SIZE / 2,
+ };
+ return (
+
+ );
+ })}
+
+ {/* Inset icon: positioned at the leading corner's focal point */}
+ {spec.iconSize > 0 && icon != null && !disabled && (
+
+
+
+ )}
+
+
+ {/* End handle */}
+
+
+ {/* Start handle (range only) */}
+ {isRange && (
+
+ )}
+
+ {/* Value indicator bubble */}
+ {showValueIndicator && (
+
+
+ {indicatorText}
+
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ trackSegment: {
+ position: 'absolute',
+ },
+ handle: {
+ borderRadius: cornerFull,
+ },
+ stopDot: {
+ width: STOP_SIZE,
+ height: STOP_SIZE,
+ borderRadius: STOP_SIZE / 2,
+ },
+ iconWrapper: {
+ position: 'absolute',
+ },
+ valueIndicator: {
+ position: 'absolute',
+ top: 0,
+ width: VALUE_INDICATOR_SIZE,
+ height: VALUE_INDICATOR_SIZE,
+ borderRadius: cornerFull,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+});
+
+export default Slider;
diff --git a/src/components/Slider/index.ts b/src/components/Slider/index.ts
new file mode 100644
index 0000000000..d55e4dd87e
--- /dev/null
+++ b/src/components/Slider/index.ts
@@ -0,0 +1,22 @@
+import * as React from 'react';
+
+import SliderComponent, { type Props } from './Slider';
+
+type CenteredProps = Omit, 'variant'>;
+type RangeProps = Omit, 'variant'>;
+
+const Slider = Object.assign(
+ // @component ./Slider.tsx
+ SliderComponent,
+ {
+ // @component ./Slider.tsx (variant="centered")
+ Centered: (props: CenteredProps) =>
+ React.createElement(SliderComponent, { ...props, variant: 'centered' }),
+ // @component ./Slider.tsx (variant="range")
+ Range: (props: RangeProps) =>
+ React.createElement(SliderComponent, { ...props, variant: 'range' }),
+ }
+);
+
+export default Slider;
+export type { Props as SliderProps } from './Slider';
diff --git a/src/components/Slider/tokens.ts b/src/components/Slider/tokens.ts
new file mode 100644
index 0000000000..f101ff7633
--- /dev/null
+++ b/src/components/Slider/tokens.ts
@@ -0,0 +1,89 @@
+import type { ColorRole } from '../../theme/types';
+
+export type SliderSize = 'xs' | 's' | 'm' | 'l' | 'xl';
+
+type SizeSpec = {
+ trackThickness: number;
+ handleHeight: number;
+ handleWidth: number;
+ handlePressWidth: number;
+ activeLeadingRadius: number;
+ inactiveTrailingRadius: number;
+ iconSize: number;
+ iconPadding: number;
+};
+
+export const SIZE_SPECS: Record = {
+ xs: {
+ trackThickness: 16,
+ handleHeight: 44,
+ handleWidth: 4,
+ handlePressWidth: 2,
+ activeLeadingRadius: 8,
+ inactiveTrailingRadius: 8,
+ iconSize: 0,
+ iconPadding: 0,
+ },
+ s: {
+ trackThickness: 24,
+ handleHeight: 44,
+ handleWidth: 4,
+ handlePressWidth: 2,
+ activeLeadingRadius: 8,
+ inactiveTrailingRadius: 8,
+ iconSize: 0,
+ iconPadding: 0,
+ },
+ m: {
+ trackThickness: 40,
+ handleHeight: 44,
+ handleWidth: 4,
+ handlePressWidth: 2,
+ activeLeadingRadius: 12,
+ inactiveTrailingRadius: 6,
+ iconSize: 24,
+ iconPadding: 6,
+ },
+ l: {
+ trackThickness: 56,
+ handleHeight: 68,
+ handleWidth: 4,
+ handlePressWidth: 2,
+ activeLeadingRadius: 16,
+ inactiveTrailingRadius: 16,
+ iconSize: 24,
+ iconPadding: 6,
+ },
+ xl: {
+ trackThickness: 96,
+ handleHeight: 108,
+ handleWidth: 4,
+ handlePressWidth: 2,
+ activeLeadingRadius: 28,
+ inactiveTrailingRadius: 28,
+ iconSize: 32,
+ iconPadding: 8,
+ },
+} as const;
+
+export const STOP_SIZE = 4;
+export const BETWEEN_HANDLE_SPACE = 4;
+export const INNER_CORNER_RADIUS = 2;
+export const VALUE_INDICATOR_SIZE = 44;
+export const VALUE_INDICATOR_BOTTOM_SPACE = 12;
+
+export const DISABLED_CONTENT_OPACITY = 0.38;
+export const DISABLED_INACTIVE_OPACITY = 0.12;
+
+const colors = {
+ activeTrack: 'primary',
+ inactiveTrack: 'secondaryContainer',
+ handle: 'primary',
+ stopOnActive: 'onPrimary',
+ stopOnInactive: 'onSecondaryContainer',
+ valueIndicatorBg: 'inverseSurface',
+ valueIndicatorText: 'inverseOnSurface',
+ disabledContent: 'onSurface',
+} as const satisfies Record;
+
+export const SliderTokens = { colors };
diff --git a/src/components/Slider/utils.ts b/src/components/Slider/utils.ts
new file mode 100644
index 0000000000..41769af863
--- /dev/null
+++ b/src/components/Slider/utils.ts
@@ -0,0 +1,92 @@
+export function clamp(value: number, min: number, max: number): number {
+ return Math.min(Math.max(value, min), max);
+}
+
+export function snapToStep(
+ value: number,
+ min: number,
+ max: number,
+ step: number
+): number {
+ if (step <= 0) return clamp(value, min, max);
+ const snapped = Math.round((value - min) / step) * step + min;
+ return clamp(snapped, min, max);
+}
+
+export function valueToFraction(
+ value: number,
+ min: number,
+ max: number
+): number {
+ if (max === min) return 0;
+ return clamp((value - min) / (max - min), 0, 1);
+}
+
+export function fractionToValue(
+ fraction: number,
+ min: number,
+ max: number,
+ step: number
+): number {
+ const raw = min + clamp(fraction, 0, 1) * (max - min);
+ return snapToStep(raw, min, max, step);
+}
+
+export function positionToFraction(
+ touchPx: number,
+ trackLengthPx: number,
+ isRTL: boolean,
+ isVertical: boolean
+): number {
+ if (trackLengthPx <= 0) return 0;
+ let fraction = touchPx / trackLengthPx;
+ // Vertical: top of track = max, bottom = min (invert)
+ if (isVertical) fraction = 1 - fraction;
+ // RTL horizontal: invert
+ if (!isVertical && isRTL) fraction = 1 - fraction;
+ return clamp(fraction, 0, 1);
+}
+
+export function stopFractions(
+ min: number,
+ max: number,
+ step: number
+): number[] {
+ if (step <= 0 || max <= min) return [];
+ const fractions: number[] = [];
+ let v = min;
+ while (v <= max + Number.EPSILON) {
+ fractions.push(valueToFraction(v, min, max));
+ v += step;
+ }
+ return fractions;
+}
+
+export type SliderVariant = 'standard' | 'centered' | 'range';
+
+export function activeSegment(
+ variant: SliderVariant,
+ valueFraction: number,
+ startFraction: number
+): [number, number] {
+ if (variant === 'range') {
+ return [
+ Math.min(startFraction, valueFraction),
+ Math.max(startFraction, valueFraction),
+ ];
+ }
+ if (variant === 'centered') {
+ return [Math.min(0.5, valueFraction), Math.max(0.5, valueFraction)];
+ }
+ return [0, valueFraction];
+}
+
+export function nearestHandle(
+ touchFraction: number,
+ startFraction: number,
+ endFraction: number
+): 'start' | 'end' {
+ const distStart = Math.abs(touchFraction - startFraction);
+ const distEnd = Math.abs(touchFraction - endFraction);
+ return distStart < distEnd ? 'start' : 'end';
+}
diff --git a/src/components/__tests__/Slider.test.tsx b/src/components/__tests__/Slider.test.tsx
new file mode 100644
index 0000000000..0abed3a919
--- /dev/null
+++ b/src/components/__tests__/Slider.test.tsx
@@ -0,0 +1,205 @@
+import * as React from 'react';
+
+import { render } from '../../test-utils';
+import Slider from '../Slider';
+import {
+ activeSegment,
+ fractionToValue,
+ nearestHandle,
+ positionToFraction,
+ snapToStep,
+ stopFractions,
+ valueToFraction,
+} from '../Slider/utils';
+
+// ---- Utility unit tests ----
+
+describe('snapToStep', () => {
+ it('returns value clamped to [min, max] when step is 0', () => {
+ expect(snapToStep(150, 0, 100, 0)).toBe(100);
+ expect(snapToStep(-10, 0, 100, 0)).toBe(0);
+ expect(snapToStep(42, 0, 100, 0)).toBe(42);
+ });
+
+ it('snaps to nearest step', () => {
+ expect(snapToStep(23, 0, 100, 25)).toBe(25);
+ expect(snapToStep(12, 0, 100, 25)).toBe(0);
+ expect(snapToStep(38, 0, 100, 25)).toBe(50);
+ expect(snapToStep(63, 0, 100, 25)).toBe(75);
+ });
+
+ it('clamps snapped value to bounds', () => {
+ expect(snapToStep(99, 0, 100, 25)).toBe(100);
+ expect(snapToStep(1, 0, 100, 25)).toBe(0);
+ });
+});
+
+describe('valueToFraction', () => {
+ it('maps min to 0 and max to 1', () => {
+ expect(valueToFraction(0, 0, 100)).toBe(0);
+ expect(valueToFraction(100, 0, 100)).toBe(1);
+ });
+
+ it('maps midpoint to 0.5', () => {
+ expect(valueToFraction(50, 0, 100)).toBe(0.5);
+ });
+
+ it('returns 0 when min === max', () => {
+ expect(valueToFraction(50, 50, 50)).toBe(0);
+ });
+});
+
+describe('fractionToValue', () => {
+ it('maps 0 to min and 1 to max', () => {
+ expect(fractionToValue(0, 0, 100, 0)).toBe(0);
+ expect(fractionToValue(1, 0, 100, 0)).toBe(100);
+ });
+
+ it('clamps out-of-range fractions', () => {
+ expect(fractionToValue(-0.5, 0, 100, 0)).toBe(0);
+ expect(fractionToValue(1.5, 0, 100, 0)).toBe(100);
+ });
+});
+
+describe('positionToFraction', () => {
+ it('maps 0 to 0 and trackLength to 1 in LTR horizontal', () => {
+ expect(positionToFraction(0, 100, false, false)).toBe(0);
+ expect(positionToFraction(100, 100, false, false)).toBe(1);
+ expect(positionToFraction(50, 100, false, false)).toBe(0.5);
+ });
+
+ it('inverts for RTL', () => {
+ expect(positionToFraction(0, 100, true, false)).toBe(1);
+ expect(positionToFraction(100, 100, true, false)).toBe(0);
+ });
+
+ it('inverts for vertical (top=high, bottom=low)', () => {
+ expect(positionToFraction(0, 100, false, true)).toBe(1);
+ expect(positionToFraction(100, 100, false, true)).toBe(0);
+ expect(positionToFraction(25, 100, false, true)).toBe(0.75);
+ });
+
+ it('returns 0 when trackLengthPx is 0', () => {
+ expect(positionToFraction(50, 0, false, false)).toBe(0);
+ });
+});
+
+describe('stopFractions', () => {
+ it('returns empty array when step is 0', () => {
+ expect(stopFractions(0, 100, 0)).toEqual([]);
+ });
+
+ it('returns correct fractions for step=25 on [0,100]', () => {
+ const fracs = stopFractions(0, 100, 25);
+ expect(fracs).toHaveLength(5);
+ expect(fracs[0]).toBeCloseTo(0);
+ expect(fracs[1]).toBeCloseTo(0.25);
+ expect(fracs[2]).toBeCloseTo(0.5);
+ expect(fracs[3]).toBeCloseTo(0.75);
+ expect(fracs[4]).toBeCloseTo(1);
+ });
+
+ it('returns empty array when max <= min', () => {
+ expect(stopFractions(100, 100, 25)).toEqual([]);
+ });
+});
+
+describe('activeSegment', () => {
+ it('standard: returns [0, valueFraction]', () => {
+ expect(activeSegment('standard', 0.6, 0)).toEqual([0, 0.6]);
+ expect(activeSegment('standard', 0, 0)).toEqual([0, 0]);
+ });
+
+ it('centered: returns segment between 0.5 and valueFraction', () => {
+ expect(activeSegment('centered', 0.7, 0)).toEqual([0.5, 0.7]);
+ expect(activeSegment('centered', 0.3, 0)).toEqual([0.3, 0.5]);
+ expect(activeSegment('centered', 0.5, 0)).toEqual([0.5, 0.5]);
+ });
+
+ it('range: returns ordered [start, end]', () => {
+ expect(activeSegment('range', 0.8, 0.2)).toEqual([0.2, 0.8]);
+ expect(activeSegment('range', 0.2, 0.8)).toEqual([0.2, 0.8]);
+ });
+});
+
+describe('nearestHandle', () => {
+ it('returns start when closer to start', () => {
+ expect(nearestHandle(0.2, 0.1, 0.9)).toBe('start');
+ });
+
+ it('returns end when closer to end', () => {
+ expect(nearestHandle(0.8, 0.1, 0.9)).toBe('end');
+ });
+
+ it('tie-breaks to end', () => {
+ expect(nearestHandle(0.5, 0.25, 0.75)).toBe('end');
+ });
+});
+
+// ---- Component render tests ----
+
+describe('Slider renders', () => {
+ it('standard slider', () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('each size', () => {
+ for (const size of ['xs', 's', 'm', 'l', 'xl'] as const) {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ }
+ });
+
+ it('centered variant', () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('range variant', () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('disabled standard', () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('with stop indicators', () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('with value indicator', () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('vertical orientation', () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('Slider.Centered shorthand', () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ it('Slider.Range shorthand', () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
+
+describe('Slider accessibility', () => {
+ it('has adjustable role with correct value', () => {
+ const { getByRole } = render();
+ const slider = getByRole('adjustable');
+ expect(slider.props.accessibilityValue).toEqual({
+ min: 0,
+ max: 100,
+ now: 42,
+ });
+ });
+});
diff --git a/src/components/__tests__/__snapshots__/Slider.test.tsx.snap b/src/components/__tests__/__snapshots__/Slider.test.tsx.snap
new file mode 100644
index 0000000000..e4fd18a3e7
--- /dev/null
+++ b/src/components/__tests__/__snapshots__/Slider.test.tsx.snap
@@ -0,0 +1,2380 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Slider renders Slider.Centered shorthand 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders Slider.Range shorthand 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders centered variant 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders disabled standard 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders each size 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders each size 2`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders each size 3`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders each size 4`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders each size 5`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders range variant 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders standard slider 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders vertical orientation 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders with stop indicators 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`Slider renders with value indicator 1`] = `
+
+
+
+
+
+
+
+
+
+ 50
+
+
+
+`;
diff --git a/src/index.tsx b/src/index.tsx
index 8863e2fa20..7c85e3a89b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -49,6 +49,7 @@ export { default as TouchableRipple } from './components/TouchableRipple/Touchab
export { default as TextInput } from './components/TextInput';
export { default as ToggleButton } from './components/ToggleButton';
export { default as SegmentedButtons } from './components/SegmentedButtons/SegmentedButtons';
+export { default as Slider } from './components/Slider';
export { default as Tooltip } from './components/Tooltip/Tooltip';
export { default as Text, customText } from './components/Typography/Text';
@@ -125,6 +126,7 @@ export type { Props as RadioButtonGroupProps } from './components/RadioButton/Ra
export type { Props as RadioButtonIOSProps } from './components/RadioButton/RadioButtonIOS';
export type { Props as RadioButtonItemProps } from './components/RadioButton/RadioButtonItem';
export type { Props as SearchbarProps } from './components/Searchbar';
+export type { Props as SliderProps } from './components/Slider/Slider';
export type { Props as SnackbarProps } from './components/Snackbar';
export type { Props as SurfaceProps } from './components/Surface';
export type { Props as SwitchProps } from './components/Switch/Switch';