react-gif-timeline
v0.1.0
Published
Control GIF animation timelines with React state — map anchor frames to states and animate between them.
Maintainers
Readme
react-gif-timeline
Control GIF animation timelines with React state. Define anchor frames mapped to states, and the library animates between them (forward or reverse), respecting the GIF's original frame timing.
State A → Frame 0 (paused)
State A→B → Animate frame 0 → frame 20, pause
State B→C → Animate frame 20 → frame 39, pause
State C→A → Animate frame 39 → frame 0 (reverse), pauseMid-transition interruptions are handled gracefully. If the state changes while animating, the new transition starts from the current frame.
Install
bun add react-gif-timelineQuick Start
Hook
import { useGifTimeline } from "react-gif-timeline"
function WeatherWidget() {
const [state, setState] = useState("sunny")
const { canvasRef, currentFrame, isTransitioning } = useGifTimeline({
src: "/weather.gif",
anchors: { sunny: 0, cloudy: 19, rainy: 39 },
activeState: state,
})
return (
<div>
<canvas ref={canvasRef} />
<p>Frame {currentFrame} {isTransitioning && "(transitioning...)"}</p>
<button onClick={() => setState("sunny")}>Sunny</button>
<button onClick={() => setState("cloudy")}>Cloudy</button>
<button onClick={() => setState("rainy")}>Rainy</button>
</div>
)
}Component
import { GifTimeline } from "react-gif-timeline"
function WeatherWidget() {
const [state, setState] = useState("sunny")
return (
<GifTimeline
src="/weather.gif"
anchors={{ sunny: 0, cloudy: 19, rainy: 39 }}
activeState={state}
speed={1.5}
renderLoading={() => <div>Loading...</div>}
renderError={(err) => <div>Error: {err}</div>}
>
{({ currentFrame, totalFrames, transitionProgress }) => (
<p>Frame {currentFrame}/{totalFrames} ({Math.round(transitionProgress * 100)}%)</p>
)}
</GifTimeline>
)
}Anchors
Anchors map your application states to GIF frame indices (0-indexed). Two formats are supported:
Named anchors (recommended)
const anchors = { idle: 0, loading: 15, done: 30 }
useGifTimeline({
anchors,
activeState: "loading", // TypeScript autocompletes keys
})Index-based anchors
const anchors = [0, 15, 30]
useGifTimeline({
anchors,
activeState: 1, // points to frame 15
})Named anchors provide full TypeScript inference. activeState is constrained to the keys you defined.
API
useGifTimeline(options)
Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| src | string \| Blob | yes | - | GIF URL or Blob |
| anchors | number[] \| Record<string, number> | yes | - | Frame indices mapped to states |
| activeState | number \| string | yes | - | Active state (index for arrays, key for records) |
| speed | number | no | 1 | Playback speed multiplier |
| onTransitionStart | (fromFrame, toFrame) => void | no | - | Called when a transition begins |
| onTransitionEnd | (state) => void | no | - | Called when a transition completes |
Return
| Field | Type | Description |
|---|---|---|
| canvasRef | RefObject<HTMLCanvasElement> | Attach to a <canvas> element |
| currentFrame | number | Currently displayed frame index |
| totalFrames | number | Total frames in the GIF |
| isLoaded | boolean | GIF is loaded and parsed |
| error | string \| null | Error message if loading failed |
| isTransitioning | boolean | A transition is in progress |
| transitionProgress | number | 0–1 progress of current transition |
<GifTimeline>
Wraps useGifTimeline with built-in canvas rendering and loading/error states.
Accepts all hook options as props, plus:
| Prop | Type | Description |
|---|---|---|
| className | string | Applied to the canvas element |
| style | CSSProperties | Applied to the canvas element |
| renderLoading | () => ReactNode | Rendered while the GIF loads |
| renderError | (error: string) => ReactNode | Rendered if loading fails |
| children | (result: GifTimelineResult) => ReactNode | Render prop with hook return values |
How It Works
- Parse: the GIF is fetched and decoded using gifuct-js
- Pre-compose: each frame is composited into a full image (resolving GIF delta/patch encoding), enabling instant forward and reverse playback
- Render: frames are drawn to a canvas via
putImageDatawith automatic DPR scaling for retina displays - Animate: when
activeStatechanges, arequestAnimationFrameloop plays through frames using the GIF's original per-frame delays, adjusted by thespeedmultiplier
Direction
- Target frame > current frame → plays forward
- Target frame < current frame → plays in reverse
Interruptions
If activeState changes during a transition, the current animation is cancelled and a new one starts from whatever frame is currently displayed.
TypeScript
The library is fully typed with generics that infer activeState from anchors:
// anchors is Record<string, number> → activeState must be a key
useGifTimeline({
anchors: { idle: 0, active: 20 },
activeState: "idle", // ✅
activeState: "unknown", // ❌ Type error
})
// anchors is number[] → activeState must be number
useGifTimeline({
anchors: [0, 20],
activeState: 0, // ✅
activeState: "idle", // ❌ Type error
})License
MIT
