Free
Open source • Copy & pasteFit Track
Workout dashboard kit
Activity rings, exercise timers, and progress cards for fitness apps.
2 screens
Expo Router + StyleSheet + Reanimated + SVG
Motion-ready
workoutstatstimer
Source code
Explore the component structure. Copy directly into any Expo app.
import { Ionicons } from '@expo/vector-icons';
import { Stack, useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
ScrollView,
StatusBar,
TouchableOpacity,
View,
Dimensions,
Pressable,
} from 'react-native';
import Animated, {
FadeInDown,
useAnimatedProps,
useSharedValue,
withTiming,
withRepeat,
withSequence,
useAnimatedStyle,
withSpring,
Easing,
interpolate,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Svg, { Circle, G } from 'react-native-svg';
import { Text } from '@/components/ui';
import WorkoutCard from '../components/workout-card';
import { FITNESS_COLORS, MOCK_WORKOUTS, WEEKLY_STATS } from '../constants';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
const { width } = Dimensions.get('window');
interface ActivityRingProps {
radius: number;
stroke: number;
color: string;
progress: number; // 0 to 1
delay?: number;
}
const ActivityRing = ({
radius,
stroke,
color,
progress,
delay = 0,
}: ActivityRingProps) => {
const circumference = 2 * Math.PI * radius;
const progressValue = useSharedValue(0);
useEffect(() => {
progressValue.value = withTiming(progress, {
duration: 1500,
easing: Easing.out(Easing.exp),
});
}, [progress]);
const animatedProps = useAnimatedProps(() => {
const strokeDashoffset = interpolate(
progressValue.value,
[0, 1],
[circumference, 0],
);
return {
strokeDashoffset,
};
});
return (
<View className="absolute items-center justify-center">
<Svg height={radius * 2 + stroke} width={radius * 2 + stroke}>
<G
rotation="-90"
origin={`${radius + stroke / 2}, ${radius + stroke / 2}`}
>
<Circle
cx="50%"
cy="50%"
r={radius}
stroke={color}
strokeWidth={stroke}
strokeOpacity={0.2}
fill="transparent"
/>
<AnimatedCircle
cx="50%"
cy="50%"
r={radius}
stroke={color}
strokeWidth={stroke}
fill="transparent"
strokeDasharray={circumference}
animatedProps={animatedProps}
strokeLinecap="round"
/>
</G>
</Svg>
</View>
);
};
const StatCard = ({
icon,
label,
value,
color,
details,
}: {
icon: keyof typeof Ionicons.glyphMap;
label: string;
value: string;
color: string;
details: string;
}) => {
const [expanded, setExpanded] = useState(false);
const scale = useSharedValue(1);
const toggleExpand = () => {
setExpanded(!expanded);
scale.value = withSequence(
withTiming(0.95, { duration: 100 }),
withTiming(1, { duration: 100 }),
);
};
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Pressable onPress={toggleExpand} className="flex-1">
<Animated.View
style={[animatedStyle]}
className={`bg-zinc-900 p-4 rounded-2xl border border-zinc-800 ${expanded ? 'border-primary/50' : ''}`}
>
<View className="flex-row items-center gap-2 mb-2">
<Ionicons name={icon} size={20} color={color} />
<Text className="text-zinc-400 text-xs font-bold uppercase">
{label}
</Text>
</View>
<Text className="text-white text-xl font-black">{value}</Text>
{expanded && (
<Animated.View entering={FadeInDown.duration(200)} className="mt-2">
<Text className="text-zinc-500 text-xs leading-4">{details}</Text>
</Animated.View>
)}
</Animated.View>
</Pressable>
);
};
const HeartRate = () => {
const scale = useSharedValue(1);
useEffect(() => {
scale.value = withRepeat(
withSequence(
withTiming(1.2, { duration: 100 }),
withTiming(1, { duration: 100 }),
withTiming(1.2, { duration: 100 }),
withTiming(1, { duration: 400 }), // Pause between beats
),
-1,
true,
);
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<Animated.View style={animatedStyle}>
<Ionicons name="heart" size={24} color={FITNESS_COLORS.danger} />
</Animated.View>
);
};
export default function FitnessDashboard() {
const router = useRouter();
const insets = useSafeAreaInsets();
return (
<View className="flex-1 bg-zinc-950">
<Stack.Screen options={{ headerShown: false }} />
<StatusBar barStyle="light-content" />
<ScrollView
className="flex-1"
contentContainerStyle={{
paddingTop: insets.top + 20,
paddingBottom: 100,
paddingHorizontal: 20,
}}
showsVerticalScrollIndicator={false}
>
{/* Header */}
<Animated.View
entering={FadeInDown.delay(100)}
className="flex-row justify-between items-center mb-8"
>
<View>
<Text className="text-zinc-400 text-sm font-medium uppercase tracking-widest mb-1">
Thursday, 12 Oct
</Text>
<Text className="text-white text-3xl font-black">
Hello, Runner{' '}
<Text style={{ color: FITNESS_COLORS.primary }}>⚡️</Text>
</Text>
</View>
<TouchableOpacity className="w-12 h-12 rounded-full border border-zinc-800 items-center justify-center bg-zinc-900">
<Ionicons name="notifications-outline" size={24} color="white" />
</TouchableOpacity>
</Animated.View>
{/* Activity Rings Section */}
<Animated.View
entering={FadeInDown.delay(200)}
className="w-full bg-zinc-900 p-6 rounded-[32px] mb-8 border border-zinc-800"
>
<View className="flex-row items-center justify-between mb-6">
<View>
<Text className="text-white text-4xl font-black mb-1">
Active
</Text>
<Text className="text-zinc-500 font-medium tracking-wide">
Today's Progress
</Text>
</View>
{/* Rings Container */}
<View className="w-24 h-24 items-center justify-center relative">
<ActivityRing
radius={40}
stroke={8}
color={FITNESS_COLORS.ring1}
progress={0.75}
/>
<ActivityRing
radius={28}
stroke={8}
color={FITNESS_COLORS.ring3}
progress={0.85}
/>
</View>
</View>
{/* Stats Grid */}
<View className="flex-row gap-3">
<StatCard
icon="flame"
label="Calories"
value="840"
color={FITNESS_COLORS.ring1}
details="240 Active, 600 Resting"
/>
<StatCard
icon="time"
label="Time"
value="45m"
color={FITNESS_COLORS.ring3}
details="15m Remaining"
/>
</View>
</Animated.View>
{/* Heart Rate Section */}
<Animated.View
entering={FadeInDown.delay(300)}
className="mb-8 flex-row items-center justify-between bg-zinc-900/50 p-4 rounded-2xl border border-zinc-800"
>
<View className="flex-row items-center gap-3">
<View className="w-10 h-10 rounded-full bg-red-500/10 items-center justify-center">
<HeartRate />
</View>
<View>
<Text className="text-white font-bold text-lg">Heart Rate</Text>
<Text className="text-zinc-500 text-xs">Avg. 128 bpm</Text>
</View>
</View>
<View className="flex-row items-end gap-1">
<Text className="text-white text-2xl font-black">112</Text>
<Text className="text-zinc-500 text-sm font-medium mb-1">bpm</Text>
</View>
</Animated.View>
{/* Weekly Activity Chart */}
<Animated.View entering={FadeInDown.delay(400)} className="mb-8">
<View className="flex-row justify-between items-end mb-4">
<Text className="text-white text-xl font-bold">Activity</Text>
<Text
style={{ color: FITNESS_COLORS.primary }}
className="font-bold"
>
Weekly Goal
</Text>
</View>
<View className="bg-zinc-900 rounded-3xl p-5 flex-row justify-between items-end h-48 border border-zinc-800">
{WEEKLY_STATS.map((stat, i) => (
<View key={i} className="items-center gap-2">
<View className="w-8 bg-zinc-800 rounded-full overflow-hidden relative justify-end h-32">
<Animated.View
entering={FadeInDown.delay(i * 50 + 500)}
style={{
height: `${(stat.value / 1000) * 100}%`,
width: '100%',
backgroundColor: stat.active
? FITNESS_COLORS.primary
: '#27272a',
borderRadius: 100,
}}
/>
</View>
<Text
className={`text-xs font-bold ${stat.active ? 'text-white' : 'text-zinc-600'}`}
>
{stat.day}
</Text>
</View>
))}
</View>
</Animated.View>
{/* Popular Workouts */}
<Animated.View entering={FadeInDown.delay(500)}>
<View className="flex-row justify-between items-center mb-4">
<Text className="text-white text-xl font-bold">
Popular Workouts
</Text>
<Text className="text-zinc-500 font-bold">See All</Text>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={{ marginHorizontal: -20, paddingHorizontal: 20 }}
>
{MOCK_WORKOUTS.map((item, index) => (
<WorkoutCard
key={item.id}
item={item}
index={index}
onPress={() => {}}
/>
))}
</ScrollView>
</Animated.View>
</ScrollView>
</View>
);
}
Building a full app?
VibeFast Pro is a complete Expo + Next.js starter kit with authentication, payments (RevenueCat), AI features, backend (Convex/Supabase), and more. Ship your app in days, not months.