How to Build Reusable React Animation Components Without Making Every Effect Config-Heavy
Build reusable React animation components without prop explosion. Learn what belongs in props, presets, composition, shared defaults, and source-level edits.


Vidushi Saxena
Developer
Category: Engineering
A reusable animation component usually begins innocently. There is children, perhaps a delay, maybe a direction. Then another page needs a softer entrance. Marketing wants more blur. Someone asks for a different viewport threshold. The next section should replay instead of running once. Before long, <Reveal> has become a small animation editor expressed as JSX.
That is not necessarily reuse. It is often implementation detail leaking into the component contract.
Reusable React animation components do not need a prop for every timing, offset, easing, threshold, stagger, or visual flourish. When variation appears, it usually belongs in one of four places: intent props, presets or shared policy, composition, or the effect implementation itself.
The useful question is not “How configurable can this component be?”
It is “Where should this variation live?”
Reusable Does Not Mean “Configurable for Every Possible Effect”
React props form an interface between a component and its caller. That makes them useful precisely because they create a boundary. The trouble starts when the boundary stops describing the component’s job and starts reproducing its internal animation timeline one value at a time.
Consider this:
<Reveal
duration={0.72}
delay={0.08}
y={28}
blur={10}
easing={[0.22, 1, 0.36, 1]}
threshold={0.35}
stagger={0.06}
once
direction="up"
>
<Heading />
</Reveal>
Nothing about those props is individually absurd. The architectural problem is that the caller now needs to understand how the reveal is built. Duration, displacement, blur, easing, viewport threshold, stagger and replay behavior have all become part of the public contract.
Once several pages depend on those knobs, changing the choreography is no longer simply an implementation edit. It becomes an API problem.
React itself gives you more than props as a reuse mechanism. Nested JSX can be passed through children, and the React documentation explicitly recommends restraint with indiscriminate prop spreading, noting that frequent spreading can be a signal that components should instead be split or composed through children.
The principle translates cleanly to motion: callers should normally describe intent, not reconstruct the effect from outside.
A component that needs to know whether it should reveal on mount or when it enters the viewport has a meaningful behavioral decision. A caller choosing the fourth Bézier control point usually does not.
That is choreography masquerading as configuration.
Every public knob also becomes something a consumer can depend on. A blur={8} prop looks cheap when it is introduced. Later it may affect reduced-motion behavior, responsive tuning, visual regression tests and the assumptions of several call sites.
The code may still be short. The contract is not.
Decide What Kind of Variation You Actually Have
Before adding the next prop, classify the change.
| Variation | Best home | Why |
|---|---|---|
| A meaningful behavior choice | Intent prop | The caller genuinely decides how the component behaves |
| A recurring combination of motion values | Preset or shared policy | The same motion language appears in several places |
| A different content or DOM structure | Composition | Structural differences should remain structural |
| A one-off creative adjustment | Effect implementation | A local exception does not need to become permanent API |
That distinction prevents configuration from quietly becoming a second animation API.
A useful architecture looks more like this:
Caller
↓
Intent props
↓
Preset / shared motion policy
↓
Engine-specific implementation
↓
Rendered effect
The caller chooses the kind of moment it needs. The implementation decides how to perform it.
Use props for meaningful caller decisions
Healthy props normally read like decisions a developer can understand without studying the timeline:
<Reveal trigger="in-view" preset="quiet">
<PricingCard />
</Reveal>
trigger="in-view" describes when the behavior occurs. preset="quiet" chooses an established motion treatment.
Compare it with this:
<Reveal
y={18}
opacityFrom={0}
duration={0.52}
ease={[0.22, 1, 0.36, 1]}
/>
The second caller is effectively authoring half the animation again.
Use presets or tokens for recurring combinations
A preset earns its place when several animation values change together as one design decision. Perhaps utility content gets a restrained entrance while campaign moments get something more expressive.
const revealPresets = {
quiet: {
distance: 12,
duration: 0.4,
blur: 0,
},
standard: {
distance: 24,
duration: 0.6,
blur: 6,
},
expressive: {
distance: 40,
duration: 0.8,
blur: 10,
},
} as const
Those numbers are not universal recommendations. Their value here is architectural: the relationship between them lives in one place. Six call sites can request quiet without six call sites becoming motion directors.
Use composition when the structure changes
Suppose three landing-page moments use the same reveal language. One wraps a heading, one contains a product mockup, and one contains a grid of cards.
They can share timing without sharing markup.
Trying to encode them as:
<Reveal type="heading" />
<Reveal type="mockup" />
<Reveal type="cards" />
pushes page structure into the animation primitive. It is usually cleaner to let the primitive accept children and keep each content structure independent.
Leave one-off craft inside the effect
Some creative decisions are meant to be specific.
A campaign hero may need a peculiar clipping angle because it aligns with the art direction. A shader may need one unusual noise value because of the asset beneath it. If no other consumer needs to make that choice, exposing maskAngle={17} or noiseStrength={0.63} does not automatically make the component more reusable.
Sometimes the correct abstraction is an opinionated implementation.
A Small Public API Can Still Produce Rich Motion
A compact component interface does not require simplistic motion behind it.
This is an API-shape example, not a complete animation implementation:
import type { ReactNode } from "react"
type RevealProps = {
children: ReactNode
preset?: "quiet" | "standard" | "expressive"
trigger?: "mount" | "in-view"
disabled?: boolean
className?: string
}
export function Reveal({
children,
preset = "standard",
trigger = "in-view",
disabled = false,
className,
}: RevealProps) {
// Engine-specific choreography remains internal.
// preset selects a known treatment.
// trigger selects a known lifecycle.
return <div className={className}>{children}</div>
}
TypeScript is particularly useful at this boundary because it can express choices instead of piles of potentially conflicting booleans. trigger: "mount" | "in-view" is easier to reason about than animateOnMount, animateOnScroll, manual, once, and whatever fifth toggle arrives next Tuesday.
An escape hatch is fine when the audience genuinely needs one, but it should remain visibly exceptional. If raw engine configuration becomes the normal way to use the component, the abstraction has stopped doing much work.
Put Shared Motion Rules Above Individual Components
Some variation should not live on the leaf component at all.
Timing language, easing conventions and reduced-motion policy can often sit above an individual effect. Motion provides two useful mechanisms for this: variants, which define reusable named animation states and can coordinate them through a component tree, and MotionConfig, which can provide configuration to descendant motion components.
"use client"
import {
motion,
MotionConfig,
type Variants,
} from "motion/react"
import type { ReactNode } from "react"
const reveal: Variants = {
hidden: {
opacity: 0,
y: 24,
},
visible: {
opacity: 1,
y: 0,
},
}
export function MotionSystem({
children,
}: {
children: ReactNode
}) {
return (
<MotionConfig
reducedMotion="user"
transition={{
duration: 0.55,
ease: [0.22, 1, 0.36, 1],
}}
>
{children}
</MotionConfig>
)
}
export function Reveal({
children,
}: {
children: ReactNode
}) {
return (
<motion.div
variants={reveal}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.35 }}
>
{children}
</motion.div>
)
}
Now the leaf component does not accept an easing curve and duration every time it appears. Shared configuration establishes a default vocabulary; the variant owns the effect’s meaningful states.
Motion currently documents MotionConfig as a way to provide fallback transitions and reduced-motion policy to child motion components. With reducedMotion="user", it respects the device setting and disables transform and layout animation while preserving other animated values such as opacity.
That does not mean every component should inherit one identical transition. A cursor response, page transition and text entrance may deserve different timing systems.
Shared policy is useful when it is genuinely shared.
With GSAP, Reuse the Lifecycle Before You Reuse Every Number
GSAP has a different architectural shape. Its imperative model is useful when choreography, sequencing and direct timeline control are the work. That still does not mean a reusable React wrapper needs to expose the entire timeline as props.
Often, the reusable layer is lifecycle: scope the effect, create it in the appropriate React phase, and clean up what it creates.
GSAP’s current React guidance provides useGSAP() through @gsap/react. The hook uses gsap.context() underneath, handles cleanup when the component is torn down, and can scope selector text to a container ref. GSAP specifically recommends this React-oriented abstraction rather than requiring every component to manage context cleanup manually.
"use client"
import { useRef } from "react"
import gsap from "gsap"
import { useGSAP } from "@gsap/react"
import type { ReactNode } from "react"
gsap.registerPlugin(useGSAP)
export function RevealGroup({
children,
disabled = false,
}: {
children: ReactNode
disabled?: boolean
}) {
const scope = useRef<HTMLDivElement>(null)
useGSAP(
() => {
if (disabled) return
gsap.fromTo(
"[data-reveal-item]",
{
opacity: 0,
y: 24,
},
{
opacity: 1,
y: 0,
duration: 0.7,
stagger: 0.08,
ease: "power3.out",
}
)
},
{
scope,
dependencies: [disabled],
revertOnUpdate: true,
}
)
return <div ref={scope}>{children}</div>
}
The important reuse decision here is not “every GSAP animation should use 0.7 seconds.”
It is that this effect owns its timeline, its selectors remain scoped to the component, and its lifecycle is cleaned up consistently. GSAP documents gsap.context() as the underlying mechanism that collects animations and allows them to be reverted together; useGSAP() packages that behavior for React.
Another effect can use completely different choreography while following the same lifecycle discipline.
A hook can standardize lifecycle without standardizing visual taste.
Know When to Split the Component Instead of Adding Another Prop
A component usually needs splitting when its “modes” stop sharing the same fundamental implementation.
Different dependency requirements are a strong signal. Different DOM semantics are another. So are materially different lifecycle rules, input methods, rendering models or fallback strategies.
A text entrance and a WebGL hero are both animation, but the category is too broad to justify this:
<Animation type="text" />
<Animation type="webgl" />
The word animation is doing no architectural work there.
A DOM reveal may animate briefly and then stop. A WebGL scene can involve a canvas, textures, shaders, device-pixel-ratio decisions, resize handling and an ongoing render loop. Making them two modes of one universal abstraction hides the fact that they have different jobs and different costs.
Vault’s current dependency documentation reflects that distinction at the product level. Dependencies are scoped to individual effects: some patterns may use React/CSS, others Motion or GSAP, and heavier rendering work may involve Three.js, React Three Fiber or other WebGL tooling. It explicitly cautions that not every effect in a category uses every listed engine.
A practical stopping rule: if the next option changes the rendering model, dependency family, semantic structure, lifecycle or fallback strategy, check whether you are actually looking at another component.
Reuse Still Has to Survive Production
A clean animation API can still ship a bad interaction.
Reusable architecture should centralize production behavior where every instance depends on it, but abstraction should not erase meaningful differences between effects. A short entrance transition, a scroll-linked sequence and a continuously rendered canvas scene do not have the same performance profile.
Reduced motion is a good example. If every instance should respect the same preference, do not rely on every caller remembering another prop.
Motion exposes MotionConfig for broader policy and useReducedMotion() when a component needs a bespoke response. Its current guidance includes replacing potentially uncomfortable translation with alternatives such as opacity or disabling motion-heavy behavior altogether.
But reduced motion is not the entire accessibility discussion. The reduced or static path should preserve content order, semantics and important interaction state. Keyboard users should not lose access because an effect owns focus badly. Pointer-driven treatments need a touch strategy. Hover-only animation cannot be the only way an important state is communicated.
Sometimes the right fallback is simply static UI.
Next.js introduces another implementation boundary. Components that rely on state, effects, event handlers or browser APIs belong behind an appropriate client boundary; current Next.js documentation defines 'use client' as an entry point into the client module graph rather than something that must be repeated in every descendant file.
Motion also currently exposes motion/react-client for motion components used from React Server Component contexts.
And none of this proves runtime performance by itself.
A component with three props is not automatically faster than one with twelve. Cost comes from what the implementation actually does: layout work, DOM volume, observers, listeners, scroll calculations, continuously running animation loops, canvas resolution, shaders, textures, image assets and code that continues working after it is no longer visible.
Profile the real page, not the abstraction in isolation. Test representative hardware, inspect long main-thread tasks and layout/paint work, and verify that persistent loops, observers or scroll work stop when they are unnecessary.
API cleanliness is a maintainability win.
It is not an FPS benchmark wearing nice TypeScript.
Source-First Components Give You One More Escape Hatch
There is another way to prevent config explosion when the component arrives as editable source: keep a genuinely local customization local.
Packaged abstractions often have pressure to convert customization into public API because consumers cannot safely reach the implementation. Editable source gives teams another layer to work at.
Vault uses that source-first model. Its current installation documentation says selected effect files are added directly to the project so developers can inspect and tune the component, supporting hooks, styles, animation setup and other effect-specific files. Its dependency documentation likewise keeps requirements visible and scoped to the effect being installed rather than imposing one universal animation stack.
That changes the abstraction trade-off.
Imagine bringing in a reveal with coherent choreography, lifecycle and fallback behavior. Its public interface can stay focused on decisions that genuinely recur. If one campaign needs a stranger clipping angle or timing relationship, the project can change that implementation locally instead of promoting the exception into a permanent prop every future caller now inherits.
The useful distinction is local ownership:
Preview
↓
Install / copy
↓
Tune the source
↓
Ship in context
React, Motion, GSAP and the browser remain the underlying technologies. Vault does not replace those tools. It supplies composed interaction patterns as editable implementation, giving teams another place for project-specific changes to live.
Once the architecture is clear, Browse Vault Effects and apply the same test to anything you bring into a project: keep recurring decisions in the API, shared language in presets or policy, structural differences in composition, and truly local craft in the implementation.
The Reusable React Animation Component Checklist
Before adding the next prop, audit the boundary rather than staring at the animation code.
| Question | Healthy signal | Warning sign |
|---|---|---|
| Can I explain this prop as a caller decision? | trigger="in-view" | springDamping={17} with one known consumer |
| Do several values always change together? | Give the combination a preset | Repeat the same five values at every call site |
| Is the difference mainly structural? | Compose different children or components | Add booleans for every layout |
| Do two modes use different engines or lifecycles? | Split the abstraction | Build a universal Animation component |
| Is reduced-motion behavior shared policy? | Handle it centrally or inside the effect | Make every caller remember a fallback object |
| Is the variation unique to one creative execution? | Tune the implementation | Promote the exception into permanent API |
| Does the component clean up the work it starts? | Scope timelines, observers and listeners | Make cleanup somebody else’s problem |
| Does the static path remain useful? | Preserve content, semantics and state | Let the effect carry meaning the UI cannot survive without |
The goal is not the fewest props possible. It is the smallest API that tells the truth about the component.
A reusable React animation component should make recurring interaction decisions easier without flattening every piece of motion into one generic primitive. Let props describe intent. Let presets carry motion language. Let composition handle structure. Let engine-specific code own its lifecycle. Let local implementation choices remain local when they have no reason to become public API.
Motion earns its place when it clarifies hierarchy, feedback, pacing or story.
So should every abstraction around it.




