Documentation Index
Fetch the complete documentation index at: https://mintlify.com/facebook/react-native/llms.txt
Use this file to discover all available pages before exploring further.
useAnimatedValueXY is a React hook that creates a persistent Animated.ValueXY instance for animating 2D positions. The animated value persists across component re-renders, making it ideal for drag-and-drop, pan gestures, and position-based animations.
Signature
function useAnimatedValueXY(
initialValue: { x: number; y: number },
config?: AnimatedConfig
): Animated.ValueXY
Parameters
initialValue - Object containing initial x and y coordinates
x - The initial x coordinate value
y - The initial y coordinate value
config (optional) - Configuration options for the animated value
useNativeDriver?: boolean - Whether to use the native driver for animations
Returns
An Animated.ValueXY instance that persists across re-renders.
Usage
import { useAnimatedValueXY } from 'react-native';
import { Animated, PanResponder, View } from 'react-native';
import { useRef } from 'react';
function DraggableBox() {
const position = useAnimatedValueXY({ x: 0, y: 0 });
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event(
[null, { dx: position.x, dy: position.y }],
{ useNativeDriver: false }
),
onPanResponderRelease: () => {
Animated.spring(position, {
toValue: { x: 0, y: 0 },
useNativeDriver: false,
}).start();
},
})
).current;
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Animated.View
{...panResponder.panHandlers}
style={{
transform: position.getTranslateTransform(),
width: 100,
height: 100,
backgroundColor: 'blue',
borderRadius: 8,
}}
/>
</View>
);
}
Common Patterns
Drag and Drop
import { useAnimatedValueXY } from 'react-native';
import { useRef } from 'react';
import { Animated, PanResponder, Dimensions } from 'react-native';
function DraggableItem() {
const position = useAnimatedValueXY({ x: 0, y: 0 });
const { width, height } = Dimensions.get('window');
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
position.setOffset({
x: (position.x as any)._value,
y: (position.y as any)._value,
});
},
onPanResponderMove: Animated.event(
[null, { dx: position.x, dy: position.y }],
{ useNativeDriver: false }
),
onPanResponderRelease: () => {
position.flattenOffset();
},
})
).current;
return (
<Animated.View
{...panResponder.panHandlers}
style={{
transform: position.getTranslateTransform(),
width: 80,
height: 80,
backgroundColor: '#4caf50',
borderRadius: 40,
}}
/>
);
}
Swipe to Dismiss
import { useAnimatedValueXY } from 'react-native';
import { useRef } from 'react';
import { Animated, PanResponder, Dimensions } from 'react-native';
function SwipeableCard({ onDismiss }: { onDismiss: () => void }) {
const position = useAnimatedValueXY({ x: 0, y: 0 });
const { width } = Dimensions.get('window');
const SWIPE_THRESHOLD = width * 0.4;
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event(
[null, { dx: position.x, dy: position.y }],
{ useNativeDriver: false }
),
onPanResponderRelease: (_, gesture) => {
if (Math.abs(gesture.dx) > SWIPE_THRESHOLD) {
// Swipe off screen
Animated.timing(position, {
toValue: { x: gesture.dx > 0 ? width : -width, y: gesture.dy },
duration: 250,
useNativeDriver: false,
}).start(onDismiss);
} else {
// Return to center
Animated.spring(position, {
toValue: { x: 0, y: 0 },
useNativeDriver: false,
}).start();
}
},
})
).current;
const rotate = position.x.interpolate({
inputRange: [-width / 2, 0, width / 2],
outputRange: ['-10deg', '0deg', '10deg'],
extrapolate: 'clamp',
});
return (
<Animated.View
{...panResponder.panHandlers}
style={{
transform: [...position.getTranslateTransform(), { rotate }],
width: 300,
height: 400,
backgroundColor: 'white',
borderRadius: 16,
shadowColor: '#000',
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
}}
/>
);
}
import { useAnimatedValueXY } from 'react-native';
import { ScrollView, Animated, Dimensions } from 'react-native';
function ParallaxHeader() {
const scrollY = useAnimatedValueXY({ x: 0, y: 0 });
const { height } = Dimensions.get('window');
const HEADER_HEIGHT = 300;
const headerTranslateY = scrollY.y.interpolate({
inputRange: [0, HEADER_HEIGHT],
outputRange: [0, -HEADER_HEIGHT / 2],
extrapolate: 'clamp',
});
const headerOpacity = scrollY.y.interpolate({
inputRange: [0, HEADER_HEIGHT / 2, HEADER_HEIGHT],
outputRange: [1, 0.5, 0],
extrapolate: 'clamp',
});
return (
<>
<Animated.View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: HEADER_HEIGHT,
backgroundColor: '#2196f3',
transform: [{ translateY: headerTranslateY }],
opacity: headerOpacity,
zIndex: 1,
}}
/>
<Animated.ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY.y } } }],
{ useNativeDriver: false }
)}
>
{/* Content */}
</Animated.ScrollView>
</>
);
}
import { useAnimatedValueXY } from 'react-native';
import { useEffect } from 'react';
import { Animated, TouchableOpacity } from 'react-native';
function FloatingButton({ onPress }: { onPress: () => void }) {
const position = useAnimatedValueXY({ x: 0, y: 100 });
useEffect(() => {
Animated.spring(position, {
toValue: { x: 0, y: 0 },
friction: 5,
tension: 40,
useNativeDriver: false,
}).start();
}, []);
return (
<Animated.View
style={{
position: 'absolute',
bottom: 20,
right: 20,
transform: position.getTranslateTransform(),
}}
>
<TouchableOpacity
onPress={onPress}
style={{
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: '#f44336',
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 6,
}}
>
{/* Icon */}
</TouchableOpacity>
</Animated.View>
);
}
Multi-touch Drag
import { useAnimatedValueXY } from 'react-native';
import { useRef, useState } from 'react';
import { Animated, PanResponder } from 'react-native';
function MultiTouchDraggable() {
const position1 = useAnimatedValueXY({ x: 0, y: 0 });
const position2 = useAnimatedValueXY({ x: 100, y: 0 });
const [activeTouch, setActiveTouch] = useState<number | null>(null);
const createPanResponder = (position: Animated.ValueXY, id: number) =>
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => setActiveTouch(id),
onPanResponderMove: Animated.event(
[null, { dx: position.x, dy: position.y }],
{ useNativeDriver: false }
),
onPanResponderRelease: () => {
setActiveTouch(null);
Animated.spring(position, {
toValue: { x: 0, y: 0 },
useNativeDriver: false,
}).start();
},
});
const panResponder1 = useRef(createPanResponder(position1, 1)).current;
const panResponder2 = useRef(createPanResponder(position2, 2)).current;
return (
<>
<Animated.View
{...panResponder1.panHandlers}
style={{
transform: position1.getTranslateTransform(),
width: 80,
height: 80,
backgroundColor: activeTouch === 1 ? '#ff5722' : '#4caf50',
borderRadius: 8,
}}
/>
<Animated.View
{...panResponder2.panHandlers}
style={{
transform: position2.getTranslateTransform(),
width: 80,
height: 80,
backgroundColor: activeTouch === 2 ? '#ff5722' : '#2196f3',
borderRadius: 8,
}}
/>
</>
);
}
ValueXY Methods
Animated.ValueXY provides several useful methods:
getLayout()
Convert to {left, top} for absolute positioning:
<Animated.View style={[{ position: 'absolute' }, position.getLayout()]} />
Convert to [{translateX}, {translateY}] for transforms:
<Animated.View style={{ transform: position.getTranslateTransform() }} />
setValue()
Directly set the value:
position.setValue({ x: 0, y: 0 });
setOffset()
Set an offset to be added to the value:
position.setOffset({ x: 100, y: 50 });
flattenOffset()
Merge offset into the base value:
position.flattenOffset();
Set offset to current value and reset value to zero:
position.extractOffset();
When to Use
Use useAnimatedValueXY when you need:
- To animate 2D positions (x, y coordinates)
- Drag-and-drop functionality
- Pan gesture handling
- Parallax scrolling effects
- Swipeable components
Alternatives
- useAnimatedValue - For single numeric values (opacity, scale, rotation)
- react-native-gesture-handler - More performant gesture handling
- react-native-reanimated - Better performance for complex animations
Implementation Details
This hook:
- Uses
useRef to store the Animated.ValueXY instance
- Creates the animated value only once on first render
- Returns the same instance on subsequent re-renders
- The initial value is only used during the first render
// Simplified implementation
function useAnimatedValueXY(
initialValue: { x: number; y: number },
config?: AnimatedConfig
) {
const ref = useRef<Animated.ValueXY | null>(null);
if (ref.current == null) {
ref.current = new Animated.ValueXY(initialValue, config);
}
return ref.current;
}
- Native Driver Limitation:
useNativeDriver: false is required for layout animations
- ScrollView: Use
scrollEventThrottle={16} for smooth 60fps scrolling
- PanResponder: Consider
react-native-gesture-handler for better performance
- Complex Animations: Use
react-native-reanimated for animations running on the UI thread
See Also