@awesome-reveal/core
v1.0.0-next.11
Published
Framework-independent core of awesome-reveal: reveal-on-scroll with IntersectionObserver and the Web Animations API.
Maintainers
Readme
@awesome-reveal/core
Reveal-on-scroll animations in plain JavaScript. Give a selector, and the elements fade and slide in when they enter the viewport.
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.
- Small. Less than 2 kB gzipped, with no dependencies and no stylesheet.
- Native. Built on IntersectionObserver and the Web Animations API.
- Accessible. Obeys the reduced-motion preference, and shows waiting content on focus and in print.
- CSP-friendly. Writes no inline styles, so it works with a strict Content Security Policy.
Do you use React, Vue or Astro? Use @awesome-reveal/react, @awesome-reveal/vue or @awesome-reveal/astro. They also hide server-rendered content until it reveals.
Install
npm install @awesome-reveal/coreQuick start
import { cascade, reveal } from "@awesome-reveal/core";
reveal(".card"); // Fade in and move up
reveal(".hero", { from: "left" }); // Fade in from the left
cascade(".features"); // Reveal the children of .features one after the otherEach element animates 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:
| from | Start offset |
| -------------------- | ------------ |
| "top" | y: -24 px |
| "right" | x: 24 px |
| "bottom" (default) | y: 24 px |
| "left" | x: -24 px |
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.
import { reveal } from "@awesome-reveal/core";
reveal(".caption", { from: { opacity: 0 } }); // Fade only
reveal(".logo", { from: { scale: 0.9 } }); // Scale only
reveal(".card", { from: { opacity: 0, x: "-100%", rotate: "-5deg", blur: "0.5rem" } });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:
import { reveal } from "@awesome-reveal/core";
reveal(".logo", {
keyframes: [
{ opacity: 0, transform: "scale(.8)", easing: "cubic-bezier(.34,1.56,.64,1)" },
{ opacity: 1, transform: "scale(1)" },
],
});- End the keyframes at the resting style of the element. The animation does not keep its last keyframe after it ends.
- Give explicit values to the first keyframe. An open WebKit bug (212409) can take an implicit start value from an earlier animation.
- To end at the element's own value, end with a neutral additive keyframe, for example
{ opacity: 0, composite: "add" }.
Change the timing
import { reveal } from "@awesome-reveal/core";
reveal(".hero h1", { from: "left", duration: 800 });
reveal(".hero p", { from: { opacity: 0 }, delay: 200 }); // Starts after the heading
reveal(".chart", { threshold: 0.5 }); // Reveals when half of it is visibleKeep delay below about 300 ms. A long delay makes content appear late when the user scrolls quickly.
Reveal a list
cascade() reveals the direct children of a container one after the other, when the container enters the viewport. The child at position index starts after delay + index * interval milliseconds.
import { cascade } from "@awesome-reveal/core";
cascade(".features", { from: { opacity: 0, y: 16 }, interval: 120 });For a long list, or a grid that is taller than the viewport, use reveal(".item") instead. A cascade schedules all children at the same time, so the last children start late or out of view.
Clean up
Each call returns a handle. Call dispose() when you remove the elements before they reveal, for example when a single-page app changes the route:
import { reveal } from "@awesome-reveal/core";
const handle = reveal(".card");
// Later, when you remove the cards:
handle.dispose();dispose() stops the observers and cancels the animations, and the elements return to their own styles. After all the entrances of a call finish, core keeps no reference to the elements, so you do not need dispose().
Play an entrance manually
Some content does not reach the observer, for example a carousel slide that its track clips. To play its waiting entrance, play its paused animations:
import { reveal } from "@awesome-reveal/core";
reveal(".slide", { from: "left" });
export function playEntrance(slide: Element) {
for (const animation of slide.getAnimations()) {
if (animation.playState === "paused") animation.play();
}
}For a cascade, call getAnimations({ subtree: true }) on the container. The loop also plays your own paused animations on the element.
API
reveal(target, options?)
Reveals each target when it enters the viewport. Returns a RevealHandle.
cascade(target, options?)
Reveals the direct element children of each target, one after the other, when the target enters the viewport. Returns a RevealHandle.
Targets
target is one of these:
- an element
- a CSS selector
- an iterable of elements
- an array-like collection, such as
children
Core finds the targets and the cascade children one time, when you call the function. It does not see elements that you add later.
Options
| Option | Type | Default | Description |
| --------------------------- | -------------------- | ---------- | --------------------------------------------------------------- |
| 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 |
| interval (cascade only) | number | 80 | The time between the entrances of two children, in milliseconds |
The package also exports the types Side, StartState, RevealOptions, CascadeOptions, RevealTarget and RevealHandle. The @awesome-reveal/core/adapter entry is for the official framework packages. It is not a public API.
Accessibility
- With
prefers-reduced-motion: reduce, all the content shows immediately, with no animation and no delay. If the preference changes, core cancels the waiting and the running animations. - Focus in a waiting element shows it at once, so a keyboard user does not focus invisible content. Focus in a waiting cascade container shows all its children.
- Focus during a running entrance or its delay does not shorten it, so keep
delayandintervalshort. - Waiting content shows in print. After the print, it waits again. Core uses the
beforeprintandafterprintevents, so a print emulation in developer tools does not show the content. - Hidden content stays in the accessibility tree. Do not make important content depend on decorative motion.
Server-side rendering
You can import the package on a server, but call reveal() and cascade() only in the browser. Core does not hide content before your script runs, so server-rendered content can show, then animate. The React, Vue and Astro packages hide it with a stylesheet.
Good to know
- Core calls
element.animate(keyframes, { duration, delay, fill: "backwards" })and pauses the animation at its start. It plays the animation when the element enters the viewport. The entrance plays one time, and the observation stops at the reveal. There is no exit animation. - The entrance animates
opacityand the individualtranslate,scale,rotateandfilterproperties, but only the properties that the start state sets. Your owntransformstays in effect, for example thetranslate(-50%, -50%)of a centred dialog. - While the element waits and during the entrance, the element's own values of the animated properties do not apply. To keep one of these values, put it on an inner element, or use native keyframes.
- Do not reveal one element in two calls. The later entrance overrides the earlier one.
- You cannot change the easing of the generated entrance. For a different easing, use native keyframes.
- If your code finishes or cancels the paused animation before the reveal, the element shows at rest and the entrance does not play.
- A paused animation does not finish. If your code waits for all of
document.getAnimations(), skip the animations withplayState === "paused".
- Only the direct element children animate. Text nodes and deeper descendants do not animate.
- To cascade the words of a text, wrap each word in an element. A transform needs a transformable element, so give inline children
display: inline-block. - A negative
intervalgives negative delays, so each later child starts further into its entrance. cascade()observes the container, which does not move.
- If the viewport is too small to show
thresholdof the element, the element reveals when it enters the viewport. In a scroll container that is too small, it reveals when it leaves the visible area. Keep the default 0 for tall elements and long cascades. - A start state with
opacity: 0, such as each side, waits at its resting position. Other start states and native keyframes wait at their first keyframe, so their offset, scale and rotation change the box that the observer sees. - If an ancestor with
overflowclips the element fully, the element does not reveal. Play the entrance manually.
- The generated entrance runs on the compositor, so a busy main thread does not make it stutter. A start state with
bluris the exception: Chromium runs it on the main thread. - In your own keyframes, animate
opacity,transform,translate,scaleandrotatewhen possible. A layout property, such aswidth,topormargin, causes a layout and a paint on each frame. - Chromium runs an animation with an additive keyframe on the main thread.
- Do not add
will-changefor a reveal. The browser treats an animated property as ifwill-changealready names it.
reveal()andcascade()do not throw for an invalid option. The browser validates the options. If it rejects an easing or a duration, the element shows with no animation. If it rejects the threshold, the entrance plays immediately.- An invalid CSS selector throws the
SyntaxErrorofquerySelectorAll. - The browser ignores an invalid CSS value in a keyframe, and reports nothing.
- Core reports the browser's errors from a microtask, so the console and your error tracker get them. It reports each message one time on a page. WebKit gives the same message to different easing and duration errors, so fix the first error and reload the page to see the next one.
Core writes no style attribute, no inline script and no CSS. It uses element.animate(), which Content Security Policy does not restrict. A policy such as style-src 'self'; script-src 'self' works with core.
Browser support
Chrome 104, Edge 104, Firefox 74 and Safari 14.1 (iOS 14.5), or later. A native keyframe with composite needs Firefox 80 or Safari 16. If IntersectionObserver or Element.animate() is not available, the content shows immediately and does not stay hidden.
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.
