Skip to content
60fps/ui
@60fps/ui-progressv0.0.1

Progress

ProgressEngine reports a 0..1 progress value on every frame, from one of two sources: a media element (video or audio — it drives playback and tracks currentTime) or an arbitrary duration in milliseconds (it owns the clock off the shared frame loop). Either way you get the same controls — play, pause, reset — and the same events. play() continues from the current position, and starts over from the beginning once complete, exactly like a native media element after ended.

One hook, two sources. The bars are written straight to the DOM from onProgress, so they animate at 60fps without re-rendering — with react-scan on, notice the bars never flash. The panels re-render only on a play/pause/complete transition.

DurationuseProgress(4000)

No media — the engine advances a timeline off the shared frame loop and reports progress each frame.

0%
Paused
MediauseProgress(videoRef)

Same hook, fed a <video> ref. Progress tracks currentTime; the buttons drive playback, and scrubbing the native controls updates the bar too.

0%
Paused

One composable, two sources. The bars bind the reactive progress ref straight in the template — Vue's fine-grained reactivity updates just those bindings each frame, no component re-render.

durationuseProgress(4000)

No media — the engine advances a timeline off the shared frame loop and reports progress each frame.

0%
Paused
MediauseProgress(videoRef)

Same composable, fed a <video> ref. Progress tracks currentTime; the buttons drive playback, and scrubbing the native controls updates the bar too.

0%
Paused

High-frequency by design

Progress changes ~60 times a second, so neither adapter routes it through coarse component state. In React the hook exposes a ref plus an onProgress callback you write straight to the DOM from — zero re-renders per frame. In Vue it returns a reactive Ref<number> you can bind in the template: fine-grained reactivity updates just those bindings. Only the coarse status (isRunning, isComplete) is component state, and it updates on play/pause/complete transitions only — exactly what a play/pause button needs.

useProgress(source, options?)

One hook returns everything: the per-frame progress, the coarse status, the controls, and the engine as an escape hatch.

import { useRef } from 'react';
import { useProgress } from '@60fps/ui-react/progress-engine';

function Player() {
	const videoRef = useRef<HTMLVideoElement>(null);
	const barRef = useRef<HTMLDivElement>(null);

	// `source` is either a media ref or a duration in milliseconds.
	const { isRunning, play, pause } = useProgress(videoRef, {
		onProgress: (value) => {
			// 60fps, no re-render: write it straight to the DOM.
			barRef.current!.style.transform = `scaleX(${value})`;
		},
		onComplete: () => console.log('done')
	});

	return (
		<>
			<video ref={videoRef} src="/clip.mp4" muted playsInline />
			<div ref={barRef} style={{ transformOrigin: 'left' }} />
			<button onClick={isRunning ? pause : play}>{isRunning ? 'Pause' : 'Play'}</button>
		</>
	);
}
<script setup lang="ts">
import { ref } from 'vue';
import { useProgress } from '@60fps/ui-vue/progress-engine';

const video = ref<HTMLVideoElement | null>(null);

// `source` is either a media ref or a duration in milliseconds. `progress` is a reactive
// Ref<number> — bind it in the template, fine-grained reactivity handles the per-frame updates.
const { progress, isRunning, play, pause } = useProgress(video, {
	onComplete: () => console.log('done')
});
</script>

<template>
	<video ref="video" src="/clip.mp4" muted playsinline />
	<div class="bar" :style="{ transform: `scaleX(${progress})`, transformOrigin: 'left' }" />
	<button @click="isRunning ? pause() : play()">{{ isRunning ? 'Pause' : 'Play' }}</button>
</template>
ArgumentTypeDefaultDescription
sourceRefObject<HTMLMediaElement | null> | numberA media element ref (drives playback) or a duration in milliseconds.
options.onProgress(value: number) => voidCalled every frame with 0..1. Runs at 60fps — keep it cheap, never set state here.
options.onComplete() => voidCalled once when progress reaches the end.
options.autoPlaybooleanfalseStart playing as soon as the source is ready.

Return value

FieldTypeDefaultDescription
progressRef / progressRefObject<number> (React) · Ref<number> (Vue)The latest 0..1 value. React: read it imperatively, it never re-renders. Vue: reactive, bind it in the template.
isRunningbooleanWhether progress is currently advancing. Updates on transitions only, never per frame.
isCompletebooleanWhether progress has reached the end.
play() => voidPlay from the current position — from the beginning when complete, like native media.
pause() => voidHold at the current position.
reset() => voidReturn to 0 and stop.
engineProgressEngineThe underlying engine, for its events (engine.on(...)) or imperative use.

Vanilla engine

The hook is a thin wrapper; you can drive ProgressEngine directly from any framework or none:

import { ProgressEngine } from '@60fps/ui-progress';

const engine = new ProgressEngine({ durationMs: 3000 });
engine.on('progress', (value) => (bar.style.transform = `scaleX(${value})`));
engine.on('complete', () => console.log('done'));
engine.play();