v0.3.0 on npm

Scroll reveal
for Astro.

Zero JS by default. Native scroll-driven animations — no runtime, no bundle, just CSS.

$ npm install astro-reveal

Scroll to see it live — and back up to reverse

Why astro-reveal

The animation is the pitch.
Everything else stays out of the way.

scroll-driven

Zero runtime JS

Purist mode uses native CSS scroll-driven animations. No IntersectionObserver, no bundle, no hydration. The browser does all the work.

any framework

UI-agnostic

Drop <Reveal> around any element, or use data-reveal on raw HTML. Works with React, Vue, Svelte, Solid, or no framework at all.

a11y built-in

Accessible by design

prefers-reduced-motion is respected at the CSS level — users who opt out of motion see static content with no layout shift.

Stagger

Cascade with index.

Pass an index prop (0, 1, 2…) and each element enters with a staggered delay. Perfect for grids and lists.

First index=0
Second index=1
Third index=2
Fourth index=3
Fifth index=4
Sixth index=5

How it's written

import Reveal from "astro-reveal/Reveal.astro";

{items.map((item, i) => (
  <Reveal animation="up" index={i}>
    <Card {...item} />
  </Reveal>
))}

Quickstart

Up in three steps.

01

Install & configure

Add astro-reveal to your project and register the integration.

Terminal

npm install astro-reveal

astro.config.ts

import { defineConfig } from "astro/config";
import reveal from "astro-reveal";

export default defineConfig({
  integrations: [reveal()], // default: purista (zero JS)
});
02

Use the component

Wrap any content in <Reveal>. The animation prop controls direction.

MyPage.astro

---
import Reveal from "astro-reveal/Reveal.astro";
---

<Reveal animation="up">
  <h2>This enters from below.</h2>
</Reveal>

<Reveal animation="left" index={1}>
  <p>This slides in from the right, staggered.</p>
</Reveal>

{/* distancePreset: "small" | "medium" | "large" — all modes */}
<Reveal animation="up" distancePreset="large">
  <div>Enters with a generous sweep.</div>
</Reveal>

{/* easing / speed props are observer/auto only */}
<Reveal animation="up" easing="bounce" speed="slow">
  <div>Bounces in slowly — observer mode.</div>
</Reveal>
03

Or use the raw attribute

No import needed. Works on any element, any framework's output.

Any HTML

<!-- On a plain HTML element -->
<div data-reveal="fade">
  <img src="photo.jpg" alt="..." />
</div>

<!-- Inside a React / Vue / Svelte component output -->
<article data-reveal="scale">
  <slot />
</article>

AI prompt

Works with your AI, too.

Paste this into Cursor, Claude, ChatGPT, or any coding assistant. It covers the full API, the gotchas, and the best practices — so your AI doesn't have to guess.

astro-reveal integration prompt
Integrate the astro-reveal library into this Astro project.

What it is: scroll reveal animations for Astro. Two engines, one API. The default
("scroll") uses native CSS scroll-driven animations and ships NO JavaScript to the
client. The "observer" mode uses IntersectionObserver (~0.6KB), plays once and stays;
"auto" uses native where supported and falls back to observer where it isn't.

Steps:
1. Install the package: `npm install astro-reveal`
2. Add the integration in astro.config (mode "scroll" is the default):
     import reveal from "astro-reveal";
     export default defineConfig({ integrations: [reveal()] });
   No need to import any CSS manually; the integration injects it for you.
3. Animate elements in either of two equivalent ways:
   - Component: import Reveal from "astro-reveal/Reveal.astro";
       <Reveal animation="up">…</Reveal>
       Props: animation ("up"|"down"|"left"|"right"|"fade"|"scale"|"blur"),
       distance, duration, index (for stagger), as (tag to render),
       distancePreset ("small"|"medium"|"large"),
       easing ("smooth"|"bounce"|"elastic"|"sharp"|"soft") — observer/auto only,
       speed ("instant"|"fast"|"normal"|"slow") — observer/auto only.
   - Raw attribute (works on any HTML, whether it comes from React/Vue/Svelte or plain):
       <div data-reveal="fade">…</div>
       Attribute equivalents: data-distance-preset, data-easing, data-speed.

Rules and best practices:
- Animate sections/blocks (hero, cards, features), NOT every paragraph of dense text.
- The animation name is the direction the element TRAVELS as it appears:
  "up" starts below and rises into place.
