About this animation
A workout set tracker with an animated exercise queue and trophy reveal, with configurable exercises, trophy image, palette, and fonts.
Why this one
Workout loggers often feel like forms. This one gives each recorded set a yellow fill, a rolling set number, a small dot burst, and a lift in the performed-rep tile. Finished exercises move into the completed group; the next exercise takes focus. The final session becomes a reward with a trophy arrival, silhouette-masked light, counting metrics, and staggered milestones.
How it works
- React stores semantic events only: the active exercise, recorded sets, performed reps, and saved state. A synchronous event ledger keeps rapid imperative calls consistent.
- Recording a set starts a 300 ms shared-value fill and a 520 ms burst. Independent pressure springs give every button immediate 0.97-scale feedback.
- Each numeric column derives its position from a shared value. Higher columns move only on a carry; fading faces finish on exact integer positions. Separators and unit labels occupy their own layout space.
- A completed exercise closes its continuous Skia status ring into a Lucide check. Its row moves above the new active exercise in the same render; the next exercise name and metrics mount together in a single commit. The complete workout fades out before the celebration fades in, with no travel or text overlap; autoplay resets use the same sequential fade.
- One celebration clock drives the trophy arrival, a spring-followed 12° tilt, alpha-masked shine, metric count-up, and 90 ms milestone cascade. The trophy stays inside a clipped slot with at least 16 pt before the heading. Static Lucide glyphs use inline SVG on web to avoid a WebGL context per icon. Reduced motion uses 150–200 ms fades and removes travel, spinning, particles, and shine.
- The demo owns autoplay through the public ref: two sets, the next exercise, an accelerated queue, the reward, and reset. A user action stops autoplay.
Install
Copy this folder without demo/ (keep both SessionIcon platform files), then install compatible dependencies:
npx expo install @shopify/react-native-skia react-native-reanimated react-native-workletsUse the Reanimated/worklets setup supplied by your Expo SDK. On web, load CanvasKit before importing this component with Skia's supported web loader. In this catalog, the host registers Skia pieces using its deferred Skia loader. Icons: Lucide (ISC); the copied paths live in icons.ts.
Usage
import { useRef } from 'react';
import { SetTrackerSession, type SetTrackerSessionRef } from './set-tracker-session';
const ref = useRef<SetTrackerSessionRef>(null);
<SetTrackerSession
ref={ref}
exercises={[
{ id: 'press', name: 'Incline Press', sets: 4, reps: 10, weight: 70, level: 'Advanced' },
{ id: 'row', name: 'Cable Row', sets: 3, reps: 12, weight: 55 },
]}
summary={{ minutes: 58, exercises: 2, totalExercises: 2, calories: 684, reps: 39 }}
trophy={require('./assets/trophy.png')}
milestones={[{ id: 'record', title: 'New personal record', icon: 'trophy' }]}
onSetComplete={(exercise, set, reps) => recordSet(exercise.id, set, reps)}
onSessionComplete={(summary) => recordSession(summary)}
onSave={(summary) => persistSession(summary)}
/>The recordSet, recordSession, and persistSession functions belong to your app. Saving and sharing are callbacks; this component does not persist data or open a share sheet itself.
Customize the full palette, copy, units, and timing:
<SetTrackerSession
exercises={plan}
summary={summary}
fontFamily="YourInterfaceFont"
monoFontFamily="YourCounterFont"
duration={300}
celebrationDuration={850}
trophySize={150}
labels={{ weightUnit: 'kg', complete: 'Record set', amazing: 'Session complete' }}
palette={{
background: '#F5F7FA', surface: '#FFFFFF', border: '#DDE3EC',
text: '#142132', muted: '#53657A', accent: '#FFE15A', accentInk: '#171A12',
blue: '#3D7BFF', success: '#16794E', successSurface: '#E6F4EC',
}}
/>Provide trophy artwork at least three times its rendered size; the demo's cropped 401×485 trophy renders at at most 160 pt with contain fitting.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
exercises | readonly SessionExercise[] | Required | Stable IDs, names, sets, reps, weight, optional level. |
summary | SessionSummary | Required | Caller-owned final minutes, exercise count, calories, and reps. |
milestones | readonly SessionMilestone[] | [] | Reward titles, detail, and bundled Lucide glyph. |
trophy | DataSourceParam | Lucide fallback | Local or remote Skia image source. |
initialExerciseIndex | number | 0 | Earlier exercises begin completed. |
initialCompletedSets | number | 0 | Recorded sets for the first active exercise. |
initialReps | number | Exercise target | Initial performed repetitions. |
palette | Partial<SessionPalette> | Night blue/yellow | All surface, text, border, light, shadow, and status colors. |
labels | Partial<SessionLabels> | American English | All structural copy, units, and announcements. |
fontFamily | string | System | Interface typeface. |
titleFontFamily | string | fontFamily | Exercise and completion headings. |
monoFontFamily | string | fontFamily | Rolling counters. |
header | ReactNode | None | Host-owned brand or header. |
duration | number | 300 | Set fill duration in ms. |
celebrationDuration | number | 850 | Reward timeline duration in ms. |
trophySize | number | 160 | Trophy size; hard maximum 180 pt. |
queueWindow | number | 5 | Number of visible rows including the latest completed row. |
formatNumber | (number) => string | String | Alternate formatting; custom formats use static accessible text. |
onSetComplete | (exercise, completedSets, reps) => void | None | One callback for each recorded set. |
onSessionComplete | (summary) => void | None | Once per finished session. |
onSave, onShare | (summary) => void | None | Host persistence and sharing actions. |
onInteraction | () => void | None | Stops demo autoplay or informs the host. |
style | StyleProp<ViewStyle> | None | Root override. |
testID | string | set-tracker-session | Prefix for stable control identifiers. |
The ref exposes completeSet(), finishSession(), and reset(). finishSession() opens the supplied summary without synthesizing individual set callbacks. Finish completes the active exercise; Start switches to a pending exercise while preserving previously recorded sets. reset() restores the initial configuration.
Compatibility
The root uses flex: 1, measures its own container with onLayout, limits content width to 420 pt, and scrolls when needed. It reserves 54 pt at the top and keeps the header left-aligned for the catalog close control. The host owns safe-area insets; wrap this component in your app's safe-area container. Small slots and landscape rely on scrolling rather than shrinking touch targets. Supply stable exercise IDs and positive integer set counts. Change a React key to replace a workout plan, or update props and call reset().
Built for Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, and Skia 2.14.0. These native modules are included in this project's Expo Go setup.
Accessibility
All actions have button roles, spoken labels, and at least 48 pt touch dimensions. Rep controls, queue actions, saving, and sharing have stable test IDs. Screen readers receive semantic announcements for recorded sets, exercise changes, session completion, and saving. Rolling numbers have a single readable numeric label. Every interaction has a direct tap action; there is no gesture-only operation. Reduced motion replaces movement with fades and omits particles and light sweeps.



