Scroll Reveal
ScrollEntrance on scroll that plays once and stays, staggers its children, scrubs with the scroll position when asked, honours reduced motion, and keeps working after every ClientRouter navigation.
Animates a named region between pages: rise, zoom, wipe, scope, fade, slide, or the browser's own shared-element morph. Zero JavaScript of its own: Astro's <ClientRouter /> drives view transitions, and the presets are keyframes on the active motion preset's curve.
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
JavaScript of its own
0 B
CSS 7.9 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 111 · Firefox 144 · Safari 18; needs view-transitions. Elsewhere: an instant swap between pages.
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
import PageTransition from '@moonarc/core/PageTransition';
// both pages, same name
---
<PageTransition name="hero" preset="rise">
<h1>Page one</h1>
</PageTransition>
<PageTransition name="cover" preset="morph">
<img src="/cover.jpg" alt="" />
</PageTransition>
<PageTransition name="main" preset="scope">
<main>…</main>
</PageTransition>Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/page-transition.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.
---
/**
* PageTransition — the page-level motion Astro's <ClientRouter /> makes
* possible and nothing else on the web can do this cheaply: wrap a region,
* give it a name, and it animates between pages. Zero JS of its own; the
* router is Astro's. Presets are keyframes in this file, resolved through
* the active preset's curve and duration, so page motion matches the rest.
*/
import type { HTMLAttributes, HTMLTag } from 'astro/types';
import type { TransitionAnimationValue, TransitionDirectionalAnimations } from 'astro';
import { fade, slide } from 'astro:transitions';
type Preset = 'fade' | 'slide' | 'rise' | 'zoom' | 'wipe' | 'scope' | 'morph' | 'none';
interface Props extends HTMLAttributes<'div'> {
/** transition:name — the same name on both pages pairs the elements. */
name?: string;
/** How the region enters and leaves. `morph` is the shared-element morph the browser draws itself. */
preset?: Preset;
/** Element to render. */
as?: HTMLTag;
}
const { name, preset = 'rise', as: Tag = 'div', class: className, ...rest } = Astro.props;
const own = (kind: 'rise' | 'zoom' | 'wipe' | 'scope'): TransitionDirectionalAnimations => {
// scope: the old page stays in view, sinking and dimming for the whole entrance, so its exit runs on the entrance duration
const out = { name: `ma-vt-${kind}-out`, duration: kind === 'scope' ? 'var(--ma-duration)' : 'var(--ma-duration-fast)', easing: 'var(--ma-ease-out)', fillMode: 'both' as const };
const inn = { name: `ma-vt-${kind}-in`, duration: 'var(--ma-duration)', easing: 'var(--ma-ease)', fillMode: 'both' as const };
return { forwards: { old: out, new: inn }, backwards: { old: { ...out, name: `ma-vt-${kind}-in`, direction: 'reverse' as const }, new: { ...inn, name: `ma-vt-${kind}-out`, direction: 'reverse' as const } } };
};
const animate: TransitionAnimationValue =
preset === 'fade' ? fade({ duration: 'var(--ma-duration)' }) : preset === 'slide' ? slide({ duration: 'var(--ma-duration)' }) : preset === 'morph' ? 'initial' : preset === 'none' ? 'none' : own(preset);
---
{
name ? (
<Tag class:list={['ma-page', className]} transition:name={name} transition:animate={animate} {...rest}>
<slot />
</Tag>
) : (
<Tag class:list={['ma-page', className]} transition:animate={animate} {...rest}>
<slot />
</Tag>
)
}
<style is:global>
@layer components {
:where(.ma-page) {
display: block;
}
@media (prefers-reduced-motion: reduce) {
/* Astro's router already drops view-transition animations under reduced motion. */
:where(.ma-page) {
animation: none;
}
}
}
@keyframes ma-vt-rise-in {
from {
opacity: 0;
translate: 0 var(--ma-travel-section, 48px);
}
}
@keyframes ma-vt-rise-out {
to {
opacity: 0;
translate: 0 calc(var(--ma-travel-section, 48px) * -0.5);
}
}
@keyframes ma-vt-zoom-in {
from {
opacity: 0;
scale: 0.96;
}
}
@keyframes ma-vt-zoom-out {
to {
opacity: 0;
scale: 1.03;
}
}
@keyframes ma-vt-wipe-in {
from {
clip-path: inset(0 100% 0 0);
}
to {
clip-path: inset(0 0 0 0);
}
}
@keyframes ma-vt-wipe-out {
from {
clip-path: inset(0 0 0 0);
}
to {
clip-path: inset(0 0 0 100%);
}
}
/* scope: the old page sinks and dims behind, the new one rises as a sheet in front */
@keyframes ma-vt-scope-in {
from {
translate: 0 100%;
}
to {
translate: 0 0;
}
}
@keyframes ma-vt-scope-out {
from {
scale: 1;
opacity: 1;
}
to {
scale: 0.92;
opacity: 0.35;
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
name | string | none | transition:name. The same name on two pages pairs the regions; omit it to animate a region on its own. |
preset | 'rise' | 'zoom' | 'wipe' | 'scope' | 'fade' | 'slide' | 'morph' | 'none' | 'rise' | Entrance and exit. scope sinks and dims the old page behind while the new one rises in front, like a sheet; morph lets the browser interpolate size and position between the two pages' elements. |
as | HTMLTag | 'div' | Element to render. |
Astro's router skips view-transition animations; the swap is instant.
ClientRouterThis is the ClientRouter. Without it the wrapper is inert and pages load normally.
Replaces: framer-motion AnimatePresence page transitions · Barba.js · Swup. See the migration table.
Entrance on scroll that plays once and stays, staggers its children, scrubs with the scroll position when asked, honours reduced motion, and keeps working after every ClientRouter navigation.
Splits a string into words, characters or lines on the server and reveals them with a capped stagger when they scroll into view.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown