Skip to content

Instantly share code, notes, and snippets.

@SukkaW
Created January 6, 2025 08:34
Show Gist options
  • Save SukkaW/d08aea948851e2fc3467543f40292fb0 to your computer and use it in GitHub Desktop.
Save SukkaW/d08aea948851e2fc3467543f40292fb0 to your computer and use it in GitHub Desktop.
'use client';
import { memo, useEffect, useRef, useState } from 'react';
import type { AnimationConfigWithData, AnimationConfigWithPath, AnimationDirection, AnimationEventCallback, AnimationEventName, AnimationItem, AnimationSegment, RendererType } from 'lottie-web';
interface LottieAnimationCallback<T = unknown> {
eventName: AnimationEventName,
callback: AnimationEventCallback<T>
}
interface LottieProps<T extends RendererType = 'svg'> extends Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'onClick'> {
eventListeners?: LottieAnimationCallback[],
options: Omit<AnimationConfigWithPath<T> | AnimationConfigWithData<T>, 'container'>,
height?: string | number,
width?: string | number,
isStopped?: boolean,
isPaused?: boolean,
speed?: number,
segments?: AnimationSegment[],
direction?: AnimationDirection,
isClickToPauseDisabled?: boolean,
title?: string
}
const defaultEventListeners: LottieAnimationCallback[] = [];
function LottieAnimation<T extends RendererType = 'svg'>({
eventListeners = defaultEventListeners,
options,
height,
width,
isStopped = false,
isPaused = false,
speed = 1,
segments,
direction = 1,
role = 'button',
isClickToPauseDisabled = false,
title = '',
style,
...rest
}: LottieProps<T>) {
const ariaLabel = rest['aria-label'] || 'animation';
const el = useRef<HTMLDivElement>(null);
const anim = useRef<AnimationItem | null>(null);
const [lottie, setLottie] = useState<typeof import('lottie-web')['default'] | null>(null);
useEffect(() => {
import('lottie-web').then(mod => setLottie(mod.default)).catch(console.error);
}, []);
useEffect(() => {
if (!lottie) return;
const {
loop = true,
autoplay = true,
renderer = 'svg' as T,
rendererSettings
} = options;
const animationOptions: AnimationConfigWithPath<T> | AnimationConfigWithData<T> = {
loop,
renderer,
autoplay,
rendererSettings,
...options,
container: el.current!
};
const registerEvents = (eventListeners: LottieAnimationCallback[]) => {
eventListeners.forEach((eventListener) => {
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener -- already handled
anim.current?.addEventListener(eventListener.eventName, eventListener.callback);
});
};
const deRegisterEvents = (eventListeners: LottieAnimationCallback[]) => {
eventListeners.forEach((eventListener) => {
anim.current?.removeEventListener(eventListener.eventName, eventListener.callback);
});
};
anim.current = lottie.loadAnimation(animationOptions);
registerEvents(eventListeners);
return () => {
deRegisterEvents(eventListeners);
anim.current?.destroy();
};
}, [options, eventListeners, lottie]);
useEffect(() => {
if (isStopped) {
anim.current?.stop();
} else if (segments) {
anim.current?.playSegments(segments);
} else {
anim.current?.play();
}
if (isPaused) {
anim.current?.pause();
} else {
anim.current?.play();
}
anim.current?.setSpeed(speed);
anim.current?.setDirection(direction);
}, [isStopped, isPaused, speed, direction, segments]);
const handleClickToPause = () => {
if (anim.current?.isPaused) {
anim.current.play();
} else {
anim.current?.pause();
}
};
const lottieStyles: React.CSSProperties = {
width: getSize(width),
height: getSize(height),
overflow: 'hidden',
margin: '0 auto',
outline: 'none',
...style
};
const onClickHandler = isClickToPauseDisabled ? undefined : handleClickToPause;
return (
<div
ref={el}
style={lottieStyles}
onClick={onClickHandler}
title={title}
role={role}
aria-label={ariaLabel}
tabIndex={0}
{...rest}
/>
);
}
function getSize(initial: string | number | undefined) {
if (typeof initial === 'number') {
return `${initial}px`;
}
return initial || '100%';
}
export default memo(LottieAnimation);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment