Skip to main content

useSynchronizedAnimation

The useSynchronizedAnimation hook keeps every element running the same CSS animation in phase, no matter when each one mounts. Without it, a spinner rendered a second after another spinner starts its keyframes from zero and the two visibly drift apart. More Info...

import { useSynchronizedAnimation } from '@archibald/storefront';

const ref = useSynchronizedAnimation<SVGSVGElement>(animationName);

On mount the hook reads document.getAnimations(), picks out the animations whose animationName matches, and copies the currentTime of the first one onto this element's animation. When the first element unmounts its time is stashed, so the next element to mount resumes from there instead of restarting.

Parameters

NameTypeRequiredDescription
animationNamestring✔️The name of the CSS @keyframes animation to synchronize.

Type parameter

NameDefaultDescription
TanyThe element type the returned ref is attached to, e.g. SVGSVGElement.

Return Value

  • Type: LegacyRef<T>
  • Description: A ref to attach to the element that carries the animation — not a wrapper around it.
note

Synchronization is web-only. @archibald/storefront's native entry point (src/native.ts, selected by the react-native condition in the package's exports map) re-exports a separate implementation from ./hooks/misc/useSynchonizedAnimations.native, and that version just returns an inert ref — it never touches document. The web implementation additionally guards on document.getAnimations and no-ops if the API is missing.

So the hook is always safe to call from shared code, but a component that renders on native must not depend on the animations actually being in phase. Note the native override also defaults its type parameter to unknown rather than any.


Practical Code Example

Taken from the Loader atom in the shop template, which animates two elements at once — the <svg> rotates while the <circle> runs a dash animation. Each ref is attached to the element that owns the corresponding animation:

import { classNamesHelper } from '@archibald/client';
import { useSynchronizedAnimation } from '@archibald/storefront';

import Classes from 'shop/client/components/atoms/loader/Loader.scss';

function Loader({ type = 'base', visible = true }: Readonly<LoaderProps>) {
const refRotate = useSynchronizedAnimation<SVGSVGElement>(Classes.rotate);
const refDash = useSynchronizedAnimation<SVGCircleElement>(Classes.dash);

const classNames = classNamesHelper([Classes.loader, Classes.loaderAnimate, type && Classes[`loader--${type}`]]);

if (!visible) {
return null;
}

return (
<svg className={classNames} ref={refRotate} viewBox="0 0 50 50" role="alert" aria-label="Loading…" aria-busy="true">
<circle className={Classes.path} ref={refDash} cx="25" cy="25" r="20" fill="none" strokeWidth="5" />
</svg>
);
}

With the animations declared in Loader.scss:

> .path {
stroke-dasharray: 100;
animation: dash 1.5s ease-in-out infinite;
}

&.loader--animate {
animation: rotate 2s linear infinite;
}

@keyframes rotate {
100% {
transform: rotate(360deg);
}
}

@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}

100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124px;
}
}
caution

Pass Classes.rotate, not the literal string 'rotate'. CSS Modules scope @keyframes names along with class names, so the name that actually reaches document.getAnimations() is the hashed one exported by the stylesheet. A hard-coded string matches nothing and the hook silently does nothing.