Animaciones en React Native 2026: Reanimated 4, CSS Animations, Gestos y Layout Animations

Guía práctica de animaciones en React Native con Reanimated 4. Aprende a usar CSS Transitions, Keyframes, worklets, gestos y Layout Animations con ejemplos de código listos para copiar y crear interfaces a 60+ FPS.

Por qué las animaciones son lo que separa una app buena de una app memorable

Puedes tener la mejor arquitectura de navegación, el estado más limpio del mundo y un rendimiento de infarto. Pero si tu app no se siente bien al usarla — si los elementos aparecen de golpe, las transiciones son bruscas y los gestos no responden con fluidez — el usuario lo nota. Y se va.

Las animaciones no son decoración. Son feedback visual.

Le dicen al usuario que algo pasó, que algo está pasando, o que algo va a pasar. Un botón que rebota al pulsarlo, una lista que desliza suavemente al eliminar un elemento, un modal que se desvanece al cerrarse… estas micro-interacciones son la diferencia entre una app que parece un prototipo y una que parece un producto pulido. Honestamente, he visto apps técnicamente impecables que se sienten "raras" solo porque les faltan estas pequeñas transiciones.

En 2026, el ecosistema de animaciones en React Native ha dado un salto enorme. Reanimated 4 trajo las animaciones CSS a React Native — transiciones y keyframes que cualquier desarrollador web ya conoce — mientras mantiene los poderosos worklets para animaciones complejas. React Native Gesture Handler sigue evolucionando con su API declarativa de gestos. Y con la Nueva Arquitectura como estándar, todo corre en el hilo de UI a 60-120 FPS sin parpadeos.

Así que, vamos al grano. En esta guía vamos a recorrer todo lo que necesitas saber para crear animaciones profesionales en React Native — desde lo más simple con CSS transitions hasta gestos complejos con worklets.

El ecosistema de animaciones en React Native: panorama 2026

Antes de escribir una sola línea de código animado, necesitas entender qué herramientas tienes disponibles y cuándo usar cada una. Lo bueno es que en 2026 el panorama se ha simplificado bastante respecto a años anteriores.

Animated API (built-in)

La API nativa de React Native. Funciona, pero es imperativa, verbosa y limitada. Necesitas crear instancias de Animated.Value, llamar a .start() manualmente, y especificar useNativeDriver: true para que corra en el hilo de UI. Para animaciones simples puede servir, pero para cualquier cosa seria, Reanimated es superior en prácticamente todos los aspectos.

Reanimated 4 (la estrella)

La versión 4 de React Native Reanimated, lanzada en 2025 por Software Mansion, es la librería de animaciones definitiva. Ofrece dos sistemas de animación:

  • CSS Animations: Transiciones y keyframes declarativos que funcionan exactamente como en la web. Cambias un valor de estado y Reanimated anima la transición automáticamente. Así de fácil.
  • Worklets: Funciones JavaScript que corren directamente en el hilo de UI para control frame-a-frame. Esenciales para gestos, scroll y animaciones interactivas.

Todo corre de forma nativa en el hilo de UI, alcanzando 120+ FPS sin depender del hilo de JavaScript. Eso sí, requiere la Nueva Arquitectura (Fabric) y React Native 0.76+.

React Native Gesture Handler

La librería complementaria de Software Mansion para detectar gestos (pan, pinch, tap, fling, rotation) de forma nativa. Se integra a la perfección con Reanimated a través de shared values, permitiendo que los gestos del usuario controlen animaciones directamente en el hilo de UI. Si vas a hacer swipe-to-delete o drag and drop, la vas a necesitar sí o sí.

react-native-css-animations

Un paquete de presets de animaciones CSS listas para usar con Reanimated 4: spin, pulse, bounce, shimmer y más. Perfecto para los casos comunes sin tener que escribir código de animación desde cero.

Instalación y configuración de Reanimated 4 con Expo

La configuración de Reanimated 4 es sencilla, pero hay un cambio importante respecto a la versión 3: los worklets ahora viven en un paquete separado llamado react-native-worklets. Esta separación permite que otras librerías aprovechen los worklets sin depender directamente de Reanimated (un detalle de diseño bastante inteligente, la verdad).

