diff --git a/packages/rn-tester/js/examples/AnimationBackend/AnimationBackendIndex.js b/packages/rn-tester/js/examples/AnimationBackend/AnimationBackendIndex.js
new file mode 100644
index 000000000000..f8eb3d25d773
--- /dev/null
+++ b/packages/rn-tester/js/examples/AnimationBackend/AnimationBackendIndex.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict-local
+ * @format
+ */
+
+import type {RNTesterModule} from '../../types/RNTesterTypes';
+
+import PlaygroundExample from './ChessboardExample';
+import SwipeableListExample from './SwipeableListExample';
+import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
+
+const canUseBackend =
+ // eslint-disable-next-line
+ ReactNativeFeatureFlags.useSharedAnimatedBackend() &&
+ ReactNativeFeatureFlags.cxxNativeAnimatedEnabled();
+
+export default ({
+ framework: 'React',
+ title: 'Animation Backend',
+ category: 'UI',
+ description: `Examples demonstrating the Animation Backend for layout-updating animations. ${canUseBackend ? '' : 'You need to enable c++ Animated and the Animation Backend to see these examples.'}`,
+ showIndividualExamples: true,
+ examples: canUseBackend ? [PlaygroundExample, SwipeableListExample] : [],
+}: RNTesterModule);
diff --git a/packages/rn-tester/js/examples/AnimationBackend/ChessboardExample.js b/packages/rn-tester/js/examples/AnimationBackend/ChessboardExample.js
new file mode 100644
index 000000000000..5bf011860ba2
--- /dev/null
+++ b/packages/rn-tester/js/examples/AnimationBackend/ChessboardExample.js
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict-local
+ * @format
+ */
+
+import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
+
+import * as React from 'react';
+import {useMemo} from 'react';
+import {Animated, StyleSheet, Text, View, useAnimatedValue} from 'react-native';
+import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist';
+
+allowStyleProp('width');
+allowStyleProp('height');
+const colors = ['lime', 'green'];
+
+function useLoop() {
+ const animatedValue = useAnimatedValue(0);
+
+ React.useEffect(() => {
+ const animation = Animated.loop(
+ Animated.sequence([
+ Animated.timing(animatedValue, {
+ toValue: 1,
+ duration: 1000,
+ useNativeDriver: true,
+ }),
+ Animated.timing(animatedValue, {
+ toValue: 0,
+ duration: 1000,
+ useNativeDriver: true,
+ }),
+ ]),
+ );
+ animation.start();
+
+ return () => {
+ animation.stop();
+ };
+ }, [animatedValue]);
+
+ return animatedValue;
+}
+
+const N = 12;
+const STATE_MAX = 30;
+
+function ChessboardExample() {
+ const [state, setState] = React.useState(0);
+
+ const animatedValue = useLoop();
+
+ React.useEffect(() => {
+ const id = setInterval(() => {
+ setState(s => (s + 1) % STATE_MAX);
+ }, 10);
+ return () => {
+ clearInterval(id);
+ };
+ }, []);
+
+ const animatedStyle = useMemo(() => {
+ return {
+ width: animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [10, 30],
+ }),
+ height: animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [10, 30],
+ }),
+ };
+ }, [animatedValue]);
+
+ return (
+
+ {state}
+
+
+ {[...Array(N).keys()].map(i => (
+
+ {[...Array(N).keys()].map(j => (
+
+ ))}
+
+ ))}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ workaround: {
+ height: 400,
+ },
+ chessboard: {
+ alignItems: 'flex-start',
+ },
+ border: {
+ borderWidth: 10,
+ borderColor: 'red',
+ },
+ row: {
+ flexDirection: 'row',
+ },
+ text: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ textAlign: 'center',
+ margin: 10,
+ },
+});
+
+export default ({
+ title: 'Chessboard',
+ name: 'chessboard',
+ description: 'Combine animating layout with state updates',
+ render: (): React.Node => ,
+}: RNTesterModuleExample);
diff --git a/packages/rn-tester/js/examples/AnimationBackend/SwipeableListExample.js b/packages/rn-tester/js/examples/AnimationBackend/SwipeableListExample.js
new file mode 100644
index 000000000000..2ede306fa51d
--- /dev/null
+++ b/packages/rn-tester/js/examples/AnimationBackend/SwipeableListExample.js
@@ -0,0 +1,286 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict-local
+ * @format
+ */
+
+import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
+
+import * as React from 'react';
+import {useCallback, useMemo, useRef} from 'react';
+import {
+ Alert,
+ Animated,
+ Dimensions,
+ Easing,
+ PanResponder,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+ useAnimatedValue,
+} from 'react-native';
+import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist';
+
+allowStyleProp('height');
+
+const windowDimensions = Dimensions.get('window');
+const BUTTON_WIDTH = 80;
+const MAX_TRANSLATE = -BUTTON_WIDTH;
+
+type Data = {
+ id: string,
+ title: string,
+};
+
+const initialData: $ReadOnlyArray = [
+ {id: '1', title: 'Kate Bell'},
+ {id: '2', title: 'John Appleseed'},
+ {id: '3', title: 'Mark Zuckerberg'},
+ {id: '4', title: 'Iron Man'},
+ {id: '5', title: 'Captain America'},
+ {id: '6', title: 'Batman'},
+ {id: '7', title: 'Matt Smith'},
+];
+
+const springConfig = {
+ stiffness: 1000,
+ damping: 500,
+ mass: 3,
+ overshootClamping: true,
+ useNativeDriver: true,
+};
+
+const timingConfig = {
+ duration: 400,
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1),
+ useNativeDriver: true,
+};
+
+function SwipeableListExample(): React.Node {
+ const [data, setData] = React.useState<$ReadOnlyArray>(initialData);
+
+ const handleRemove = useCallback((id: string) => {
+ setData(currentData => currentData.filter(item => item.id !== id));
+ Alert.alert('Removed');
+ }, []);
+
+ return (
+
+ {data.map(item => (
+ handleRemove(item.id)}
+ />
+ ))}
+
+ );
+}
+
+type ListItemProps = {
+ item: Data,
+ onRemove: () => void,
+};
+
+function ListItem({item, onRemove}: ListItemProps): React.Node {
+ const isRemoving = useRef(false);
+ const translateX = useAnimatedValue(0);
+ const removalOffset = useAnimatedValue(0); // Separate value for removal animation
+ const height = useAnimatedValue(78);
+ const opacity = useAnimatedValue(1);
+
+ const clampedTranslateX = useMemo(
+ () =>
+ translateX.interpolate({
+ inputRange: [MAX_TRANSLATE, 0],
+ outputRange: [MAX_TRANSLATE, 0],
+ extrapolate: 'clamp',
+ }),
+ [translateX],
+ );
+
+ // Combine clamped pan position with removal offset
+ const finalTranslateX = useMemo(
+ () => Animated.add(clampedTranslateX, removalOffset),
+ [clampedTranslateX, removalOffset],
+ );
+
+ const panResponder = useMemo(
+ () =>
+ PanResponder.create({
+ onMoveShouldSetPanResponder: (_, gestureState) => {
+ return Math.abs(gestureState.dx) > 10;
+ },
+ onPanResponderGrant: () => {
+ translateX.extractOffset();
+ },
+ onPanResponderMove: Animated.event([null, {dx: translateX}], {
+ useNativeDriver: false,
+ }),
+ onPanResponderRelease: (_, gestureState) => {
+ translateX.flattenOffset();
+
+ const shouldOpen = gestureState.vx < -0.05;
+ const targetValue = shouldOpen ? MAX_TRANSLATE : 0;
+
+ Animated.spring(translateX, {
+ toValue: targetValue,
+ velocity: gestureState.vx,
+ ...springConfig,
+ }).start();
+ },
+ }),
+ [translateX],
+ );
+
+ const handleRemove = useCallback(() => {
+ if (isRemoving.current) {
+ return;
+ }
+ isRemoving.current = true;
+
+ // Animate removal offset separately - this bypasses the clamp
+ Animated.parallel([
+ Animated.timing(height, {
+ toValue: 0,
+ ...timingConfig,
+ }),
+ Animated.timing(opacity, {
+ toValue: 0,
+ ...timingConfig,
+ }),
+ Animated.timing(removalOffset, {
+ toValue: -windowDimensions.width - MAX_TRANSLATE,
+ ...timingConfig,
+ }),
+ ]).start(() => {
+ onRemove();
+ });
+ }, [height, opacity, removalOffset, onRemove]);
+
+ const animatedStyle = useMemo(
+ () => ({
+ height,
+ opacity,
+ transform: [{translateX: finalTranslateX}],
+ }),
+ [height, opacity, finalTranslateX],
+ );
+
+ const removeButton = useMemo(
+ () => ({
+ title: 'Delete',
+ backgroundColor: 'red',
+ color: 'white',
+ onPress: handleRemove,
+ }),
+ [handleRemove],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+type ButtonData = {
+ title: string,
+ backgroundColor: string,
+ color: string,
+ onPress: () => void,
+};
+
+function Button({item}: {item: ButtonData}): React.Node {
+ return (
+
+
+ {item.title}
+
+
+ );
+}
+
+function ListItemContent({item}: {item: Data}): React.Node {
+ return (
+
+
+ {item.title[0]}
+
+ {item.title}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ item: {
+ justifyContent: 'center',
+ },
+ itemContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 8,
+ borderBottomWidth: 1,
+ borderBottomColor: '#ccc',
+ },
+ avatarContainer: {
+ width: 50,
+ height: 50,
+ borderRadius: 25,
+ backgroundColor: 'grey',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ avatarText: {
+ fontSize: 18,
+ color: 'white',
+ },
+ title: {
+ fontSize: 18,
+ marginLeft: 16,
+ },
+ button: {
+ width: windowDimensions.width,
+ paddingRight: windowDimensions.width - BUTTON_WIDTH,
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ buttonInner: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ flex: 1,
+ width: BUTTON_WIDTH,
+ },
+ buttonsContainer: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: windowDimensions.width,
+ width: windowDimensions.width,
+ },
+});
+
+export default ({
+ title: 'Swipeable List',
+ name: 'swipeableList',
+ description: 'A swipeable list with width animation',
+ render: (): React.Node => ,
+}: RNTesterModuleExample);
diff --git a/packages/rn-tester/js/utils/RNTesterList.android.js b/packages/rn-tester/js/utils/RNTesterList.android.js
index 51001fbbb113..c0abd94423fb 100644
--- a/packages/rn-tester/js/utils/RNTesterList.android.js
+++ b/packages/rn-tester/js/utils/RNTesterList.android.js
@@ -174,6 +174,12 @@ const APIs: Array = ([
category: 'UI',
module: require('../examples/Animated/AnimatedIndex').default,
},
+ {
+ key: 'AnimationBackendIndex',
+ category: 'UI',
+ module: require('../examples/AnimationBackend/AnimationBackendIndex')
+ .default,
+ },
{
key: 'Animation - GratuitousAnimation',
category: 'UI',
diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js
index 342c7e028e1c..b69af517a43b 100644
--- a/packages/rn-tester/js/utils/RNTesterList.ios.js
+++ b/packages/rn-tester/js/utils/RNTesterList.ios.js
@@ -180,6 +180,11 @@ const APIs: Array = ([
key: 'AnimatedIndex',
module: require('../examples/Animated/AnimatedIndex').default,
},
+ {
+ key: 'AnimationBackendIndex',
+ module: require('../examples/AnimationBackend/AnimationBackendIndex')
+ .default,
+ },
{
key: 'AnExApp',
module: require('../examples/AnimatedGratuitousApp/AnExApp'),