@awesome-reveal/astro
v1.0.0-next.11
Published
Reveal-on-scroll animations for Astro, built on the Web Animations API and IntersectionObserver.
Maintainers
Readme
@awesome-reveal/astro
Reveal-on-scroll animations for Astro. Wrap content in <Reveal>, and it fades and slides in when it enters the viewport. No framework, no hydration directive and no stylesheet import.
Preview: This is a prerelease of version 1.0. The API can change before the stable release. Tell us what works and what does not in GitHub Discussions.
- Simple. Two components and a few options.
- Native. A small custom element, built on IntersectionObserver and the Web Animations API.
- No flash. Server-rendered content stays hidden until it reveals.
- Astro-ready. Works with the
<ClientRouter />, server islands andtransition:persist. - Accessible. Obeys the reduced-motion preference, and shows waiting content on focus and in print.
Works with Astro 6 and 7.
Install
npm install @awesome-reveal/astroThe package loads its stylesheet and a small client script automatically.
Quick start
---
import { Reveal } from "@awesome-reveal/astro";
---
<Reveal><h1>Hello</h1></Reveal>The content fades in and moves up one time, when it enters the viewport.
Usage
Choose the entrance
from sets where the element starts. A side gives a fade and a 24 px movement. The default is "bottom".
<Reveal from="left">From the left</Reveal>For a different entrance, give a start state. The element animates from this state to its resting style. A start state replaces the default, so add opacity: 0 if you want a fade.
<Reveal from={{ opacity: 0 }}>Fade only</Reveal>
<Reveal from={{ scale: 0.9 }}>Scale only</Reveal>
<Reveal from={{ opacity: 0, x: "-100%", rotate: "-5deg", blur: 8 }}>All together</Reveal>A start state has the properties opacity, x, y, scale, rotate and blur. A number for x, y or blur is in pixels, and a number for rotate is in degrees. A string can contain a CSS value with its unit.
Use your own keyframes
For full control, such as a custom easing, give native keyframes instead of from. End them at the resting style of the element.
<Reveal
keyframes={[
{ opacity: 0, transform: "scale(.8)", easing: "cubic-bezier(.34,1.56,.64,1)" },
{ opacity: 1, transform: "scale(1)" },
]}
>
Bounce in
</Reveal>Change the timing
<Reveal as="h2" from="left" duration={800}>Title</Reveal>
<Reveal from={{ opacity: 0 }} delay={200}>Starts after the title</Reveal>
<Reveal threshold={0.5}>Reveals when half of it is visible</Reveal>Keep delay below about 300 ms. A long delay makes content appear late when the user scrolls quickly.
Reveal a list
Cascade reveals its direct children one after the other, when it enters the viewport. interval sets the time between two children, and the default is 80 ms.
---
import { Cascade } from "@awesome-reveal/astro";
---
<Cascade as="ul" from="left" interval={120}>
<li>Fast</li>
<li>Small</li>
</Cascade>For a long list, or a grid that is taller than the viewport, wrap each item in Reveal instead. A cascade schedules all children at the same time, so the last children start late or out of view.
Control the reveal
when controls the entrance. true plays it, and false resets it with no exit animation. Without when, the viewport controls it. After the page loads, set the when property of the awesome-reveal host element.
when does not show or hide content. With when={false}, the element is transparent, but screen readers read it and the keyboard can focus it. inert does not hide content: it only blocks focus, clicks and assistive technology. To hide content, also set hidden on the rendered tag:
---
import { Reveal } from "@awesome-reveal/astro";
---
<button id="toggle" aria-expanded="false">Toggle</button>
<Reveal id="details" when={false} hidden from={{ opacity: 0 }}>Details</Reveal>
<script>
import type {} from "@awesome-reveal/astro";
const button = document.querySelector("#toggle")!;
const details = document.querySelector("#details")!;
const host = details.closest("awesome-reveal")!;
button.addEventListener("click", () => {
const open = !host.when;
host.when = open;
details.toggleAttribute("hidden", !open);
button.setAttribute("aria-expanded", String(open));
});
</script>The type import adds the awesome-reveal element to the DOM types. To play the entrance again, set when to false, then to true.
Run code at the reveal
The rendered tag dispatches a reveal event that bubbles, when the reveal starts. The host has a revealed property. To wait for the end of the entrance, wait for the finished promises of the animations of the event target:
document.addEventListener("reveal", (event) => {
const element = event.target as Element;
Promise.all(element.getAnimations({ subtree: true }).map((animation) => animation.finished))
.then(() => element.classList.add("entered"))
.catch(() => {});
});The promise rejects when the entrance stops early, for example after when becomes false. Catch the rejection. It resolves at once when no entrance plays, for example with reduced motion. It also waits for your own animations of the element and its descendants.
API
<Reveal> and <Cascade>
| Prop | Type | Default | Description |
| --------------------------- | -------------------- | ---------- | --------------------------------------------------------------- |
| as | RevealTag | "div" | The rendered tag |
| from | Side \| StartState | "bottom" | The side or the start state of the entrance |
| keyframes | Keyframe[] | — | Native keyframes. You cannot use them together with from |
| duration | number | 600 | The duration of the entrance, in milliseconds |
| delay | number | 0 | The time between the reveal and the entrance, in milliseconds |
| threshold | number | 0 | The visible fraction, from 0 to 1, that reveals the element |
| when | boolean \| null | — | true plays the entrance, false resets it |
| interval (Cascade only) | number | 80 | The time between the entrances of two children, in milliseconds |
The components pass the other attributes and the children to the rendered tag.
The awesome-reveal host element
Each component renders its tag inside an awesome-reveal host element with display: contents.
| Member | Description |
| -------------- | ------------------------------------------------------------------------- |
| when | Gets or sets the manual control. null gives the control to the viewport |
| revealed | Tells if the element is revealed |
| reveal event | Comes from the rendered tag and bubbles, one time for each reveal |
Behavior
- The entrance plays one time. The observation stops at the reveal.
- The
revealevent also fires when no entrance plays, for example with reduced motion or focus. Afterwhenbecomesfalse, the next reveal fires it again. - A change to
whenorthresholdapplies immediately. A change to the other options applies at the next reveal. when={false}during thedelayresets the entrance, and the entrance does not play.
Where you can put a reveal
The HTML parser moves some tags out of the host element. Obey these rules:
asaccepts an HTML tag orsvg. TypeScript rejects document tags (html,head,body), table parts (caption,col,colgroup,thead,tbody,tfoot,tr,td,th) and SVG-only tags.Do not put
RevealorCascadedirectly in a<table>, a table section, a row or a<select>. Reveal the whole table, or reveal content inside a table cell:<Reveal as="table"> <tbody> <tr><td>The whole table reveals together.</td></tr> </tbody> </Reveal>In a
<p>, also inline in MDX text, use an inline tag, such asas="span".
If the parser moves the tag, the host reports an error in the browser console, and the tag shows without an entrance after a few seconds.
Accessibility
- With
prefers-reduced-motion: reduce, all the content shows immediately, with no animation. - Focus in a waiting element shows it at once, so a keyboard user does not focus invisible content. This does not apply with
when={false}. - Focus during a running entrance or its delay does not shorten it, so keep
delayandintervalshort. - Waiting content shows in print and when JavaScript is off.
- Hidden content stays in the accessibility tree. Do not make important content depend on decorative motion.
Good to know
- The cascade reads its children when it reveals. A child that you add after the reveal shows with no animation.
- Only the direct element children animate. Text and children with
display: contentsshow immediately. - A framework island and the host of a nested
Revealhavedisplay: contents, so the cascade does not animate them. A nestedRevealplays only its own entrance. To put an island in the cascade, wrap it in an element. delaymoves the start of the whole sequence. Therevealevent comes from the rendered tag.
- Reveals work after a
<ClientRouter />navigation, in server islands and withtransition:persist. - A prerendered page does not animate until the browser activates it.
- The server markup has
data-awesome-reveal="hidden", ordata-awesome-reveal="cascade"for a cascade. - The stylesheet hides content for at most 5 seconds before the client script attaches the element. If the script fails, the content shows. If the attach happens later, for example on a slow network, the content shows, then hides again until it reveals.
- The stylesheet hides with the
awesome-reveal-holdCSS animation, which replaces your ownanimationon a waiting element until it reveals. Put your own animation on an inner element. The hold firesanimationstartandanimationendevents, and shows ingetAnimations(). - An inline
opacitywith!important, or your ownanimationwith!important, can show the element before the reveal. - The stylesheet does not reach a shadow root.
- In browsers without the
scriptingmedia feature, such as Safari 16, content shows after 5 seconds when JavaScript is off.
The package writes no style attribute, no inline script and no CSS at runtime. But Astro can put the package stylesheet and your scripts inline in the HTML. Use one of these setups:
- Set
security.csp: true. Astro adds a<meta>policy with hashes for the inline styles and scripts that it emits. Astro adds the policy only to the build, so useastro buildandastro previewto check it. Astro does not support this option with the<ClientRouter />. - Send your own policy, such as
style-src 'self'; script-src 'self', and setbuild.inlineStylesheets: 'never'. With the default'auto', Astro puts stylesheets smaller thanvite.build.assetsInlineLimit(4 kB by default) in a<style>element. If the policy blocks the package stylesheet, the content does not hide before the reveal, and theawesome-revealhost is notdisplay: contents. Also make sure that the built HTML has no inline<script>that your policy blocks.
To reveal content inside a React or Vue island, use @awesome-reveal/react or @awesome-reveal/vue in the island, and import its reveal.css in the page. Hydrate the island eagerly, for example with client:load.
The @awesome-reveal/core README describes how the entrance works, the thresholds of tall and clipped content, performance and errors. Read it on npm.
Agent skills
The package includes the adding-reveal-animations skill, which teaches a coding agent this API. pnpm can link it into your project after you approve it.