Instalación con Expo

# Instalar Reanimated 4, Worklets y Gesture Handler
npx expo install react-native-reanimated react-native-worklets react-native-gesture-handler

# Opcional: presets de animaciones CSS
npx expo install react-native-css-animations

Configuración de Babel

El cambio más importante en Reanimated 4 es el plugin de Babel. Ahora debes usar react-native-worklets/plugin en lugar del antiguo react-native-reanimated/plugin. Si no haces este cambio, vas a ver una advertencia y tus animaciones podrían no funcionar:

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      // ... otros plugins
      'react-native-worklets/plugin', // ¡DEBE ser el ÚLTIMO plugin!
    ],
  };
};

Importante: El plugin de worklets debe ser el último en el array de plugins. El orden importa porque necesita procesar el código después de todas las demás transformaciones. Me ha pasado más de una vez olvidar este detalle y perder tiempo depurando.

Configuración de Gesture Handler

Tu componente raíz debe estar envuelto en GestureHandlerRootView:

// app/_layout.tsx (con Expo Router)
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Stack />
    </GestureHandlerRootView>
  );
}

Después de la configuración, limpia la caché de Metro y reconstruye la app:

npx expo start --clear

CSS Transitions: animar cambios de estado sin esfuerzo

Las CSS Transitions son la novedad estrella de Reanimated 4. Si vienes del desarrollo web, te van a parecer completamente naturales. La idea es simple: defines qué propiedades quieres animar, con qué duración y curva de easing, y cuando esos valores cambian a través del estado de React, Reanimated se encarga de la transición automáticamente.

Sin hooks extra. Sin imperativos. Solo estado de React y propiedades de estilo.

Propiedades soportadas

Las transiciones CSS en Reanimated 4 soportan las siguientes propiedades de configuración:

  • transitionProperty — qué propiedad animar ('opacity', 'width', 'all', etc.)
  • transitionDuration — duración de la animación ('300ms', '0.5s')
  • transitionDelay — retraso antes de iniciar
  • transitionTimingFunction — curva de easing ('ease', 'ease-in-out', 'linear', etc.)

Ejemplo: toggle de visibilidad animado

import { useState } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';

export function AnimatedToggle() {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <>
      <Pressable
        onPress={() => setIsVisible(!isVisible)}
        style={styles.button}
      >
        <Text style={styles.buttonText}>
          {isVisible ? 'Ocultar' : 'Mostrar'}
        </Text>
      </Pressable>

      <Animated.View
        style={[
          styles.box,
          {
            opacity: isVisible ? 1 : 0,
            transform: [{ scale: isVisible ? 1 : 0.8 }],
            transitionProperty: 'opacity, transform',
            transitionDuration: '400ms',
            transitionTimingFunction: 'ease-in-out',
          },
        ]}
      >
        <Text style={styles.text}>¡Hola, mundo animado!</Text>
      </Animated.View>
    </>
  );
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#6C63FF',
    padding: 14,
    borderRadius: 10,
    alignItems: 'center',
    marginBottom: 20,
  },
  buttonText: { color: '#fff', fontWeight: '700', fontSize: 16 },
  box: {
    backgroundColor: '#E8E5FF',
    padding: 30,
    borderRadius: 16,
    alignItems: 'center',
  },
  text: { fontSize: 18, fontWeight: '600', color: '#333' },
});

¿Ves lo que pasó ahí? No hay useSharedValue, no hay useAnimatedStyle, no hay withTiming. Solo cambias el estado de React y las propiedades CSS se encargan del resto. Reanimated detecta el cambio de opacity y transform y los anima suavemente en el hilo de UI. Es casi mágico la primera vez que lo pruebas.

Ejemplo: botón con feedback visual

import { useState } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';