- For staggering a list, pass an incrementing index:
  items.map((x, i) => <Reveal index={i}>…</Reveal>).
- Fine-tune with CSS custom properties: --reveal-distance, --reveal-scale,
  --reveal-blur, --reveal-duration, --reveal-easing, --reveal-index, --reveal-stagger.
- CSS vars can be set on any ancestor and [data-reveal] children will inherit them.
  Precedence order: inline style > preset attribute > inherited > default.
  Example: <section style="--reveal-distance: 3rem"> — all reveals inside inherit it.
- easing and speed presets only work in "observer" or "auto" mode. They have no
  effect in the default "scroll" (purist) mode.
- The "scroll" mode is scrubbed: the animation reverses as you scroll back up. If you
  want "appear once and stay", use reveal({ mode: "observer" }).
- Accessibility (prefers-reduced-motion) is already handled by the library.

Do not re-implement animations by hand: use the package's API.

Modes

Purist or Observer.
You pick once, per build.

The mode is a global config — one engine per site. You cannot mix them in the same build. Choose based on your tradeoffs.

Purist (default)

← you're here mode: "scroll"
  • Zero runtime JS — pure CSS
  • Scrubs with scroll position (reverses on scroll-up)
  • No hydration, no bundle impact
  • Requires browser support for animation-timeline
  • No "play once and stay" behaviour
// astro.config.ts
import reveal from "astro-reveal";

integrations: [reveal()]
// or explicitly:
integrations: [reveal({ mode: "scroll" })]

Observer

mode: "observer"
  • ~0.6 KB gzipped runtime
  • Plays once and stays — great for dashboards
  • Full browser support via IntersectionObserver
  • Easing & speed presets (easing, speed props)
  • Ships a tiny JS bundle
  • Does not scrub with scroll
// astro.config.ts
import reveal from "astro-reveal";

integrations: [reveal({ mode: "observer" })]
Try it live →

Browser support — purist mode

Scroll-driven animations (animation-timeline: scroll()) are supported in Chrome 115+, Edge 115+, and Firefox 110+ (with flag). Safari 18+ added support. Check caniuse.com/css-animation-timeline for current data. If you need broader support today, use observer mode.

Customization

Tune with CSS vars.

CSS vars, presets, and ancestor theming — all without JS. Precedence: inline > preset > inherited > default.

Variable Description
--reveal-distance Travel distance for directional animations (up/down/left/right)
--reveal-scale Starting scale for the scale animation
--reveal-blur Starting blur amount for the blur animation
--reveal-duration Animation duration (observer mode only)
--reveal-easing Easing function
--reveal-index Stagger position — set via the index prop
--reveal-stagger Stagger delay increment per index step

Presets

Shorthand props for common values — less typing, same result.

distancePreset — all modes

<!-- attribute syntax -->
<div data-reveal="up" data-distance-preset="large">…</div>

<!-- component syntax -->
<Reveal animation="up" distancePreset="large">…</Reveal>

<!-- values: "small" | "medium" | "large" -->
obs

observer / auto mode only on live sites

easing — "smooth" | "bounce" | "elastic" | "sharp" | "soft"

<div data-reveal="up" data-easing="bounce">…</div>
<Reveal animation="up" easing="bounce">…</Reveal>

speed — "instant" | "fast" | "normal" | "slow"

<div data-reveal="up" data-speed="slow">…</div>
<Reveal animation="up" speed="slow">…</Reveal>

Theming & inheritance

Set --reveal-* on any ancestor — all [data-reveal] children inherit them. Inline or preset values on the element itself always win.

<section style="--reveal-distance: 3rem; --reveal-stagger: 120ms">
  <!-- every [data-reveal] inside inherits these -->
  <div data-reveal="up">Card A</div>
  <div data-reveal="up">Card B</div>
  <div data-reveal="up" style="--reveal-distance: 1rem">
    <!-- inline wins: this one travels only 1rem -->
    Card C
  </div>
</section>

Global override

:root {
  --reveal-distance: 3rem;   /* bigger travel */
  --reveal-easing: ease-out; /* simpler curve */
  --reveal-stagger: 100ms;   /* slower cascade */
}

Inline override

<div data-reveal="up" style="--reveal-distance: 4rem; --reveal-stagger: 120ms">
  <!-- travels further, stagger is slower -->
</div>