React-first refactor of the WordPress AniLibrary wrapper plugin.
- npm: @jsilff/anilibrary-react
- GitHub: jsilff/AniLibrary-React
- Releases: v0.1.0 and later
npm install @jsilff/anilibrary-reactRequires React 18.2+ as a peer dependency (react and react-dom).
import { AnimationWrapper } from "@jsilff/anilibrary-react";
import "@jsilff/anilibrary-react/styles.css";
export function Example() {
return (
<AnimationWrapper preset="fade" trigger="scroll" direction="up">
<h2>Animated content</h2>
<p>The wrapper controls when and how this content animates.</p>
</AnimationWrapper>
);
}Classes are still useful, but they should be an implementation detail. The wrapper component gives React apps a declarative API, handles refs, effect cleanup, reduced-motion checks, scroll observers, and nested wrapper timing.
AnimationWrapper accepts these animation props:
preset:fade,slide,zoom,blur-in,rotate-in,flip,text-rise,word-cascade,letter-pop,pulse-soft,float-soft, orbounce-softtrigger:scroll,load,hover,click,loop, orinheritcontentKind:text,media,layout, ormixeddirection:up,down,left,right,clockwise,counterclockwise,vertical, orhorizontalzoomMode:inoroutduration,delay,stagger,intensity,easing,threshold,rootMarginonce,loop,clickToggle,hideUntilHovertextGranularity:word,character, orlineinheritParentDelay,followParentAnimationoptionsAt: responsive overrides keyed by CSS media query (see below)
The package also exports:
normalizeAnimationOptionsapplyOptionsAtshouldPrimeOnMountanimationStartsHiddengetAnimationClassNamegetAnimationDataAttributessetupAnimationWrapperinitAnimationWrappers
Use optionsAt to override animation props at specific breakpoints. Entries are evaluated in order; the first matching query wins. Any prop except optionsAt itself can be overridden (direction, stagger, rootMargin, etc.).
<AnimationWrapper
preset="slide"
trigger="scroll"
direction="right"
once
optionsAt={[
{ query: "(max-width: 767px)", direction: "up" },
]}
>
<h2>Slides in from the right on desktop, up on mobile</h2>
</AnimationWrapper>On breakpoint change, the runtime re-resolves options and re-primes hidden state before play. Animations that already ran with once are not restarted.
Note:
prefers-reduced-motion: reduceis handled globally by the library — animations are disabled automatically and content is shown immediately. You do not need (and should not add) a reduced-motion entry inoptionsAt; doing so implies it is opt-in when it is already built in.
Initial hidden state (no flash on load)
Scroll- and load-triggered presets that start hidden (fade, slide, zoom, etc.) use a two-step priming system so content does not flash visible before the animation runtime attaches:
abw-pending(CSS) — applied at render time so the wrapper is hidden on first paint and during SSR hydration.primeInitialState()(runtime) — runs synchronously inuseLayoutEffectbefore browser paint, applying the exact keyframe-from values (opacity,transform,filter) to animation targets.
Once the animation runs or completes, pending/hidden styles are cleared. Presets that start fully visible (e.g. pulse-soft, float-soft) skip priming.
Import @jsilff/anilibrary-react/styles.css in your app — the .abw-pending rule lives there.
For lists, grids, or timelines, prefer one parent wrapper with contentKind="layout" and stagger instead of wrapping each item in its own scroll-triggered AnimationWrapper. That uses a single IntersectionObserver and avoids mobile jitter from many observers re-firing.
<AnimationWrapper
preset="slide"
trigger="scroll"
direction="up"
contentKind="layout"
stagger={80}
once
rootMargin="0px 0px -5% 0px"
className="grid gap-6"
>
{items.map((item) => (
<article key={item.id}>{item.title}</article>
))}
</AnimationWrapper>Direct children are stagger targets by default. Mark specific children with className="abw-stagger-item" or data-ffaw-stagger-item="1" when you need finer control.
For lazy-mounted sections (e.g. below-the-fold content loaded after hydration), use trigger="load" on the parent instead of scroll.
Nested child wrappers that should animate with the parent stagger (not their own observer) should use trigger="inherit" with followParentAnimation:
<AnimationWrapper preset="fade" trigger="scroll" contentKind="layout" stagger={80} once>
{items.map((item) => (
<AnimationWrapper
key={item.id}
as="article"
preset="fade"
trigger="inherit"
followParentAnimation
>
{item.title}
</AnimationWrapper>
))}
</AnimationWrapper>When using contentKind="text" with presets like letter-pop or word-cascade, put the heading or paragraph inside the wrapper (recommended), or set as="h1" / as="p" so the wrapper is the text element itself — both patterns are supported as of v0.1.1.
// Recommended
<AnimationWrapper preset="letter-pop" trigger="load" contentKind="text" textGranularity="character">
<h1>Animated title</h1>
</AnimationWrapper>
// Also supported (v0.1.1+)
<AnimationWrapper as="h1" preset="letter-pop" trigger="load" contentKind="text" textGranularity="character">
Animated title
</AnimationWrapper>The React API deliberately preserves the old runtime contract:
import {
getAnimationClassName,
getAnimationDataAttributes,
} from "@jsilff/anilibrary-react";
const props = {
className: getAnimationClassName({ preset: "fade" }),
...getAnimationDataAttributes({ preset: "fade" }),
};That makes it possible to migrate the WordPress editor later without forcing a rewrite of saved markup.
git clone https://github.com/jsilff/AniLibrary-React.git
cd AniLibrary-React
npm install # runs prepare → build automatically
npm run typecheck
npm run buildThe compiled output lives in dist/ (gitignored). prepare runs on npm install, npm pack, and publish so dist/ is always built before the package is consumed.
# From this repo
npm run build
npm link
# From your app
npm link @jsilff/anilibrary-reactOr add a file dependency:
{
"dependencies": {
"@jsilff/anilibrary-react": "file:../path/to/anilibrary-react"
}
}npm install in the app will trigger prepare and build dist/ automatically.