All skills
wshobson avatar

/react-native-design

@be57c0b
by Seth Hobsonwshobson/agents40k stars
4,281

Master React Native styling, navigation, and Reanimated animations for cross-platform mobile development. Use when building React Native apps, implementing navigation patterns, or creating performant animations.

Use this Skill: https://skilld.dev/gh/wshobson/agents/react-native-design

This session only. Nothing lands on disk.

referencesreanimated-patterns.md

≈4.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

React Native Reanimated 3 Patterns

Core Concepts

Shared Values and Animated Styles

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
  withDelay,
  withSequence,
  withRepeat,
  Easing,
} from 'react-native-reanimated';

function BasicAnimations() {
  // Shared value - can be modified from JS or UI thread
  const opacity = useSharedValue(0);
  const scale = useSharedValue(1);
  const rotation = useSharedValue(0);

  // Animated style - runs on UI thread
  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [
      { scale: scale.value },
      { rotate: `${rotation.value}deg` },
    ],
  }));

  const animate = () => {
    // Spring animation
    scale.value = withSpring(1.2, {
      damping: 10,
      stiffness: 100,
    });

    // Timing animation with easing
    opacity.value = withTiming(1, {
      duration: 500,
      easing: Easing.bezier(0.25, 0.1, 0.25, 1),
    });

    // Sequence of animations
    rotation.value = withSequence(
      withTiming(15, { duration: 100 }),
      withTiming(-15, { duration: 100 }),
      withTiming(0, { duration: 100 })
    );
  };

  return (
    <Animated.View style={[styles.box, animatedStyle]} />
  );
}

Animation Callbacks

import { runOnJS, runOnUI } from 'react-native-reanimated';

function AnimationWithCallbacks() {
  const translateX = useSharedValue(0);
  const [status, setStatus] = useState('idle');

  const updateStatus = (newStatus: string) => {
    setStatus(newStatus);
  };

  const animate = () => {
    translateX.value = withTiming(
      200,
      { duration: 1000 },
      (finished) => {
        'worklet';
        if (finished) {
          // Call JS function from worklet
          runOnJS(updateStatus)('completed');
        }
      }
    );
  };

  return (
    <Animated.View
      style={[
        styles.box,
        useAnimatedStyle(() => ({
          transform: [{ translateX: translateX.value }],
        })),
      ]}
    />
  );
}

Gesture Handler Integration

Pan Gesture

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  clamp,
} from 'react-native-reanimated';

function DraggableBox() {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const context = useSharedValue({ x: 0, y: 0 });

  const panGesture = Gesture.Pan()
    .onStart(() => {
      context.value = { x: translateX.value, y: translateY.value };
    })
    .onUpdate((event) => {
      translateX.value = event.translationX + context.value.x;
      translateY.value = event.translationY + context.value.y;
    })
    .onEnd((event) => {
      // Apply velocity decay
      translateX.value = withSpring(
        clamp(translateX.value + event.velocityX / 10, -100, 100)
      );
      translateY.value = withSpring(
        clamp(translateY.value + event.velocityY / 10, -100, 100)
      );
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
    ],
  }));

  return (
    <GestureDetector gesture={panGesture}>
      <Animated.View style={[styles.box, animatedStyle]} />
    </GestureDetector>
  );
}

Pinch and Rotate Gestures

function ZoomableImage() {
  const scale = useSharedValue(1);
  const rotation = useSharedValue(0);
  const savedScale = useSharedValue(1);
  const savedRotation = useSharedValue(0);

  const pinchGesture = Gesture.Pinch()
    .onUpdate((event) => {
      scale.value = savedScale.value * event.scale;
    })
    .onEnd(() => {
      savedScale.value = scale.value;
      // Snap back if too small
      if (scale.value < 1) {
        scale.value = withSpring(1);
        savedScale.value = 1;
      }
    });

  const rotateGesture = Gesture.Rotation()
    .onUpdate((event) => {
      rotation.value = savedRotation.value + event.rotation;
    })
    .onEnd(() => {
      savedRotation.value = rotation.value;
    });

  const composed = Gesture.Simultaneous(pinchGesture, rotateGesture);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { scale: scale.value },
      { rotate: `${rotation.value}rad` },
    ],
  }));

  return (
    <GestureDetector gesture={composed}>
      <Animated.Image
        source={{ uri: imageUrl }}
        style={[styles.image, animatedStyle]}
      />
    </GestureDetector>
  );
}

Tap Gesture with Feedback

