MoveKitMoveKit
Back to Blog

How to Add Professional 3D Exercise Animations to Your React Native Fitness App

Step-by-step guide to integrating loopable 3D exercise animations into a React Native app using expo-av. Covers video playback, muscle highlights, and building an exercise picker.

April 2, 202612 min read
How to Add Professional 3D Exercise Animations to Your React Native Fitness App

Quick Answer

How do I add exercise animations to a React Native app?

Use MoveKit's loopable MP4 clips with expo-av (or react-native-video). Each clip is H.264, 720p, 30fps, and loops seamlessly. Load from CDN, set isLooping and isMuted, and you have professional 3D exercise demonstrations in about 15 minutes.

  • expo-av or react-native-video for playback
  • MoveKit clips are silent, loopable MP4s — no audio session conflicts
  • Preview tier (480p, ~300 KB) for grids, standard tier (720p) for detail views
  • Muscle highlight variants included for educational fitness apps
  • Commercial license included — $1.99/clip or $59.99 for all 51 exercises

Every fitness app needs exercise demonstrations. But finding consistent, professional-quality animations is surprisingly hard. Most developers end up with a mishmash of stock GIFs, inconsistent art styles, or expensive custom work that takes weeks to produce.

This tutorial shows you how to add clean, loopable 3D exercise animations to a React Native app using MoveKit clips and expo-av. By the end, you will have a polished exercise browser with professional animations playing in your app.

Chestintermediate

Barbell Bench Press

Barbell
View in Library →

What We Are Building

A simple exercise browser that:

  • Loads 3D exercise animations from a CDN
  • Plays them in seamless loops with no stutter
  • Displays muscle group info and difficulty
  • Lets users browse exercises in a filterable grid

Prerequisites

  • React Native project with Expo (SDK 51+)
  • Basic React Native knowledge
  • A few MoveKit exercise clips — grab the free sample pack to follow along

Step 1: Install expo-av

We use expo-av for video playback. It handles looping, muting, and background behavior out of the box.

npx expo install expo-av

If you prefer bare React Native without Expo, react-native-video (v6+) works too. The API is slightly different but the concepts are identical.

Step 2: Understand the File Structure

Each MoveKit exercise comes as an MP4 optimized for app use:

  • Format: H.264 MP4 — universal iOS and Android compatibility
  • Resolution: 720p (HD) — sharp on mobile without bloating your bundle
  • Framerate: 30fps smooth animation
  • Loopable: seamless first-to-last frame, no stutter when repeating
  • Silent: no audio track — will not interfere with the user's music

Exercise Data Shape

Each exercise has metadata you will want in your app:

interface Exercise {
  slug: string;             // e.g. "barbell-bench-press"
  name: string;             // e.g. "Barbell Bench Press"
  primaryMuscles: string[]; // e.g. ["Chest"]
  secondaryMuscles: string[];
  equipment: string[];      // e.g. ["Barbell"]
  difficulty: "beginner" | "intermediate" | "advanced";
  durationSeconds: number;  // animation loop length
  videoUrl: string;         // CDN URL to the MP4
  posterUrl: string;        // first-frame thumbnail (WebP)
}

Step 3: Build the Exercise Video Player

Here is a reusable component that plays a MoveKit animation in a seamless loop:

// components/ExerciseVideo.tsx
import React from "react";
import { View, StyleSheet } from "react-native";
import { Video, ResizeMode } from "expo-av";

interface ExerciseVideoProps {
  videoUrl: string;
  posterUrl?: string;
  style?: object;
}

export function ExerciseVideo({
  videoUrl, posterUrl, style,
}: ExerciseVideoProps) {
  return (
    <View style={[styles.container, style]}>
      <Video
        source={{ uri: videoUrl }}
        posterSource={posterUrl ? { uri: posterUrl } : undefined}
        usePoster={!!posterUrl}
        posterStyle={styles.poster}
        shouldPlay
        isLooping
        isMuted
        resizeMode={ResizeMode.CONTAIN}
        style={styles.video}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    width: "100%",
    aspectRatio: 1,
    borderRadius: 16,
    overflow: "hidden",
    backgroundColor: "#0a0a0a",
  },
  video: { width: "100%", height: "100%" },
  poster: { resizeMode: "contain" },
});

Key details:

  • isLooping — MoveKit clips loop seamlessly. This flag makes it automatic.
  • isMuted — clips have no audio, but setting this avoids iOS audio session conflicts.
  • shouldPlay — starts playback immediately on mount.
  • usePoster — shows the first-frame thumbnail while loading, preventing a flash of black.

Step 4: Create an Exercise Card

Wrap the video player in a card that shows exercise metadata:

// components/ExerciseCard.tsx
import React from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { ExerciseVideo } from "./ExerciseVideo";