export function AnimatedButton({ title, onPress }: { title: string; onPress: () => void }) {
  const [isPressed, setIsPressed] = useState(false);

  return (
    <Pressable
      onPressIn={() => setIsPressed(true)}
      onPressOut={() => setIsPressed(false)}
      onPress={onPress}
    >
      <Animated.View
        style={[
          styles.btn,
          {
            backgroundColor: isPressed ? '#4A42D4' : '#6C63FF',
            transform: [{ scale: isPressed ? 0.95 : 1 }],
            transitionProperty: 'background-color, transform',
            transitionDuration: '150ms',
            transitionTimingFunction: 'ease-out',
          },
        ]}
      >
        <Text style={styles.label}>{title}</Text>
      </Animated.View>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  btn: { padding: 16, borderRadius: 12, alignItems: 'center' },
  label: { color: '#fff', fontSize: 16, fontWeight: '700' },
});

Este patrón es perfecto para darle vida a cualquier botón. El usuario presiona, el botón se encoge ligeramente y cambia de color. Suelta, y vuelve a su estado original. Todo con 150ms de transición que se siente natural e inmediato. Es uno de esos detalles pequeños que hacen que una app se sienta profesional.

CSS Keyframes: animaciones repetitivas y secuenciales

Mientras las transiciones van de A a B cuando cambia el estado, los keyframes te permiten crear animaciones con múltiples pasos que pueden repetirse indefinidamente. Piensa en loaders, pulsos de notificación, efectos shimmer — básicamente cualquier animación que corre sola sin que el usuario interactúe.

Propiedades de keyframes

  • animationName — un objeto que define los keyframes con porcentajes o from/to
  • animationDuration — duración de un ciclo completo
  • animationIterationCount — cuántas veces se repite ('infinite' para siempre)
  • animationTimingFunction — curva de easing
  • animationDelay — retraso antes de comenzar
  • animationDirection — dirección ('normal', 'reverse', 'alternate')
  • animationFillMode — estado final de la animación

Ejemplo: indicador de carga con pulso

import { View, Text, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';

const pulseKeyframes = {
  from: { opacity: 1, transform: [{ scale: 1 }] },
  '50%': { opacity: 0.5, transform: [{ scale: 1.08 }] },
  to: { opacity: 1, transform: [{ scale: 1 }] },
};

export function PulseLoader() {
  return (
    <View style={styles.container}>
      <Animated.View
        style={[
          styles.dot,
          {
            animationName: pulseKeyframes,
            animationDuration: '1200ms',
            animationIterationCount: 'infinite',
            animationTimingFunction: 'ease-in-out',
          },
        ]}
      />
      <Animated.View
        style={[
          styles.dot,
          {
            animationName: pulseKeyframes,
            animationDuration: '1200ms',
            animationIterationCount: 'infinite',
            animationTimingFunction: 'ease-in-out',
            animationDelay: '200ms',
          },
        ]}
      />
      <Animated.View
        style={[
          styles.dot,
          {
            animationName: pulseKeyframes,
            animationDuration: '1200ms',
            animationIterationCount: 'infinite',
            animationTimingFunction: 'ease-in-out',
            animationDelay: '400ms',
          },
        ]}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flexDirection: 'row', gap: 10, justifyContent: 'center' },
  dot: {
    width: 16,
    height: 16,
    borderRadius: 8,
    backgroundColor: '#6C63FF',
  },
});

Tres puntos que pulsan con un desfase de 200ms entre cada uno. Es el típico indicador de "algo está pasando" que ves en apps de mensajería. Y fíjate: no necesitaste ni un solo hook de Reanimated para hacerlo.

Ejemplo: skeleton loader con efecto shimmer

import { View, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';

const shimmerKeyframes = {
  from: { opacity: 0.4 },
  '50%': { opacity: 1 },
  to: { opacity: 0.4 },
};

export function SkeletonCard() {
  const animBase = {
    animationName: shimmerKeyframes,
    animationDuration: '1500ms',
    animationIterationCount: 'infinite' as const,
    animationTimingFunction: 'ease-in-out' as const,
  };

  return (
    <View style={styles.card}>
      <Animated.View style={[styles.imagePlaceholder, animBase]} />
      <View style={styles.content}>
        <Animated.View style={[styles.titleLine, animBase]} />
        <Animated.View
          style={[styles.subtitleLine, { ...animBase, animationDelay: '150ms' }]}
        />
        <Animated.View
          style={[styles.bodyLine, { ...animBase, animationDelay: '300ms' }]}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: '#fff',
    borderRadius: 16,
    overflow: 'hidden',
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
  },
  imagePlaceholder: {
    height: 180,
    backgroundColor: '#E0E0E0',
  },
  content: { padding: 16, gap: 10 },
  titleLine: {
    height: 20,
    width: '70%',
    backgroundColor: '#E0E0E0',
    borderRadius: 4,
  },
  subtitleLine: {
    height: 14,
    width: '50%',
    backgroundColor: '#E0E0E0',
    borderRadius: 4,
  },
  bodyLine: {
    height: 14,
    width: '90%',
    backgroundColor: '#E0E0E0',
    borderRadius: 4,
  },
});