function TappableCard({ onPress, children }: TappableCardProps) {
  const scale = useSharedValue(1);
  const opacity = useSharedValue(1);

  const tapGesture = Gesture.Tap()
    .onBegin(() => {
      scale.value = withSpring(0.97);
      opacity.value = withTiming(0.8, { duration: 100 });
    })
    .onFinalize(() => {
      scale.value = withSpring(1);
      opacity.value = withTiming(1, { duration: 100 });
    })
    .onEnd(() => {
      runOnJS(onPress)();
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
    opacity: opacity.value,
  }));

  return (
    <GestureDetector gesture={tapGesture}>
      <Animated.View style={[styles.card, animatedStyle]}>
        {children}
      </Animated.View>
    </GestureDetector>
  );
}

Common Animation Patterns

Fade In/Out

function FadeInView({ visible, children }: FadeInViewProps) {
  const opacity = useSharedValue(0);

  useEffect(() => {
    opacity.value = withTiming(visible ? 1 : 0, { duration: 300 });
  }, [visible]);

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    display: opacity.value === 0 ? 'none' : 'flex',
  }));

  return (
    <Animated.View style={animatedStyle}>
      {children}
    </Animated.View>
  );
}

Slide In/Out

function SlideInView({ visible, direction = 'right', children }) {
  const translateX = useSharedValue(direction === 'right' ? 100 : -100);
  const opacity = useSharedValue(0);

  useEffect(() => {
    if (visible) {
      translateX.value = withSpring(0);
      opacity.value = withTiming(1);
    } else {
      translateX.value = withSpring(direction === 'right' ? 100 : -100);
      opacity.value = withTiming(0);
    }
  }, [visible, direction]);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
    opacity: opacity.value,
  }));

  return (
    <Animated.View style={animatedStyle}>
      {children}
    </Animated.View>
  );
}

Staggered List Animation

function StaggeredList({ items }: { items: Item[] }) {
  return (
    <FlatList
      data={items}
      keyExtractor={(item) => item.id}
      renderItem={({ item, index }) => (
        <StaggeredItem item={item} index={index} />
      )}
    />
  );
}

function StaggeredItem({ item, index }: { item: Item; index: number }) {
  const translateY = useSharedValue(50);
  const opacity = useSharedValue(0);

  useEffect(() => {
    translateY.value = withDelay(
      index * 100,
      withSpring(0, { damping: 15 })
    );
    opacity.value = withDelay(
      index * 100,
      withTiming(1, { duration: 300 })
    );
  }, [index]);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: translateY.value }],
    opacity: opacity.value,
  }));

  return (
    <Animated.View style={[styles.listItem, animatedStyle]}>
      <Text>{item.title}</Text>
    </Animated.View>
  );
}

Pulse Animation

function PulseView({ children }: { children: React.ReactNode }) {
  const scale = useSharedValue(1);

  useEffect(() => {
    scale.value = withRepeat(
      withSequence(
        withTiming(1.05, { duration: 500 }),
        withTiming(1, { duration: 500 })
      ),
      -1, // infinite
      false // no reverse
    );

    return () => {
      cancelAnimation(scale);
    };
  }, []);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  return (
    <Animated.View style={animatedStyle}>
      {children}
    </Animated.View>
  );
}

Shake Animation

function ShakeView({ trigger, children }) {
  const translateX = useSharedValue(0);

  useEffect(() => {
    if (trigger) {
      translateX.value = withSequence(
        withTiming(-10, { duration: 50 }),
        withTiming(10, { duration: 50 }),
        withTiming(-10, { duration: 50 }),
        withTiming(10, { duration: 50 }),
        withTiming(0, { duration: 50 })
      );
    }
  }, [trigger]);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
  }));

  return (
    <Animated.View style={animatedStyle}>
      {children}
    </Animated.View>
  );
}

Advanced Patterns

Interpolation

import { interpolate, Extrapolation } from 'react-native-reanimated';

function ParallaxHeader() {
  const scrollY = useSharedValue(0);

  const headerStyle = useAnimatedStyle(() => {
    const height = interpolate(
      scrollY.value,
      [0, 200],
      [300, 100],
      Extrapolation.CLAMP
    );

    const opacity = interpolate(
      scrollY.value,
      [0, 150, 200],
      [1, 0.5, 0],
      Extrapolation.CLAMP
    );

    const translateY = interpolate(
      scrollY.value,
      [0, 200],
      [0, -50],
      Extrapolation.CLAMP
    );

    return {
      height,
      opacity,
      transform: [{ translateY }],
    };
  });

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.header, headerStyle]}>
        <Text style={styles.headerTitle}>Header</Text>
      </Animated.View>
      <Animated.ScrollView
        onScroll={useAnimatedScrollHandler({
          onScroll: (event) => {
            scrollY.value = event.contentOffset.y;
          },
        })}
        scrollEventThrottle={16}
      >
        {/* Content */}
      </Animated.ScrollView>
    </View>
  );
}