const difficultyColors = {
  beginner: "#10b981",
  intermediate: "#f59e0b",
  advanced: "#ef4444",
};

export function ExerciseCard({ exercise, onPress }) {
  return (
    <Pressable style={styles.card} onPress={() => onPress?.(exercise)}>
      <ExerciseVideo
        videoUrl={exercise.videoUrl}
        posterUrl={exercise.posterUrl}
      />
      <View style={styles.info}>
        <View style={styles.badges}>
          <View style={styles.muscleBadge}>
            <Text style={styles.badgeText}>
              {exercise.primaryMuscles[0]}
            </Text>
          </View>
          <View style={[
            styles.difficultyBadge,
            { backgroundColor: difficultyColors[exercise.difficulty] + "20" },
          ]}>
            <Text style={[
              styles.badgeText,
              { color: difficultyColors[exercise.difficulty] },
            ]}>
              {exercise.difficulty}
            </Text>
          </View>
        </View>
        <Text style={styles.name}>{exercise.name}</Text>
        <Text style={styles.equipment}>{exercise.equipment[0]}</Text>
      </View>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  card: {
    borderRadius: 16,
    backgroundColor: "#111",
    overflow: "hidden",
    borderWidth: 1,
    borderColor: "#222",
  },
  info: { padding: 12, gap: 6 },
  badges: { flexDirection: "row", gap: 6 },
  muscleBadge: {
    backgroundColor: "#1a1a2e",
    paddingHorizontal: 8,
    paddingVertical: 3,
    borderRadius: 6,
  },
  difficultyBadge: {
    paddingHorizontal: 8,
    paddingVertical: 3,
    borderRadius: 6,
  },
  badgeText: {
    fontSize: 11,
    fontWeight: "600",
    color: "#a0a0b0",
    textTransform: "capitalize",
  },
  name: { fontSize: 15, fontWeight: "600", color: "#fff" },
  equipment: { fontSize: 13, color: "#666" },
});

Step 5: Build the Exercise Picker Grid

Create a scrollable, filterable grid of exercises:

// screens/ExercisePickerScreen.tsx
import React, { useState } from "react";
import {
  View, Text, FlatList, StyleSheet,
  SafeAreaView, ScrollView, Pressable,
} from "react-native";
import { ExerciseCard } from "../components/ExerciseCard";

// Replace with your own CDN URLs after purchasing
const EXERCISES = [
  {
    slug: "barbell-bench-press",
    name: "Barbell Bench Press",
    primaryMuscles: ["Chest"],
    equipment: ["Barbell"],
    difficulty: "intermediate",
    videoUrl: "YOUR_CDN/barbell-bench-press/video-preview.mp4",
    posterUrl: "YOUR_CDN/barbell-bench-press/poster-thumb.webp",
  },
  {
    slug: "dumbbell-curl",
    name: "Dumbbell Curl",
    primaryMuscles: ["Biceps"],
    equipment: ["Dumbbell"],
    difficulty: "beginner",
    videoUrl: "YOUR_CDN/dumbbell-curl/video-preview.mp4",
    posterUrl: "YOUR_CDN/dumbbell-curl/poster-thumb.webp",
  },
  // ... add all purchased exercises
];

const FILTERS = ["All", "Chest", "Back", "Biceps", "Quadriceps", "Core"];

export function ExercisePickerScreen() {
  const [muscle, setMuscle] = useState("All");
  const filtered = muscle === "All"
    ? EXERCISES
    : EXERCISES.filter((e) => e.primaryMuscles.includes(muscle));

  return (
    <SafeAreaView style={styles.container}>
      <Text style={styles.title}>Exercises</Text>
      <ScrollView
        horizontal
        showsHorizontalScrollIndicator={false}
        contentContainerStyle={styles.filters}
      >
        {FILTERS.map((f) => (
          <Pressable
            key={f}
            style={[styles.chip, muscle === f && styles.chipActive]}
            onPress={() => setMuscle(f)}
          >
            <Text style={[
              styles.chipText,
              muscle === f && styles.chipTextActive,
            ]}>{f}</Text>
          </Pressable>
        ))}
      </ScrollView>
      <FlatList
        data={filtered}
        keyExtractor={(item) => item.slug}
        numColumns={2}
        columnWrapperStyle={styles.row}
        contentContainerStyle={styles.grid}
        renderItem={({ item }) => (
          <View style={styles.gridItem}>
            <ExerciseCard exercise={item} />
          </View>
        )}
      />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: "#000" },
  title: {
    fontSize: 28, fontWeight: "700", color: "#fff",
    paddingHorizontal: 16, paddingTop: 16, paddingBottom: 8,
  },
  filters: { paddingHorizontal: 16, paddingVertical: 12, gap: 8 },
  chip: {
    paddingHorizontal: 14, paddingVertical: 7, borderRadius: 20,
    backgroundColor: "#1a1a1a", borderWidth: 1, borderColor: "#333",
  },
  chipActive: { backgroundColor: "#fff", borderColor: "#fff" },
  chipText: { fontSize: 13, fontWeight: "500", color: "#999" },
  chipTextActive: { color: "#000" },
  grid: { padding: 12 },
  row: { gap: 12, marginBottom: 12 },
  gridItem: { flex: 1 },
});