Un skeleton loader profesional con solo CSS keyframes. Cada línea parpadea con un ligero desfase para crear esa ilusión de carga progresiva que todos conocemos. Apps como Instagram, Twitter y Shopify usan exactamente este patrón.

Presets de react-native-css-animations: animaciones en una línea

¿No quieres definir keyframes manualmente para casos comunes? Lo entiendo. Para eso existe el paquete react-native-css-animations de Software Mansion, que te da presets listos para usar:

import Animated from 'react-native-reanimated';
import { spin, pulse, bounce, shimmer, ping } from 'react-native-css-animations';

// Loader giratorio
<Animated.View style={[styles.spinner, spin]} />

// Skeleton con pulso
<Animated.View style={[styles.placeholder, pulse]} />

// Indicador de scroll hacia abajo
<Animated.View style={[styles.arrow, bounce]} />

// Efecto shimmer para carga
<Animated.View style={[styles.bar, shimmer]} />

// Notificación con efecto ping
<Animated.View style={[styles.badge, ping]} />

Cada preset es simplemente un objeto de estilos CSS que combinas con tus propios estilos usando el spread de arrays. Es la forma más rápida de añadir animaciones pulidas sin escribir lógica personalizada. Para prototipos rápidos, esto es oro.

Worklets y Shared Values: control total sobre las animaciones

Las CSS Animations son fantásticas para el 70-80% de los casos. Pero cuando necesitas control de verdad — responder a gestos, vincular animaciones al scroll, crear físicas personalizadas — necesitas el sistema de worklets de Reanimated.

Aquí es donde las cosas se ponen interesantes.

¿Qué es un Shared Value?

Un useSharedValue es un valor mutable que vive en el hilo de UI. A diferencia del estado de React, cambiar un shared value no causa un re-render. Este es el mecanismo que permite animar a 120 FPS incluso cuando el hilo de JavaScript está ocupado haciendo otras cosas.

¿Qué es un Worklet?

Un worklet es una función JavaScript que se ejecuta en el hilo de UI. Reanimated las transfiere automáticamente al hilo nativo gracias al plugin de Babel. Cuando necesitas comunicarte de vuelta con el hilo de JS (por ejemplo, para actualizar estado de React), usas runOnJS.

Ejemplo: animación de expansión con withSpring

import { Pressable, Text, StyleSheet } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';

export function SpringBox() {
  const width = useSharedValue(100);
  const isExpanded = useSharedValue(false);

  const animatedStyle = useAnimatedStyle(() => ({
    width: width.value,
  }));

  const handlePress = () => {
    isExpanded.value = !isExpanded.value;
    width.value = withSpring(isExpanded.value ? 280 : 100, {
      damping: 15,
      stiffness: 120,
    });
  };

  return (
    <>
      <Pressable onPress={handlePress} style={styles.button}>
        <Text style={styles.buttonText}>Expandir / Contraer</Text>
      </Pressable>

      <Animated.View style={[styles.box, animatedStyle]}>
        <Text style={styles.text}>Spring</Text>
      </Animated.View>
    </>
  );
}