Color Interpolation

import { interpolateColor } from 'react-native-reanimated';

function ColorTransition() {
  const progress = useSharedValue(0);

  const animatedStyle = useAnimatedStyle(() => {
    const backgroundColor = interpolateColor(
      progress.value,
      [0, 0.5, 1],
      ['#6366f1', '#8b5cf6', '#ec4899']
    );

    return { backgroundColor };
  });

  const toggleColor = () => {
    progress.value = withTiming(progress.value === 0 ? 1 : 0, {
      duration: 1000,
    });
  };

  return (
    <Pressable onPress={toggleColor}>
      <Animated.View style={[styles.box, animatedStyle]} />
    </Pressable>
  );
}

Derived Values

import { useDerivedValue } from 'react-native-reanimated';

function DerivedValueExample() {
  const x = useSharedValue(0);
  const y = useSharedValue(0);

  // Derived value computed from other shared values
  const distance = useDerivedValue(() => {
    return Math.sqrt(x.value ** 2 + y.value ** 2);
  });

  const angle = useDerivedValue(() => {
    return Math.atan2(y.value, x.value) * (180 / Math.PI);
  });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: x.value },
      { translateY: y.value },
      { rotate: `${angle.value}deg` },
    ],
    opacity: interpolate(distance.value, [0, 200], [1, 0.5]),
  }));

  return (
    <Animated.View style={[styles.box, animatedStyle]} />
  );
}

Layout Animations

import Animated, {
  Layout,
  FadeIn,
  FadeOut,
  SlideInLeft,
  SlideOutRight,
  ZoomIn,
  ZoomOut,
} from 'react-native-reanimated';

function AnimatedList() {
  const [items, setItems] = useState([1, 2, 3, 4, 5]);

  const addItem = () => {
    setItems([...items, items.length + 1]);
  };

  const removeItem = (id: number) => {
    setItems(items.filter((item) => item !== id));
  };

  return (
    <View style={styles.container}>
      <Button title="Add Item" onPress={addItem} />
      {items.map((item) => (
        <Animated.View
          key={item}
          style={styles.item}
          entering={FadeIn.duration(300).springify()}
          exiting={SlideOutRight.duration(300)}
          layout={Layout.springify()}
        >
          <Text>Item {item}</Text>
          <Pressable onPress={() => removeItem(item)}>
            <Text>Remove</Text>
          </Pressable>
        </Animated.View>
      ))}
    </View>
  );
}

Swipeable Card

function SwipeableCard({ onSwipeLeft, onSwipeRight }) {
  const translateX = useSharedValue(0);
  const rotateZ = useSharedValue(0);
  const context = useSharedValue({ x: 0 });

  const SWIPE_THRESHOLD = 120;

  const panGesture = Gesture.Pan()
    .onStart(() => {
      context.value = { x: translateX.value };
    })
    .onUpdate((event) => {
      translateX.value = event.translationX + context.value.x;
      rotateZ.value = interpolate(
        translateX.value,
        [-200, 0, 200],
        [-15, 0, 15]
      );
    })
    .onEnd((event) => {
      if (translateX.value > SWIPE_THRESHOLD) {
        translateX.value = withTiming(500, { duration: 200 }, () => {
          runOnJS(onSwipeRight)();
        });
      } else if (translateX.value < -SWIPE_THRESHOLD) {
        translateX.value = withTiming(-500, { duration: 200 }, () => {
          runOnJS(onSwipeLeft)();
        });
      } else {
        translateX.value = withSpring(0);
        rotateZ.value = withSpring(0);
      }
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { rotate: `${rotateZ.value}deg` },
    ],
  }));

  const leftIndicatorStyle = useAnimatedStyle(() => ({
    opacity: interpolate(
      translateX.value,
      [0, SWIPE_THRESHOLD],
      [0, 1],
      Extrapolation.CLAMP
    ),
  }));

  const rightIndicatorStyle = useAnimatedStyle(() => ({
    opacity: interpolate(
      translateX.value,
      [-SWIPE_THRESHOLD, 0],
      [1, 0],
      Extrapolation.CLAMP
    ),
  }));

  return (
    <GestureDetector gesture={panGesture}>
      <Animated.View style={[styles.card, animatedStyle]}>
        <Animated.View style={[styles.likeIndicator, leftIndicatorStyle]}>
          <Text>LIKE</Text>
        </Animated.View>
        <Animated.View style={[styles.nopeIndicator, rightIndicatorStyle]}>
          <Text>NOPE</Text>
        </Animated.View>
        {/* Card content */}
      </Animated.View>
    </GestureDetector>
  );
}

