astro-reveal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/Reveal.astro +54 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +41 -0
- package/dist/runtime/observer.d.ts +1 -0
- package/dist/runtime/observer.js +99 -0
- package/dist/styles/reveal.css +129 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nicolás Picoto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# astro-reveal
|
|
2
|
+
|
|
3
|
+
Scroll reveal animations for [Astro](https://astro.build) — smooth slide-ins, fades, scales — that work with **any** UI framework or no framework at all.
|
|
4
|
+
|
|
5
|
+
- **Zero runtime JS by default.** Uses native CSS [scroll-driven animations](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline). In purist mode your page ships a single stylesheet and nothing else.
|
|
6
|
+
- **Opt-in JS engine** (`mode: "observer"`) when you want the classic "reveal once and stay" behaviour and universal browser support — about **0.6 KB** gzipped.
|
|
7
|
+
- **UI-agnostic.** Astro outputs HTML in the end, so the same `data-reveal` attribute animates content whether it came from a React, Vue, Svelte island, or plain HTML.
|
|
8
|
+
- **FOUC-proof and accessible by design.** No flash of hidden content, and `prefers-reduced-motion` is respected out of the box.
|
|
9
|
+
- **Plays nice with View Transitions** and late-hydrating islands.
|
|
10
|
+
|
|
11
|
+
It's the [AOS](https://github.com/michalsnik/aos) idea, rebuilt for the Astro era.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx astro add astro-reveal
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or manually:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install astro-reveal
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// astro.config.mjs
|
|
27
|
+
import { defineConfig } from "astro/config";
|
|
28
|
+
import reveal from "astro-reveal";
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
integrations: [reveal()],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That's it. The integration injects the stylesheet for you — no manual CSS import needed.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
Use the component:
|
|
40
|
+
|
|
41
|
+
```astro
|
|
42
|
+
---
|
|
43
|
+
import Reveal from "astro-reveal/Reveal.astro";
|
|
44
|
+
---
|
|
45
|
+
<Reveal animation="up">
|
|
46
|
+
<h1>I rise into place as you scroll.</h1>
|
|
47
|
+
</Reveal>
|
|
48
|
+
|
|
49
|
+
<Reveal animation="scale" distance="3rem" as="section">
|
|
50
|
+
<p>Render as any tag with `as`.</p>
|
|
51
|
+
</Reveal>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or the raw attribute (works on anything, including markup from other frameworks):
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<div data-reveal="fade">Hello</div>
|
|
58
|
+
<img data-reveal="left" src="/photo.jpg" alt="" />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Animations
|
|
62
|
+
|
|
63
|
+
| Value | Effect |
|
|
64
|
+
| ------- | --------------------------------------- |
|
|
65
|
+
| `up` | rises into place (starts below) |
|
|
66
|
+
| `down` | drops into place (starts above) |
|
|
67
|
+
| `left` | slides left (starts to the right) |
|
|
68
|
+
| `right` | slides right (starts to the left) |
|
|
69
|
+
| `fade` | opacity only |
|
|
70
|
+
| `scale` | scales up from slightly smaller |
|
|
71
|
+
| `blur` | un-blurs into focus |
|
|
72
|
+
|
|
73
|
+
## The two engines
|
|
74
|
+
|
|
75
|
+
You pick the engine; the API stays identical.
|
|
76
|
+
|
|
77
|
+
| `mode` | Runtime JS | Behaviour | Browser support |
|
|
78
|
+
| -------------------- | ---------- | ---------------------------------- | ------------------------------------------- |
|
|
79
|
+
| `"scroll"` (default) | **none** | scrubbed — reverses on scroll up | animates where supported, static elsewhere |
|
|
80
|
+
| `"observer"` | ~0.6 KB | plays **once** and stays | everywhere |
|
|
81
|
+
| `"auto"` | ~0.6 KB | native where supported, JS fallback | identical everywhere |
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
reveal({ mode: "observer" }); // the "on steroids" mode
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Why this matters: in `"scroll"` mode the hidden ("from") state only exists inside an `@supports (animation-timeline: view())` block. A browser that can't animate it **never hides the content** — so there is no flash and nothing can get stuck invisible. The trade-off is that scroll-driven animations are *scrubbed*: scroll back up and they play in reverse. If you want "appear once and stay put", switch to `"observer"`.
|
|
88
|
+
|
|
89
|
+
## Options
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
reveal({
|
|
93
|
+
mode: "scroll", // "scroll" | "observer" | "auto"
|
|
94
|
+
once: true, // observer/auto: reveal once and keep it
|
|
95
|
+
threshold: 0.15, // observer/auto: IntersectionObserver threshold
|
|
96
|
+
rootMargin: "0px 0px -10% 0px", // observer/auto: fire slightly before the fold
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Customisation
|
|
101
|
+
|
|
102
|
+
Tune per element with CSS custom properties (via the component or inline style):
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<div data-reveal="up" style="--reveal-distance: 4rem; --reveal-duration: 1s;"></div>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| Variable | Default | Applies to |
|
|
109
|
+
| -------------------- | ------------------------------ | ----------------- |
|
|
110
|
+
| `--reveal-distance` | `1.5rem` | up/down/left/right |
|
|
111
|
+
| `--reveal-scale` | `0.94` | scale |
|
|
112
|
+
| `--reveal-blur` | `8px` | blur |
|
|
113
|
+
| `--reveal-duration` | `700ms` | observer mode |
|
|
114
|
+
| `--reveal-easing` | `cubic-bezier(.16,1,.3,1)` | observer mode |
|
|
115
|
+
| `--reveal-index` | `0` | stagger position |
|
|
116
|
+
| `--reveal-stagger` | `90ms` | observer mode |
|
|
117
|
+
|
|
118
|
+
Set them globally by targeting `[data-reveal]` in your own CSS. Everything ships inside `@layer astro-reveal`, so your styles always win without specificity battles.
|
|
119
|
+
|
|
120
|
+
### Stagger
|
|
121
|
+
|
|
122
|
+
```astro
|
|
123
|
+
{items.map((item, i) => <Reveal animation="up" index={i}>{item}</Reveal>)}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
In `observer` mode each item gets `transition-delay: index * --reveal-stagger` for a crisp cascade. In `scroll` mode stagger is approximated by nudging each item's scroll range (grouped staggers are crisper in `observer` mode).
|
|
127
|
+
|
|
128
|
+
## Accessibility
|
|
129
|
+
|
|
130
|
+
Users with `prefers-reduced-motion: reduce` see content in its final state immediately, in every mode. This is built into the CSS, not bolted on — there's no configuration to forget.
|
|
131
|
+
|
|
132
|
+
## Browser support
|
|
133
|
+
|
|
134
|
+
Native scroll-driven animations (`animation-timeline: view()`) ship in Chromium-based browsers. Firefox and Safari support has been moving — **check [caniuse.com/css-scroll-timeline](https://caniuse.com/?search=animation-timeline) for the current state** before relying on purist mode for a specific audience.
|
|
135
|
+
|
|
136
|
+
- If you target only modern Chromium → `"scroll"` (zero JS).
|
|
137
|
+
- If you need identical behaviour everywhere → `"auto"` (native where it can, JS where it can't).
|
|
138
|
+
- If you specifically want "reveal once and stay" → `"observer"`.
|
|
139
|
+
|
|
140
|
+
## How it works
|
|
141
|
+
|
|
142
|
+
A tiny head-inline script (only injected in `observer`/`auto` modes) decides which engine runs by toggling a single `reveal-js` class on `<html>` before paint. The two CSS rule sets — one scoped to `html:not(.reveal-js)` (native), one to `html.reveal-js` (JS) — are therefore mutually exclusive by construction. No double-animating, no race conditions.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT © Nicolás Picotto
|
package/Reveal.astro
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
/*
|
|
3
|
+
* <Reveal> — ergonomic wrapper over the `data-reveal` attribute.
|
|
4
|
+
* Works the same under both engines because it only sets the attribute and a
|
|
5
|
+
* few CSS custom properties; the integration's CSS/JS does the rest.
|
|
6
|
+
*
|
|
7
|
+
* <Reveal animation="up">…</Reveal>
|
|
8
|
+
* <Reveal animation="scale" distance="3rem" index={2} as="section">…</Reveal>
|
|
9
|
+
*
|
|
10
|
+
* For raw HTML (no component), just use the attribute directly:
|
|
11
|
+
* <div data-reveal="fade">…</div>
|
|
12
|
+
*/
|
|
13
|
+
export type RevealAnimation = "up" | "down" | "left" | "right" | "fade" | "scale" | "blur";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Direction the element travels as it appears. @default "up" */
|
|
17
|
+
animation?: RevealAnimation;
|
|
18
|
+
/** Element/tag to render. @default "div" */
|
|
19
|
+
as?: string;
|
|
20
|
+
/** Travel distance (up/down/left/right) e.g. "2rem". */
|
|
21
|
+
distance?: string;
|
|
22
|
+
/** Stagger position within a group (0,1,2,…). */
|
|
23
|
+
index?: number;
|
|
24
|
+
/** Transition duration in observer mode, e.g. "500ms". */
|
|
25
|
+
duration?: string;
|
|
26
|
+
class?: string;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
animation = "up",
|
|
32
|
+
as: Tag = "div",
|
|
33
|
+
distance,
|
|
34
|
+
index,
|
|
35
|
+
duration,
|
|
36
|
+
class: className,
|
|
37
|
+
style: userStyle,
|
|
38
|
+
...rest
|
|
39
|
+
} = Astro.props;
|
|
40
|
+
|
|
41
|
+
const vars = [
|
|
42
|
+
distance ? `--reveal-distance:${distance}` : "",
|
|
43
|
+
index != null ? `--reveal-index:${index}` : "",
|
|
44
|
+
duration ? `--reveal-duration:${duration}` : "",
|
|
45
|
+
]
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(";");
|
|
48
|
+
|
|
49
|
+
const style = [vars, userStyle].filter(Boolean).join(";") || undefined;
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
<Tag data-reveal={animation} class={className} style={style} {...rest}>
|
|
53
|
+
<slot />
|
|
54
|
+
</Tag>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AstroIntegration } from "astro";
|
|
2
|
+
export type RevealMode = "scroll" | "observer" | "auto";
|
|
3
|
+
export interface RevealOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Which engine to use.
|
|
6
|
+
* - "scroll" (default) Native CSS scroll-driven animations. Zero runtime
|
|
7
|
+
* JS. Scrubbed: reverses as you scroll back up. Browsers
|
|
8
|
+
* without support show content normally (no animation, no FOUC).
|
|
9
|
+
* - "observer" IntersectionObserver. ~1KB JS. Plays once and stays. Works
|
|
10
|
+
* everywhere. This is the "on steroids" mode.
|
|
11
|
+
* - "auto" Native where supported, falls back to observer where not, so
|
|
12
|
+
* it looks identical across browsers (always ships the ~1KB).
|
|
13
|
+
* @default "scroll"
|
|
14
|
+
*/
|
|
15
|
+
mode?: RevealMode;
|
|
16
|
+
/**
|
|
17
|
+
* Observer/auto only. Reveal once and keep it revealed (true), or re-hide
|
|
18
|
+
* when the element leaves the viewport (false).
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
once?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Observer/auto only. IntersectionObserver threshold.
|
|
24
|
+
* @default 0.15
|
|
25
|
+
*/
|
|
26
|
+
threshold?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Observer/auto only. IntersectionObserver rootMargin. The default trims the
|
|
29
|
+
* bottom so reveals fire slightly before the element hits the fold.
|
|
30
|
+
* @default "0px 0px -10% 0px"
|
|
31
|
+
*/
|
|
32
|
+
rootMargin?: string;
|
|
33
|
+
}
|
|
34
|
+
export default function astroReveal(options?: RevealOptions): AstroIntegration;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
// Read our own package name so injected imports keep resolving even if the
|
|
3
|
+
// package is renamed/forked. (dist/index.js -> ../package.json = package root.)
|
|
4
|
+
const PKG = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
mode: "scroll",
|
|
7
|
+
once: true,
|
|
8
|
+
threshold: 0.15,
|
|
9
|
+
rootMargin: "0px 0px -10% 0px",
|
|
10
|
+
};
|
|
11
|
+
function headInline(opts) {
|
|
12
|
+
const runtime = {
|
|
13
|
+
mode: opts.mode,
|
|
14
|
+
once: opts.once,
|
|
15
|
+
threshold: opts.threshold,
|
|
16
|
+
rootMargin: opts.rootMargin,
|
|
17
|
+
};
|
|
18
|
+
// Runs in <head> before paint. Decides the engine by toggling the marker
|
|
19
|
+
// class, so the CSS rule sets stay mutually exclusive. No marker => purist.
|
|
20
|
+
return `(function(){var o=${JSON.stringify(runtime)};window.__ASTRO_REVEAL__=o;try{var s=window.CSS&&CSS.supports&&CSS.supports("animation-timeline: view()");if(o.mode==="observer"||(o.mode==="auto"&&!s)){document.documentElement.classList.add("reveal-js");}}catch(e){document.documentElement.classList.add("reveal-js");}})();`;
|
|
21
|
+
}
|
|
22
|
+
export default function astroReveal(options = {}) {
|
|
23
|
+
const config = { ...DEFAULTS, ...options };
|
|
24
|
+
return {
|
|
25
|
+
name: PKG.name,
|
|
26
|
+
hooks: {
|
|
27
|
+
"astro:config:setup": ({ injectScript, logger }) => {
|
|
28
|
+
// The stylesheet is always needed. Injecting it at the `page-ssr`
|
|
29
|
+
// stage associates it with each page's server render, so Astro emits
|
|
30
|
+
// the <link>/inline <style> reliably — and ships ZERO client JS.
|
|
31
|
+
injectScript("page-ssr", `import ${JSON.stringify(`${PKG.name}/styles.css`)};`);
|
|
32
|
+
if (config.mode !== "scroll") {
|
|
33
|
+
injectScript("head-inline", headInline(config));
|
|
34
|
+
injectScript("page", `import ${JSON.stringify(`${PKG.name}/observer`)};`);
|
|
35
|
+
}
|
|
36
|
+
logger.info(`enabled in "${config.mode}" mode` +
|
|
37
|
+
(config.mode === "scroll" ? " (zero runtime JS)" : ` (once: ${config.once})`));
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* astro-reveal — observer runtime ("on steroids" mode)
|
|
3
|
+
* Injected only when mode is "observer" or "auto". Loaded deferred.
|
|
4
|
+
*
|
|
5
|
+
* Reads config from window.__ASTRO_REVEAL__ (set by the head-inline script,
|
|
6
|
+
* which also decides whether the `.reveal-js` marker belongs on <html>).
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities — the four things AOS gets wrong:
|
|
9
|
+
* 1. Never touch anything unless this browser is actually using the JS
|
|
10
|
+
* engine (the `.reveal-js` marker is present). In "auto" mode a browser
|
|
11
|
+
* with native scroll-timeline support has no marker, so we no-op.
|
|
12
|
+
* 2. Re-scan after Astro View Transitions (`astro:page-load`).
|
|
13
|
+
* 3. Watch for late-hydrated islands via MutationObserver.
|
|
14
|
+
* 4. Bail entirely under prefers-reduced-motion (CSS already shows content).
|
|
15
|
+
*/
|
|
16
|
+
const REVEALED = "is-revealed";
|
|
17
|
+
function getConfig() {
|
|
18
|
+
const w = window;
|
|
19
|
+
return (w.__ASTRO_REVEAL__ ?? {
|
|
20
|
+
mode: "observer",
|
|
21
|
+
once: true,
|
|
22
|
+
threshold: 0.15,
|
|
23
|
+
rootMargin: "0px 0px -10% 0px",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function shouldRun() {
|
|
27
|
+
// Only the JS engine touches the DOM. If the marker isn't there
|
|
28
|
+
// (purist / auto-on-supporting-browser), the CSS engine owns this page.
|
|
29
|
+
if (!document.documentElement.classList.contains("reveal-js"))
|
|
30
|
+
return false;
|
|
31
|
+
// Respect the user. The hidden state is gated by no-preference in CSS,
|
|
32
|
+
// so content is already visible — nothing for us to do.
|
|
33
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
34
|
+
return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
let observer = null;
|
|
38
|
+
let mutationObserver = null;
|
|
39
|
+
const tracked = new WeakSet();
|
|
40
|
+
function reveal(el) {
|
|
41
|
+
el.classList.add(REVEALED);
|
|
42
|
+
}
|
|
43
|
+
function observe(el, config) {
|
|
44
|
+
if (tracked.has(el) || el.classList.contains(REVEALED))
|
|
45
|
+
return;
|
|
46
|
+
tracked.add(el);
|
|
47
|
+
observer.observe(el);
|
|
48
|
+
}
|
|
49
|
+
function collect(root = document) {
|
|
50
|
+
return Array.from(root.querySelectorAll("[data-reveal]"));
|
|
51
|
+
}
|
|
52
|
+
function setup() {
|
|
53
|
+
if (!shouldRun())
|
|
54
|
+
return;
|
|
55
|
+
const config = getConfig();
|
|
56
|
+
observer?.disconnect();
|
|
57
|
+
observer = new IntersectionObserver((entries) => {
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (!entry.isIntersecting) {
|
|
60
|
+
if (!config.once)
|
|
61
|
+
entry.target.classList.remove(REVEALED);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
reveal(entry.target);
|
|
65
|
+
if (config.once)
|
|
66
|
+
observer.unobserve(entry.target);
|
|
67
|
+
}
|
|
68
|
+
}, { threshold: config.threshold, rootMargin: config.rootMargin });
|
|
69
|
+
for (const el of collect())
|
|
70
|
+
observe(el, config);
|
|
71
|
+
// Watch for elements added after first paint (hydrated islands, CMS
|
|
72
|
+
// fragments swapped in, client:visible components, etc.)
|
|
73
|
+
mutationObserver?.disconnect();
|
|
74
|
+
mutationObserver = new MutationObserver((mutations) => {
|
|
75
|
+
for (const m of mutations) {
|
|
76
|
+
for (const node of Array.from(m.addedNodes)) {
|
|
77
|
+
if (!(node instanceof Element))
|
|
78
|
+
continue;
|
|
79
|
+
if (node.matches?.("[data-reveal]"))
|
|
80
|
+
observe(node, config);
|
|
81
|
+
for (const child of collect(node))
|
|
82
|
+
observe(child, config);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
87
|
+
}
|
|
88
|
+
function init() {
|
|
89
|
+
if (document.readyState === "loading") {
|
|
90
|
+
document.addEventListener("DOMContentLoaded", setup, { once: true });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
setup();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
init();
|
|
97
|
+
// Astro View Transitions: the DOM is swapped, observers are stale. Re-arm.
|
|
98
|
+
document.addEventListener("astro:page-load", setup);
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* astro-reveal — scroll reveal animations for Astro
|
|
3
|
+
* --------------------------------------------------
|
|
4
|
+
* Two engines, one API, never both at once:
|
|
5
|
+
*
|
|
6
|
+
* PURIST (default) : native CSS scroll-driven animations.
|
|
7
|
+
* 0 bytes of runtime JS. Scrubbed (reverses on scroll up).
|
|
8
|
+
* Active on html:not(.reveal-js) + @supports view().
|
|
9
|
+
*
|
|
10
|
+
* OBSERVER (steroids): IntersectionObserver toggles `.is-revealed`.
|
|
11
|
+
* ~1KB JS. Plays once and stays. Works everywhere.
|
|
12
|
+
* Active when html.reveal-js is present.
|
|
13
|
+
*
|
|
14
|
+
* The `.reveal-js` marker is added (or not) by a tiny head-inline script
|
|
15
|
+
* that the integration injects only when the chosen mode needs it. That
|
|
16
|
+
* single class is what flips a browser from one engine to the other, so the
|
|
17
|
+
* two rule sets below are mutually exclusive by construction.
|
|
18
|
+
*
|
|
19
|
+
* Everything lives in `@layer astro-reveal` so your own styles always win
|
|
20
|
+
* without specificity fights.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
@layer astro-reveal {
|
|
24
|
+
|
|
25
|
+
/* Tunable knobs. Override per-element via inline style or globally here. */
|
|
26
|
+
[data-reveal] {
|
|
27
|
+
--reveal-distance: 1.5rem;
|
|
28
|
+
--reveal-blur: 8px;
|
|
29
|
+
--reveal-scale: 0.94;
|
|
30
|
+
--reveal-duration: 700ms;
|
|
31
|
+
--reveal-easing: cubic-bezier(0.16, 1, 0.3, 1); /* easeOutExpo-ish */
|
|
32
|
+
--reveal-index: 0; /* stagger position within a group */
|
|
33
|
+
--reveal-stagger: 90ms; /* per-index delay (observer mode) */
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ============================================================
|
|
37
|
+
* PURIST ENGINE — native scroll-driven animations
|
|
38
|
+
* Only on browsers that support it AND when JS engine is off.
|
|
39
|
+
* If unsupported, NOTHING here applies, so content shows normally.
|
|
40
|
+
* => FOUC / hidden-forever content is structurally impossible.
|
|
41
|
+
* ============================================================ */
|
|
42
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
43
|
+
@supports (animation-timeline: view()) {
|
|
44
|
+
:where(html:not(.reveal-js)) [data-reveal] {
|
|
45
|
+
animation: linear both;
|
|
46
|
+
animation-timeline: view();
|
|
47
|
+
/* Best-effort stagger: nudge the scroll window per index.
|
|
48
|
+
Grouped stagger is crisper in observer mode. */
|
|
49
|
+
animation-range:
|
|
50
|
+
entry calc(0% + var(--reveal-index) * 4%)
|
|
51
|
+
cover calc(30% + var(--reveal-index) * 4%);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:where(html:not(.reveal-js)) [data-reveal="up"] { animation-name: reveal-up; }
|
|
55
|
+
:where(html:not(.reveal-js)) [data-reveal="down"] { animation-name: reveal-down; }
|
|
56
|
+
:where(html:not(.reveal-js)) [data-reveal="left"] { animation-name: reveal-left; }
|
|
57
|
+
:where(html:not(.reveal-js)) [data-reveal="right"] { animation-name: reveal-right; }
|
|
58
|
+
:where(html:not(.reveal-js)) [data-reveal="fade"] { animation-name: reveal-fade; }
|
|
59
|
+
:where(html:not(.reveal-js)) [data-reveal="scale"] { animation-name: reveal-scale; }
|
|
60
|
+
:where(html:not(.reveal-js)) [data-reveal="blur"] { animation-name: reveal-blur; }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ============================================================
|
|
65
|
+
* OBSERVER ENGINE — IntersectionObserver + class toggle
|
|
66
|
+
* Hidden state is scoped under `.reveal-js`, which is only ever
|
|
67
|
+
* added by JS. So if JS is disabled, the class never appears and
|
|
68
|
+
* content stays visible. Reduced-motion users never get hidden.
|
|
69
|
+
* ============================================================ */
|
|
70
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
71
|
+
html.reveal-js [data-reveal] {
|
|
72
|
+
opacity: 0;
|
|
73
|
+
transition-property: opacity, transform, filter;
|
|
74
|
+
transition-duration: var(--reveal-duration);
|
|
75
|
+
transition-timing-function: var(--reveal-easing);
|
|
76
|
+
transition-delay: calc(var(--reveal-index) * var(--reveal-stagger));
|
|
77
|
+
will-change: opacity, transform;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
html.reveal-js [data-reveal="up"] { transform: translateY(var(--reveal-distance)); }
|
|
81
|
+
html.reveal-js [data-reveal="down"] { transform: translateY(calc(-1 * var(--reveal-distance))); }
|
|
82
|
+
html.reveal-js [data-reveal="left"] { transform: translateX(var(--reveal-distance)); }
|
|
83
|
+
html.reveal-js [data-reveal="right"] { transform: translateX(calc(-1 * var(--reveal-distance))); }
|
|
84
|
+
html.reveal-js [data-reveal="scale"] { transform: scale(var(--reveal-scale)); }
|
|
85
|
+
html.reveal-js [data-reveal="blur"] { filter: blur(var(--reveal-blur)); }
|
|
86
|
+
/* "fade" needs no transform, opacity:0 above is enough */
|
|
87
|
+
|
|
88
|
+
/* The revealed state: everything snaps back to neutral. */
|
|
89
|
+
html.reveal-js [data-reveal].is-revealed {
|
|
90
|
+
opacity: 1;
|
|
91
|
+
transform: none;
|
|
92
|
+
filter: none;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ============================================================
|
|
97
|
+
* KEYFRAMES (shared by the purist engine)
|
|
98
|
+
* Naming follows AOS: the value is the direction the element
|
|
99
|
+
* TRAVELS as it appears. `up` rises into place (starts below).
|
|
100
|
+
* ============================================================ */
|
|
101
|
+
@keyframes reveal-up {
|
|
102
|
+
from { opacity: 0; transform: translateY(var(--reveal-distance)); }
|
|
103
|
+
to { opacity: 1; transform: translateY(0); }
|
|
104
|
+
}
|
|
105
|
+
@keyframes reveal-down {
|
|
106
|
+
from { opacity: 0; transform: translateY(calc(-1 * var(--reveal-distance))); }
|
|
107
|
+
to { opacity: 1; transform: translateY(0); }
|
|
108
|
+
}
|
|
109
|
+
@keyframes reveal-left {
|
|
110
|
+
from { opacity: 0; transform: translateX(var(--reveal-distance)); }
|
|
111
|
+
to { opacity: 1; transform: translateX(0); }
|
|
112
|
+
}
|
|
113
|
+
@keyframes reveal-right {
|
|
114
|
+
from { opacity: 0; transform: translateX(calc(-1 * var(--reveal-distance))); }
|
|
115
|
+
to { opacity: 1; transform: translateX(0); }
|
|
116
|
+
}
|
|
117
|
+
@keyframes reveal-fade {
|
|
118
|
+
from { opacity: 0; }
|
|
119
|
+
to { opacity: 1; }
|
|
120
|
+
}
|
|
121
|
+
@keyframes reveal-scale {
|
|
122
|
+
from { opacity: 0; transform: scale(var(--reveal-scale)); }
|
|
123
|
+
to { opacity: 1; transform: scale(1); }
|
|
124
|
+
}
|
|
125
|
+
@keyframes reveal-blur {
|
|
126
|
+
from { opacity: 0; filter: blur(var(--reveal-blur)); }
|
|
127
|
+
to { opacity: 1; filter: blur(0); }
|
|
128
|
+
}
|
|
129
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astro-reveal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scroll reveal animations for Astro. Zero-JS by default via native CSS scroll-driven animations, with an opt-in IntersectionObserver engine. UI-framework agnostic.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Nicolás Picoto",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"astro",
|
|
10
|
+
"astro-integration",
|
|
11
|
+
"astro-component",
|
|
12
|
+
"withastro",
|
|
13
|
+
"animation",
|
|
14
|
+
"scroll",
|
|
15
|
+
"reveal",
|
|
16
|
+
"scroll-driven-animations",
|
|
17
|
+
"aos",
|
|
18
|
+
"intersection-observer"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./Reveal.astro": "./Reveal.astro",
|
|
26
|
+
"./observer": "./dist/runtime/observer.js",
|
|
27
|
+
"./styles.css": "./dist/styles/reveal.css"
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"Reveal.astro",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc && node scripts/copy-assets.mjs",
|
|
39
|
+
"dev": "tsc --watch",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"changeset": "changeset",
|
|
42
|
+
"version": "changeset version",
|
|
43
|
+
"release": "npm run build && changeset publish",
|
|
44
|
+
"prepublishOnly": "npm run build"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"astro": ">=4.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@changesets/cli": "^2.31.0",
|
|
54
|
+
"@types/node": "^20.11.0",
|
|
55
|
+
"astro": "^4.16.0",
|
|
56
|
+
"typescript": "^5.4.0"
|
|
57
|
+
},
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "https://github.com/nicopicoto/astro-reveal.git"
|
|
61
|
+
}
|
|
62
|
+
}
|