Step 6: Optimize for Performance

Use Preview Tier for Grids

MoveKit exercises come in multiple tiers:

  • Preview (480p): ~200-400 KB — perfect for list and grid thumbnails
  • Standard (720p): ~1-2 MB — for detail views and downloads

Use the smaller preview tier in grids and switch to the standard tier when a user taps into the detail view.

Cache Videos Locally

For offline support, download the MP4s at install time and serve from the local filesystem:

import * as FileSystem from "expo-file-system";

async function cacheExerciseVideo(slug: string, remoteUrl: string) {
  const localUri =
    `${FileSystem.documentDirectory}exercises/${slug}.mp4`;
  const fileInfo = await FileSystem.getInfoAsync(localUri);
  if (fileInfo.exists) return localUri;

  await FileSystem.makeDirectoryAsync(
    `${FileSystem.documentDirectory}exercises/`,
    { intermediates: true }
  );
  const download = await FileSystem.downloadAsync(remoteUrl, localUri);
  return download.uri;
}

Lazy Load in Long Lists

In lists with 20+ exercises, avoid autoplaying every video. Use FlatList's onViewableItemsChanged callback to track which items are visible and only play those.

Step 7: Muscle Highlight Variants

Every MoveKit exercise includes a muscle highlight variant — the same animation with colored overlays showing which muscles are being worked. Primary muscles appear in a strong highlight, secondary muscles in a lighter shade.

Backintermediate

Barbell Bent Over Row

Barbell
View in Library →

This is perfect for educational fitness apps where users want to understand which muscles each exercise targets. You can add a toggle button to switch between the standard and highlight variants.

Styling Tips

  • Dark backgrounds work best. The MoveKit mannequin is light-colored on a dark background. Match your exercise views to a dark theme for visual consistency.
  • Square aspect ratio. The animations are rendered in a square frame, so use aspectRatio: 1 for the cleanest look.
  • No controls needed. These are short, looping animations — not long-form video. Hide playback controls and let them autoplay silently.
  • Poster images prevent flash. Always use the poster thumbnail as a placeholder while the video loads. MoveKit provides WebP thumbnails specifically for this.

What You Get with MoveKit

  • 51 exercises across all major muscle groups (chest, back, shoulders, arms, legs, core)
  • Consistent 3D mannequin — same character and art style across every animation
  • Muscle highlight variants — colored overlays showing primary and secondary muscles
  • Loopable MP4s — H.264, 720p, 30fps, seamless loops
  • Commercial license included — use in your published app, no extra fees
  • Individual clips at $1.99 or the full library for $59.99
  • Free sample pack — 2 exercises to test before you buy

Next Steps

  1. Grab the free sample pack to test with real clips
  2. Browse the full exercise library to see all 51 exercises
  3. Check the licensing page for commercial use details

If you are building a fitness app and tired of inconsistent stock animations, MoveKit saves you weeks of sourcing and gives your app a professional look from day one.

FAQ

Does MoveKit work with React Native CLI (no Expo)?

Yes. Use react-native-video (v6+) instead of expo-av. The component API differs slightly — use the repeat prop instead of isLooping and muted instead of isMuted — but the MoveKit MP4 files work identically.

What about Flutter or SwiftUI?

MoveKit clips are standard H.264 MP4 files. They work in any platform that supports video playback. In Flutter, use video_player or chewie. In SwiftUI, use AVPlayer with AVPlayerLooper. The integration pattern is the same: load the URL, set to loop, mute, and autoplay.

How large are the files?

Preview tier clips (480p) are 200-400 KB each — lightweight enough for list views. Standard tier clips (720p) are 1-2 MB — perfect for detail screens. The full library of 51 exercises totals around 50-80 MB at standard quality.

Can I host the files myself?

Yes. After purchase, you download the MP4 files and can host them anywhere — your own CDN, S3, Firebase Storage, or bundle them directly into your app. There is no lock-in to MoveKit's CDN.

Is the commercial license really included?

Yes. Every purchase includes a commercial license. You can use the animations in published iOS, Android, and web apps, in online courses, and in social media content. The only restriction is you cannot resell the raw animation files or include them in a competing marketplace. See the licensing page for full details.