Bottom Sheet

const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + 50;
const MIN_TRANSLATE_Y = 0;
const SNAP_POINTS = [-SCREEN_HEIGHT * 0.5, -SCREEN_HEIGHT * 0.25, 0];

function BottomSheet({ children }) {
  const translateY = useSharedValue(0);
  const context = useSharedValue({ y: 0 });

  const panGesture = Gesture.Pan()
    .onStart(() => {
      context.value = { y: translateY.value };
    })
    .onUpdate((event) => {
      translateY.value = clamp(
        context.value.y + event.translationY,
        MAX_TRANSLATE_Y,
        MIN_TRANSLATE_Y
      );
    })
    .onEnd((event) => {
      // Find closest snap point
      const destination = SNAP_POINTS.reduce((prev, curr) =>
        Math.abs(curr - translateY.value) < Math.abs(prev - translateY.value)
          ? curr
          : prev
      );

      translateY.value = withSpring(destination, {
        damping: 50,
        stiffness: 300,
      });
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: translateY.value }],
  }));

  const backdropStyle = useAnimatedStyle(() => ({
    opacity: interpolate(
      translateY.value,
      [MIN_TRANSLATE_Y, MAX_TRANSLATE_Y],
      [0, 0.5]
    ),
    pointerEvents: translateY.value < -50 ? 'auto' : 'none',
  }));

  return (
    <>
      <Animated.View style={[styles.backdrop, backdropStyle]} />
      <GestureDetector gesture={panGesture}>
        <Animated.View style={[styles.bottomSheet, animatedStyle]}>
          <View style={styles.handle} />
          {children}
        </Animated.View>
      </GestureDetector>
    </>
  );
}

Performance Tips

Memoization

// Memoize animated style when dependencies don't change
const animatedStyle = useAnimatedStyle(
  () => ({
    transform: [{ translateX: translateX.value }],
  }),
  [],
); // Empty deps if no external dependencies

// Use useMemo for complex calculations outside worklets
const threshold = useMemo(() => calculateThreshold(screenWidth), [screenWidth]);

Worklet Best Practices

// Do: Keep worklets simple
const simpleWorklet = () => {
  "worklet";
  return scale.value * 2;
};

// Don't: Complex logic in worklets
// Move complex logic to JS with runOnJS

// Do: Use runOnJS for callbacks
const onComplete = () => {
  setIsAnimating(false);
};

opacity.value = withTiming(1, {}, (finished) => {
  "worklet";
  if (finished) {
    runOnJS(onComplete)();
  }
});

Cancel Animations

import { cancelAnimation } from 'react-native-reanimated';

function AnimatedComponent() {
  const translateX = useSharedValue(0);

  useEffect(() => {
    // Start animation
    translateX.value = withRepeat(
      withTiming(100, { duration: 1000 }),
      -1,
      true
    );

    // Cleanup: cancel animation on unmount
    return () => {
      cancelAnimation(translateX);
    };
  }, []);

  return <Animated.View style={animatedStyle} />;
}

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive guides, code snippets, and documentation for React Native styling, navigation patterns, and Reanimated animations. All analyzed contents follow secure and standard mobile development practices.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    4 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at be57c0b. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 4 months ago
  • TypeScript
  • react-native
  • reanimated
  • react-navigation
  • styling
  • animations
  • gesture-handler
  • cross-platform
  • mobile

README badge

README badge for wshobson/agents/react-native-design

Teaches React Native styling with StyleSheet, navigation with React Navigation 6+, and animations with Reanimated 3 for cross-platform mobile apps. Covers responsive layouts, platform-specific design, gesture interactions, and performance optimization for iOS and Android.

Generated from the current SKILL.md.

Does this skill cover React Navigation 6+?
Yes. The skill explicitly targets React Navigation 6+ for implementing navigation patterns in React Native apps.
What animation library does this use?
Reanimated 3. The skill focuses on creating performant animations by running them on the UI thread with worklets.
Does this cover platform-specific styling for iOS and Android?
Yes. The skill includes guidance on implementing platform-specific designs and using Platform.select for iOS/Android differences.
Does this cover gesture interactions?
Yes. The skill covers gesture-driven interactions with Gesture Handler and addresses common gesture conflict issues.
What about responsive layouts and different screen sizes?
The skill includes patterns for styling responsive layouts across different screen sizes and handling safe areas on notched devices.

Generated from the current SKILL.md. These answers refresh after source changes.