const styles = StyleSheet.create({
  button: {
    backgroundColor: '#6C63FF',
    padding: 14,
    borderRadius: 10,
    alignItems: 'center',
    marginBottom: 20,
  },
  buttonText: { color: '#fff', fontWeight: '700' },
  box: {
    height: 100,
    backgroundColor: '#4ECDC4',
    borderRadius: 16,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: { color: '#fff', fontWeight: '700', fontSize: 18 },
});

La función withSpring usa física de resortes para crear un movimiento natural con rebote. El parámetro damping controla cuánto rebote hay (menor valor = más rebote) y stiffness controla la velocidad del resorte (mayor valor = más rápido). Esta combinación produce animaciones que se sienten orgánicas, como si el elemento tuviera peso real. Vale la pena jugar un rato con estos valores hasta encontrar el "feel" correcto para tu app.

Los tres primitivos de animación

  • withTiming(toValue, config) — Animación de duración fija con curva de easing. Ideal para transiciones predecibles.
  • withSpring(toValue, config) — Animación basada en física de resortes. Se siente más natural y responde mejor a la velocidad del gesto.
  • withDecay(config) — Animación de inercia que desacelera gradualmente. Perfecta para scroll con momentum.

Gestos con React Native Gesture Handler y Reanimated

Cuando combinas Gesture Handler con Reanimated, obtienes animaciones controladas por los dedos del usuario en tiempo real. Los gestos actualizan shared values directamente en el hilo de UI, eliminando cualquier latencia perceptible. El resultado es una experiencia que se siente tan nativa como una app de SwiftUI o Jetpack Compose.

Ejemplo: tarjeta deslizable (swipe-to-delete)

import { Text, Dimensions, StyleSheet } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
  runOnJS,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';

const { width: SCREEN_WIDTH } = Dimensions.get('window');
const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.3;

interface SwipeableCardProps {
  title: string;
  onDelete: () => void;
}

export function SwipeableCard({ title, onDelete }: SwipeableCardProps) {
  const translateX = useSharedValue(0);
  const cardHeight = useSharedValue(80);

  const panGesture = Gesture.Pan()
    .activeOffsetX([-10, 10])
    .onUpdate((event) => {
      // Solo permitir deslizar a la izquierda
      translateX.value = Math.min(0, event.translationX);
    })
    .onEnd(() => {
      if (translateX.value < -SWIPE_THRESHOLD) {
        // Deslizar hasta el final y colapsar
        translateX.value = withTiming(-SCREEN_WIDTH, { duration: 250 });
        cardHeight.value = withTiming(0, { duration: 300 }, () => {
          runOnJS(onDelete)();
        });
      } else {
        // Volver a la posición original
        translateX.value = withSpring(0, { damping: 20, stiffness: 200 });
      }
    });

  const cardStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
    height: cardHeight.value,
    marginBottom: interpolate(
      cardHeight.value,
      [0, 80],
      [0, 12],
      Extrapolation.CLAMP
    ),
  }));

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

  return (
    <Animated.View style={[styles.wrapper, { height: cardHeight }]}>
      <Animated.View style={[styles.deleteBackground, deleteStyle]}>
        <Text style={styles.deleteText}>Eliminar</Text>
      </Animated.View>

      <GestureDetector gesture={panGesture}>
        <Animated.View style={[styles.card, cardStyle]}>
          <Text style={styles.cardTitle}>{title}</Text>
        </Animated.View>
      </GestureDetector>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  wrapper: { position: 'relative', overflow: 'hidden' },
  deleteBackground: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: '#FF4757',
    justifyContent: 'center',
    alignItems: 'flex-end',
    paddingRight: 30,
    borderRadius: 12,
  },
  deleteText: { color: '#fff', fontWeight: '700', fontSize: 16 },
  card: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 20,
    justifyContent: 'center',
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 6,
  },
  cardTitle: { fontSize: 16, fontWeight: '600', color: '#333' },
});

Este componente implementa el clásico "deslizar para eliminar" que ves en Gmail o Apple Mail. El gesto Pan actualiza translateX en tiempo real mientras el dedo se mueve. Cuando supera el umbral y el usuario suelta, la tarjeta se desliza fuera de la pantalla y colapsa su altura. La función interpolate mapea el desplazamiento a la opacidad del fondo rojo — cuanto más deslizas, más visible se hace el texto "Eliminar". Un detalle sutil pero que comunica muy bien la intención.

