
Card
Card description
A small system for revealing content as it scrolls into view — one element at a time, staggered as a group, or independently across a page's sections.
Three components cover most cases:
MotionReveal — animates a single element in as it scrolls into view.MotionRevealGroup — wraps a set of MotionReveals and triggers them
together as one staggered sequence.MotionRevealIndividual — wraps a set of children and reveals each one
independently, with no shared trigger or stagger.A few conceptual notes before diving in:
animation
has its own default offset/duration/ease in motionRevealDefaults,
shared with the raw Framer variants — pass offset/duration on any
instance to override just that value.MotionRevealGroup, individual MotionReveals automatically defer their
trigger to the group (via context, no prop needed) — which is why
amount/repeat/margin go inert on a grouped MotionReveal.MotionRevealGroup and
MotionRevealIndividual forward straight to MotionReveal's props
instead of reinventing their own — learn MotionReveal once and you know
all three.margin needs scroll room to work. Shrinking the trigger zone only
helps if there's enough page content below the target to actually scroll
it into that zone — an element near the bottom of the page (especially
the last one) may never satisfy a large margin and simply never
reveal.MotionRevealAnimates a single element in — fading, sliding, or scaling — once it scrolls
into view. Built on Framer Motion's variants API.

Card description
import { MotionReveal } from '@/animations';
import { Card } from '@/ui/components/card';
<MotionReveal animation="fade-up">
<Card title="Card 1" description="Card description" media={image} />
</MotionReveal>;Default offset/duration/ease per animation, defined once in
motionRevealDefaults and shared with the raw Framer variants (fadeUp,
zoomIn, etc):
animation | offset | duration | ease |
|---|---|---|---|
fade-up | 24px | 500ms | standard (ease-out) |
fade-down | 24px | 500ms | standard (ease-out) |
fade-left | 24px | 400ms | standard (ease-out) |
fade-right | 24px | 400ms | standard (ease-out) |
fade-in | — | 300ms | standard (ease-out) |
zoom-in | scale 0.95 | 300ms | bounce (overshoot spring) |
ease isn't overridable per instance — swap animation or fall back to
composing your own Framer variants for a different curve.
| Prop | Default | Overridable? | Notes |
|---|---|---|---|
animation | 'fade-up' | — | See the table above for each effect's defaults. |
delay | undefined | — | Ms before this instance's animation starts. |
duration | animation default | yes | Ms. Overrides just the duration, keeping the animation's default offset/scale. |
offset | animation default | yes | Px travelled for fade-* animations. No effect on zoom-in or fade-in. |
amount | 0.5 | — | Fraction of the element visible before it triggers. Ignored inside a MotionRevealGroup — set it on the group instead. |
repeat | false | — | Replay on every viewport re-entry instead of once. Ignored inside a MotionRevealGroup. |
margin | undefined | — | Percent to shrink the bottom of the viewport inward before triggering, e.g. 50 to only trigger once the element reaches the upper half of the screen. Ignored inside a MotionRevealGroup. |
transition | undefined | — | Merged on top of the computed delay/duration/ease, e.g. to swap in a spring. |
MotionRevealGroupMotionReveal cards in
MotionRevealGroup so they trigger together as one staggered sequence
when the group scrolls into view, instead of each card triggering
independently.Cards load with skeletons on first visit — toggle Simulate loading to show or hide the skeleton UI.
import { MotionReveal, MotionRevealGroup } from '@/animations';
import { Card } from '@/ui/components/card';
function CardGrid() {
return (
<MotionRevealGroup className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{cards.map((image, index) => (
<MotionReveal key={`${index}-${animation}`} animation={animation}>
<Card
title={`Card ${index + 1}`}
description="Card description"
media={image}
/>
</MotionReveal>
))}
</MotionRevealGroup>
);
}| Prop | Default | Notes |
|---|---|---|
stagger | 100 | Ms between each child's animation start. |
delayChildren | 0 | Ms before the first child starts. |
amount | 0.2 | Fraction of the group visible before the sequence triggers. |
repeat | false | Replay the whole sequence on every viewport re-entry. |
margin | undefined | Percent to shrink the bottom of the viewport inward before triggering, e.g. 50 to only trigger once the group reaches the upper half of the screen. |
MotionRevealIndividualMotionRevealIndividual so each one gets its own independent trigger,
with no shared trigger or stagger, unlike MotionRevealGroup.Reach for this when wrapping a page's top-level sections, where each one should reveal on its own schedule as it individually scrolls into view.
Intro copy and a primary call to action.
A grid of feature highlights.
Social proof from customers.
Plans and a final call to action.
import { MotionRevealIndividual } from '@/animations';
function Page() {
return (
<MotionRevealIndividual animation="fade-up">
<HeroSection />
<FeaturesSection />
<PricingSection />
</MotionRevealIndividual>
);
}Accepts the same props as MotionReveal (animation, delay, duration,
offset, amount, repeat, margin, transition), applied uniformly to
every child.
Wrap the grid in Suspense and use CardSkeleton as the fallback while data
loads. The demo uses the same markup for Simulate loading and the
Suspense fallback.
import { Suspense } from 'react';
import { placeholderImageList } from '@/assets';
import { CardSkeleton } from '@/ui/components/card-skeleton';
<Suspense
fallback={
<div
className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"
role="status"
aria-busy="true"
aria-live="polite"
>
<span className="sr-only">Loading cards</span>
{placeholderImageList.map((_, index) => (
<CardSkeleton key={index} />
))}
</div>
}
>
<CardGrid />
</Suspense>;Next.js docs: Loading UI and Streaming