Skip to main content
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,
      }}
    />
  );
}

Parallax Scrolling

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>
    </>
  );
}

Floating Action Button

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()]} />

getTranslateTransform()

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();

extractOffset()

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;
}

Performance Tips

  • 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

Build docs developers (and LLMs) love