Layout Animations: entrada y salida con estilo

Las Layout Animations de Reanimated permiten animar elementos automáticamente cuando se montan o desmontan del árbol de componentes. En vez de que un elemento aparezca de golpe, puedes hacer que entre con un fade, un slide, un bounce o cualquier combinación que se te ocurra.

Animaciones predefinidas

Reanimated incluye docenas de animaciones predefinidas listas para usar:

import Animated, {
  FadeIn,
  FadeOut,
  SlideInLeft,
  SlideOutRight,
  BounceIn,
  ZoomIn,
  FlipInXUp,
  LightSpeedInLeft,
} from 'react-native-reanimated';

// Fade simple
<Animated.View entering={FadeIn} exiting={FadeOut}>
  <Text>Aparezco suavemente</Text>
</Animated.View>

// Slide desde la izquierda con duración personalizada
<Animated.View entering={SlideInLeft.duration(500)} exiting={SlideOutRight}>
  <Text>Entro deslizando</Text>
</Animated.View>

// Bounce con delay
<Animated.View entering={BounceIn.delay(200)}>
  <Text>¡Boing!</Text>
</Animated.View>

// Zoom con callback al completar
<Animated.View
  entering={ZoomIn.duration(400).withCallback((finished) => {
    'worklet';
    if (finished) {
      console.log('Animación completada');
    }
  })}
>
  <Text>Zoom in</Text>
</Animated.View>

Modificadores disponibles

Todas las animaciones predefinidas se pueden personalizar encadenando modificadores:

  • .duration(ms) — Duración de la animación (por defecto: 300ms)
  • .delay(ms) — Retraso antes de iniciar
  • .randomDelay() — Retraso aleatorio entre 0 y el delay configurado
  • .springify() — Convierte la animación en una basada en resorte (solo iOS y Android)
  • .withCallback(fn) — Callback que se ejecuta al terminar la animación
  • .withInitialValues(values) — Sobreescribe los valores iniciales

Ejemplo práctico: lista animada de tareas

import { useState } from 'react';
import { View, Text, Pressable, TextInput, StyleSheet } from 'react-native';
import Animated, {
  FadeInRight,
  FadeOutLeft,
  LinearTransition,
} from 'react-native-reanimated';

interface Todo {
  id: string;
  text: string;
}

export function AnimatedTodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState('');

  const addTodo = () => {
    if (!input.trim()) return;
    setTodos((prev) => [
      { id: Date.now().toString(), text: input.trim() },
      ...prev,
    ]);
    setInput('');
  };

  const removeTodo = (id: string) => {
    setTodos((prev) => prev.filter((t) => t.id !== id));
  };

  return (
    <View style={styles.container}>
      <View style={styles.inputRow}>
        <TextInput
          value={input}
          onChangeText={setInput}
          placeholder="Nueva tarea..."
          style={styles.input}
          onSubmitEditing={addTodo}
        />
        <Pressable onPress={addTodo} style={styles.addButton}>
          <Text style={styles.addText}>+</Text>
        </Pressable>
      </View>

      {todos.map((todo) => (
        <Animated.View
          key={todo.id}
          entering={FadeInRight.duration(300)}
          exiting={FadeOutLeft.duration(250)}
          layout={LinearTransition.springify()}
          style={styles.todoItem}
        >
          <Text style={styles.todoText}>{todo.text}</Text>
          <Pressable onPress={() => removeTodo(todo.id)}>
            <Text style={styles.removeText}>✕</Text>
          </Pressable>
        </Animated.View>
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { gap: 8 },
  inputRow: { flexDirection: 'row', gap: 10, marginBottom: 12 },
  input: {
    flex: 1,
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 10,
    padding: 12,
    fontSize: 16,
  },
  addButton: {
    backgroundColor: '#6C63FF',
    width: 48,
    height: 48,
    borderRadius: 10,
    justifyContent: 'center',
    alignItems: 'center',
  },
  addText: { color: '#fff', fontSize: 24, fontWeight: '700' },
  todoItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    backgroundColor: '#F8F8FF',
    padding: 16,
    borderRadius: 10,
  },
  todoText: { fontSize: 16, color: '#333', flex: 1 },
  removeText: { color: '#FF4757', fontSize: 18, fontWeight: '700', paddingLeft: 12 },
});

Cada tarea entra desde la derecha con un fade, sale hacia la izquierda al eliminarla, y cuando un elemento desaparece los demás se reposicionan suavemente gracias a LinearTransition.springify(). Es el mismo tipo de animación que encuentras en apps como Todoist o Apple Reminders. Y sinceramente, con tan poco código el resultado es bastante impresionante.

Cuándo usar CSS vs Worklets: guía de decisión

Con dos sistemas de animación disponibles, la pregunta obvia es: ¿cuándo uso cada uno? Esta tabla resume bastante bien la decisión:

Caso de uso Enfoque recomendado
Mostrar/ocultar elementos (modales, tooltips) CSS Transitions
Cambios de color, opacidad, tamaño por estado CSS Transitions
Loaders, spinners, efectos de carga CSS Keyframes
Skeleton loaders y shimmer CSS Keyframes
Animaciones de pulso o notificación CSS Keyframes / Presets
Drag & drop, swipe-to-delete Worklets + Gesture Handler
Parallax o headers colapsables al scroll Worklets + useAnimatedScrollHandler
Pinch-to-zoom en imágenes Worklets + Gesture Handler
Animaciones con física (spring, decay) Worklets (withSpring, withDecay)
Montaje/desmontaje de componentes Layout Animations

La regla general: empieza siempre con CSS Transitions o Keyframes. Son más simples, más fáciles de mantener y cubren la mayoría de casos. Solo baja a worklets cuando necesitas control frame-a-frame, respuesta a gestos en tiempo real, o animaciones que no dependen de cambios de estado de React. En mi experiencia, empezar con CSS y migrar a worklets solo cuando hace falta te ahorra bastante complejidad.

Mejores prácticas de rendimiento para animaciones

Animaciones fluidas a 60 FPS no suceden por accidente. Requieren prestar atención a algunos principios clave que, una vez que los interiorizas, se vuelven segunda naturaleza.

1. Anima solo propiedades compositables

Las propiedades transform y opacity son las más baratas de animar porque no requieren recalcular el layout. Animar width, height, padding o margin es bastante más costoso porque fuerza un re-layout de los elementos vecinos. Siempre que puedas, usa transform: [{ scale }] en lugar de animar dimensiones directamente.

2. Define keyframes fuera del componente

Si defines un objeto de keyframes dentro del render, se crea una nueva referencia en cada re-render, lo que puede causar reinicios inesperados de la animación. Define los keyframes como constantes fuera del componente:

// ✅ Bien: fuera del componente
const fadeKeyframes = {
  from: { opacity: 0 },
  to: { opacity: 1 },
};

// ❌ Mal: dentro del render (se recrea cada vez)
function MyComponent() {
  return (
    <Animated.View style={{ animationName: { from: { opacity: 0 }, to: { opacity: 1 } } }} />
  );
}

3. Usa cancelAnimation para limpiar

Si un componente se desmonta mientras una animación de worklet está corriendo, puede causar warnings o comportamiento inesperado. La solución es usar cancelAnimation en el cleanup de un efecto:

import { useEffect } from 'react';
import { useSharedValue, withRepeat, withTiming, cancelAnimation } from 'react-native-reanimated';

function MyComponent() {
  const rotation = useSharedValue(0);

  useEffect(() => {
    rotation.value = withRepeat(withTiming(360, { duration: 2000 }), -1);
    return () => cancelAnimation(rotation);
  }, []);

  // ...
}

4. Cuidado con listas grandes de elementos animados

Renderizar muchos componentes Reanimated dentro de FlatList o FlashList puede causar drops de FPS. Si tienes listas largas con elementos animados, considera usar LayoutAnimationConfig con skipEntering cuando la lista se monta inicialmente, y reserva las animaciones para cambios individuales. Tu usuario no va a notar la diferencia en el montaje inicial, pero sí va a notar si la lista se traba.

5. Siempre mide el rendimiento real

Usa React Native DevTools para monitorear los FPS del hilo de UI y del hilo de JS. Una animación puede verse perfecta en el simulador pero tener problemas en dispositivos de gama baja. Prueba siempre en dispositivos reales y en modo release (--variant release). Este paso es innegociable.

Migración de Reanimated 3 a Reanimated 4

Si tu proyecto ya usa Reanimated 3, la migración a la versión 4 es generalmente sencilla gracias a la compatibilidad hacia atrás. Pero hay algunos puntos que debes tener en cuenta.

Cambios obligatorios

  1. Instalar react-native-worklets como dependencia separada.
  2. Cambiar el plugin de Babel de react-native-reanimated/plugin a react-native-worklets/plugin.
  3. Verificar la Nueva Arquitectura: Reanimated 4 solo funciona con Fabric. Si todavía usas Paper, necesitas migrar a React Native 0.76+ o quedarte en Reanimated 3.x.

APIs renombradas

Algunas funciones que se movieron al paquete react-native-worklets fueron renombradas:

  • runOnJSscheduleOnRN (aunque runOnJS sigue funcionando como alias deprecado)
  • runOnUIscheduleOnUI
  • executeOnUIRuntimeSyncrunOnUISync

APIs eliminadas

  • useAnimatedGestureHandler fue eliminado. Usa la API de Gesture de react-native-gesture-handler 2.x en su lugar.
  • useWorkletCallback también fue eliminado.

Tu código existente de worklets, shared values y useAnimatedStyle seguirá funcionando sin cambios. La migración es una buena oportunidad para empezar a usar las nuevas CSS Animations en las partes de tu app donde encajen mejor.

Preguntas frecuentes

¿Reanimated 4 funciona con la arquitectura antigua de React Native?

No. Reanimated 4 requiere exclusivamente la Nueva Arquitectura (Fabric). Si tu proyecto aún usa la arquitectura antigua (Paper), necesitas actualizar a React Native 0.76 o superior, que trae la Nueva Arquitectura habilitada por defecto. Si no puedes migrar todavía, Reanimated 3.x sigue recibiendo mantenimiento y funciona con ambas arquitecturas.

¿Cuál es la diferencia entre CSS Transitions y CSS Keyframes en Reanimated 4?

Las CSS Transitions animan automáticamente el cambio entre dos valores cuando una propiedad cambia mediante el estado de React. Son ideales para cambios simples como mostrar/ocultar, cambiar colores o redimensionar. Los CSS Keyframes, por otro lado, permiten definir animaciones con múltiples pasos intermedios y pueden repetirse indefinidamente — piensa en loaders, shimmer y pulsos. La diferencia clave: transitions reaccionan a cambios de estado, keyframes corren de forma autónoma.

¿Necesito react-native-gesture-handler para usar Reanimated 4?

No necesariamente. Reanimated 4 funciona perfectamente solo para animaciones CSS, keyframes, layout animations y worklets sin gestos. Ahora bien, si necesitas animaciones controladas por gestos del usuario (arrastrar, deslizar, pellizcar), ahí sí vas a necesitar react-native-gesture-handler. Se integra directamente con los shared values de Reanimated para crear interacciones fluidas a 60+ FPS.

¿Cómo afecta Reanimated 4 al tamaño del bundle de mi app?

Reanimated 4 modularizó su código extrayendo los worklets al paquete separado react-native-worklets. Esto significa que si solo usas CSS Animations sin worklets avanzados, el tree-shaking puede reducir el tamaño del bundle. En la práctica, Reanimated añade aproximadamente 200-300KB al bundle nativo, lo cual es bastante razonable considerando todo lo que te da a cambio.

¿Puedo combinar CSS Animations y Worklets en el mismo componente?

Sí, sin problema. Puedes combinar ambos sistemas en el mismo componente e incluso en la misma vista. Por ejemplo, CSS Transitions para el color de fondo y worklets para una animación de gesto simultánea. Reanimated 4 fue diseñado para que ambos enfoques coexistan sin conflictos, así que puedes elegir la herramienta óptima para cada propiedad que necesites animar.

Sobre el Autor Editorial Team

Our team of expert writers and editors.