@cssints/reveal 0.0.0-stage → 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexei Accio
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 CHANGED
@@ -1,3 +1,141 @@
1
- # Temporary Holding Version
1
+ # @cssints/reveal
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Scroll-driven reveal animations for [cssints](../../README.md): an element runs one of
4
+ [@cssints/motion](../motion/README.md)'s entrance presets as it scrolls into view, on `animation-timeline: view()` and
5
+ `animation-range`, as one chain member of `cssints/plugin` `scope()`. In the spirit of
6
+ [the supersonic plugin](https://github.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation) and
7
+ [lax.js](https://github.com/alexfoxy/lax.js), with the browser doing the work. Where a target lacks scroll-driven
8
+ animations, a 0.3 kB script plays the same keyframes once when the element enters.
9
+
10
+ ```ts
11
+ import { cn, hover } from "cssints" with { type: "cssints" };
12
+ import { reveal } from "@cssints/reveal" with { type: "cssints" };
13
+
14
+ export const fade = reveal();
15
+ export const card = reveal()
16
+ .preset("slide-in-up")
17
+ .range("entry 0% entry 100%")
18
+ .duration("500ms")
19
+ .easing("linear");
20
+ export const tile = cn(reveal().preset("blur-in"), hover(reveal().duration("1s")));
21
+ ```
22
+
23
+ ```css
24
+ @layer _.k {
25
+ @keyframes kf-7f473efd {
26
+ from {
27
+ opacity: var(--motion-o);
28
+ translate: var(--motion-x) var(--motion-y);
29
+ scale: var(--motion-s);
30
+ filter: blur(var(--motion-b));
31
+ }
32
+ }
33
+ @keyframes kf-4f766ef7 {
34
+ from {
35
+ opacity: var(--motion-o);
36
+ }
37
+ }
38
+ ._reveal-core {
39
+ animation: var(--reveal-k) var(--reveal-d) var(--reveal-e) both;
40
+ animation-range: var(--reveal-g);
41
+ --reveal-k: kf-7f473efd;
42
+ --reveal-rk: kf-4f766ef7;
43
+ --reveal-d: 300ms;
44
+ --reveal-e: ease-out;
45
+ --reveal-g: entry 0% cover 40%;
46
+ --motion-o: 0;
47
+ --motion-x: 0;
48
+ --motion-y: 0;
49
+ --motion-s: 1;
50
+ --motion-b: 0;
51
+ animation-timeline: view();
52
+ }
53
+ @media (prefers-reduced-motion: reduce) {
54
+ ._reveal-core {
55
+ animation-name: var(--reveal-rk);
56
+ }
57
+ }
58
+ /* only when a target lacks scroll-driven animations */
59
+ @supports not (animation-timeline: view()) {
60
+ [data-reveal] ._reveal-core:not([data-reveal-in]) {
61
+ animation-play-state: paused;
62
+ }
63
+ }
64
+ }
65
+ @layer _.a {
66
+ ._1a9p5a9 {
67
+ --motion-y: 25%;
68
+ }
69
+ ._yorqor {
70
+ --reveal-g: entry 0% entry 100%;
71
+ }
72
+ ._k4abtq {
73
+ --reveal-d: 500ms;
74
+ }
75
+ /* ... */
76
+ }
77
+ ```
78
+
79
+ ## Rules
80
+
81
+ - **Methods**, each once, in any order, all optional: `preset(name)` (`fade-in`), `range(<'animation-range'>)`
82
+ (`entry 0% cover 40%`), `duration(<time [0s,∞]>)` (300ms) and `easing(<easing-function>)` (`ease-out`). Each is one
83
+ class that sets a custom property, so a method works under a condition. Where the timeline is native, the range
84
+ decides when the animation runs and the scroll drives it (back and forth); the duration and the easing shape the
85
+ fallback's one play (the easing applies to the scrubbed progress too).
86
+ - **Presets** are motion's entrance presets: `fade-in`, `slide-in-up`, `slide-in-down`, `slide-in-left`,
87
+ `slide-in-right`, `scale-in`, `blur-in`. The `-out` ones are not: the fallback can only play when an element enters.
88
+ - **Reuse of motion.** The kernel names motion's `motionKeyframes.in` and `motionKeyframes.reducedIn`, which carries
89
+ their rules (shared with `motion()` on the same page: one rule each), and a preset sets the values those keyframes
90
+ read, from motion's `presets` table under motion's names (`motionVars`). Nothing is copied, not even a name.
91
+ - **Reduced motion.** Under `prefers-reduced-motion: reduce` the kernel runs the opacity-only keyframes (motion's rule):
92
+ a slide, scale or blur becomes a fade over the same range.
93
+ - **One kernel, written as styles.** The `animation` shorthand resets `animation-timeline` and `animation-range`; a
94
+ kernel prints a shorthand before its longhands, so the timeline set beside it wins.
95
+ - **Conflicts.** `reveal()` sets the animation: a `cn()` that holds it and an `animation*` property is a build error,
96
+ derived from the kernel: `cssints: reveal: kernel core sets animation, animation-timeline, animation-range, animation-name (this style also sets animation-duration)`.
97
+ - **Errors** are the grammar's, in the types and at build time: `reveal.preset: unexpected "fade-out" in "fade-out",
98
+ expected fade-in | ...`, `reveal.range: unexpected "sideways" in "entry 0% sideways"`.
99
+
100
+ ## Browsers
101
+
102
+ - **Native**: Chrome 115, Safari 26 (scroll-driven animations, `css.properties.animation-timeline` and
103
+ `animation-range`). Firefox has them behind a flag only.
104
+ - **The fallback.** When a browserslist target lacks them, the build adds the `@supports not` rule above to the sheet
105
+ (it stays out of the sheet for targets that all have them), and the kernel's `animation-timeline` and `animation-range` give no warning: the runtime polyfills both. The
106
+ script, `@cssints/reveal/runtime`, comes with it: the runtime entry names it (`script`), so the module that uses
107
+ `reveal()` imports it through `virtual:cssints/runtime/reveal.io`, which is empty when every target has the timeline.
108
+ Nothing to import by hand. In the browser it does nothing where `CSS.supports("animation-timeline:view()")`; elsewhere it
109
+ marks `<html data-reveal>` (the rule then holds each reveal at its first frame) and an `IntersectionObserver` gives an
110
+ element `data-reveal-in` when it enters the viewport, which plays the keyframes once, over `duration()`. A
111
+ `MutationObserver` picks up elements added later. 536 bytes minified, 319 gzip; no imports.
112
+ - **Why not a polyfill.** [flackr's scroll-timeline polyfill](https://github.com/flackr/scroll-timeline) scrubs the
113
+ animation as the native timeline does, but it is about 61 kB minified (17 kB gzip), rewrites stylesheets and runs on
114
+ every scroll. A reveal is an entrance: playing it once on entry is what most sites want, costs 1/50 of the bytes and
115
+ nothing per scroll. The price is that the fallback is not scrubbed and ignores `range()`.
116
+ - Without the script, a target that lacks the timeline plays the animation on load, as `motion()` would: nothing stays
117
+ hidden.
118
+ - Tested in Chrome 154 and WebKit 26.6 (native, and the fallback forced) and Firefox 155 (the fallback):
119
+ `bun run test:browsers`.
120
+
121
+ ## Limits
122
+
123
+ - With targets that mix both (the default browserslist), the script ships to every browser and only probes where the
124
+ timeline is native.
125
+ - The fallback does not follow the range, the scroll direction or an element that leaves the viewport: it plays once. An
126
+ element that gets the class after it is in the document (not added with it) is not seen.
127
+ - Until the script runs, a reveal below the fold may start playing; it is paused where it is and resumes on entry. The
128
+ script runs with the first module that uses `reveal()`.
129
+ - Entrance presets only, one per element; for an exit on scroll write the animation with `animationTimeline("view()")`
130
+ and `animationRange("exit ...")` yourself.
131
+
132
+ ```bash
133
+ cd packages/reveal && bun run test # fixtures through the engine (output by targets, runtime module, conflicts, errors)
134
+ cd packages/reveal && bun run test:browsers # Chrome, Firefox and WebKit through playwright-core
135
+ ```
136
+
137
+ ## Credits
138
+
139
+ In the spirit of [the supersonic plugin](https://github.com/the-illarionov/the-supersonic-plugin-for-scroll-based-animation)
140
+ (MIT, Alex Illarionov) and [lax.js](https://github.com/alexfoxy/lax.js) (MIT, Alex Fox). The presets are
141
+ [@cssints/motion](../motion/README.md)'s. No code is copied.
@@ -0,0 +1,58 @@
1
+ //#region src/index.d.ts
2
+ /** The scope of `reveal()`, for a scope that embeds it (its kernel `core`). */
3
+ export declare const revealScope: import("cssints/plugin").Scope<"reveal", {
4
+ readonly "#core": {
5
+ readonly css: (css: import("cssints/plugin").Css) => import("cssints").Style[];
6
+ };
7
+ readonly "#io": {
8
+ readonly polyfills: ["css.properties.animation-timeline", "css.properties.animation-range"];
9
+ readonly script: "@cssints/reveal/runtime";
10
+ readonly css: "@supports not (animation-timeline:view()){[data-reveal] &:not([data-reveal-in]){animation-play-state:paused}}";
11
+ };
12
+ readonly reveal: {
13
+ readonly uses: readonly ["core", "io"];
14
+ readonly chain: {
15
+ readonly preset: {
16
+ readonly args: "fade-in | slide-in-up | slide-in-down | slide-in-left | slide-in-right | scale-in | blur-in";
17
+ readonly emit: (name: string) => (readonly ["--motion-o" | "--motion-x" | "--motion-y" | "--motion-s" | "--motion-b", string])[];
18
+ };
19
+ readonly range: {
20
+ readonly args: "<'animation-range'>";
21
+ readonly emit: (v: string) => [string, string][];
22
+ };
23
+ readonly duration: {
24
+ readonly args: "<time [0s,∞]>";
25
+ readonly emit: (v: string) => [string, string][];
26
+ };
27
+ readonly easing: {
28
+ readonly args: "<easing-function>";
29
+ readonly emit: (v: string) => [string, string][];
30
+ };
31
+ };
32
+ };
33
+ }>;
34
+ /**
35
+ * `reveal().preset("slide-in-up").range("entry 0% entry 100%").duration("500ms").easing("linear")`: motion's entrance
36
+ * preset driven by the element's scroll into view, each method once and optional (`fade-in`, `entry 0% cover 40%`,
37
+ * 300ms and `ease-out` for the fallback, which plays once on entry). Under `prefers-reduced-motion: reduce` only the
38
+ * opacity moves. It sets the animation, so an `animation*` property in the same `cn()` is a build error.
39
+ */
40
+ export declare const reveal: () => import("cssints/plugin").Chain<"reveal", {
41
+ readonly preset: {
42
+ readonly args: "fade-in | slide-in-up | slide-in-down | slide-in-left | slide-in-right | scale-in | blur-in";
43
+ readonly emit: (name: string) => (readonly ["--motion-o" | "--motion-x" | "--motion-y" | "--motion-s" | "--motion-b", string])[];
44
+ };
45
+ readonly range: {
46
+ readonly args: "<'animation-range'>";
47
+ readonly emit: (v: string) => [string, string][];
48
+ };
49
+ readonly duration: {
50
+ readonly args: "<time [0s,∞]>";
51
+ readonly emit: (v: string) => [string, string][];
52
+ };
53
+ readonly easing: {
54
+ readonly args: "<easing-function>";
55
+ readonly emit: (v: string) => [string, string][];
56
+ };
57
+ }, {}>;
58
+ //#endregion
package/dist/index.mjs ADDED
@@ -0,0 +1,68 @@
1
+ import { motionKeyframes, motionVars, presets } from "@cssints/motion";
2
+ import { scope } from "cssints/plugin";
3
+ //#region src/index.ts
4
+ const names = "fade-in | slide-in-up | slide-in-down | slide-in-left | slide-in-right | scale-in | blur-in";
5
+ const fields = Object.keys(motionVars);
6
+ const emitPreset = (name) => {
7
+ const p = presets[name];
8
+ return fields.flatMap((f) => {
9
+ const v = p[f];
10
+ return v === void 0 ? [] : [[motionVars[f], v]];
11
+ });
12
+ };
13
+ /** The scope of `reveal()`, for a scope that embeds it (its kernel `core`). */
14
+ const revealScope = scope("reveal", {
15
+ "#core": { css: (css) => [
16
+ css.vars({
17
+ "--_k": motionKeyframes.in,
18
+ "--_rk": motionKeyframes.reducedIn,
19
+ "--_d": "300ms",
20
+ "--_e": "ease-out",
21
+ "--_g": "entry 0% cover 40%",
22
+ [motionVars.opacity]: 0,
23
+ [motionVars.x]: 0,
24
+ [motionVars.y]: 0,
25
+ [motionVars.scale]: 1,
26
+ [motionVars.blur]: 0
27
+ }),
28
+ css.animation("var(--_k) var(--_d) var(--_e) both"),
29
+ css.animationTimeline("view()"),
30
+ css.animationRange("var(--_g)"),
31
+ css.media("(prefers-reduced-motion: reduce)")(css.animationName("var(--_rk)"))
32
+ ] },
33
+ "#io": {
34
+ polyfills: ["css.properties.animation-timeline", "css.properties.animation-range"],
35
+ script: "@cssints/reveal/runtime",
36
+ css: `@supports not (animation-timeline:view()){[data-reveal] &:not([data-reveal-in]){animation-play-state:paused}}`
37
+ },
38
+ reveal: {
39
+ uses: ["core", "io"],
40
+ chain: {
41
+ preset: {
42
+ args: names,
43
+ emit: emitPreset
44
+ },
45
+ range: {
46
+ args: "<'animation-range'>",
47
+ emit: (v) => [["--_g", v]]
48
+ },
49
+ duration: {
50
+ args: "<time [0s,∞]>",
51
+ emit: (v) => [["--_d", v]]
52
+ },
53
+ easing: {
54
+ args: "<easing-function>",
55
+ emit: (v) => [["--_e", v]]
56
+ }
57
+ }
58
+ }
59
+ });
60
+ /**
61
+ * `reveal().preset("slide-in-up").range("entry 0% entry 100%").duration("500ms").easing("linear")`: motion's entrance
62
+ * preset driven by the element's scroll into view, each method once and optional (`fade-in`, `entry 0% cover 40%`,
63
+ * 300ms and `ease-out` for the fallback, which plays once on entry). Under `prefers-reduced-motion: reduce` only the
64
+ * opacity moves. It sets the animation, so an `animation*` property in the same `cn()` is a build error.
65
+ */
66
+ const { reveal } = revealScope.export();
67
+ //#endregion
68
+ export { reveal, revealScope };
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1 @@
1
+ if(typeof CSS<`u`&&!CSS.supports(`animation-timeline:view()`)){let e=document.documentElement,t=`._reveal-core`,n=new IntersectionObserver(e=>{for(let t of e)t.isIntersecting&&(t.target.setAttribute(`data-reveal-in`,``),n.unobserve(t.target))}),r=e=>{e.matches(t)&&n.observe(e);for(let r of e.querySelectorAll(t))n.observe(r)};e.setAttribute(`data-reveal`,``),r(e),new MutationObserver(e=>{for(let t of e)for(let e of t.addedNodes)e instanceof Element&&r(e)}).observe(e,{childList:!0,subtree:!0})}
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@cssints/reveal",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Scroll-driven reveal animations for cssints: motion's presets on animation-timeline: view(), with a tiny IntersectionObserver fallback",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/alexeiaccio/css-in-ts-compiler.git",
9
+ "directory": "packages/reveal"
10
+ },
11
+ "homepage": "https://cssints.dev",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "type": "module",
16
+ "sideEffects": [
17
+ "./src/runtime.ts",
18
+ "./dist/runtime.mjs"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.mts",
23
+ "default": "./dist/index.mjs"
24
+ },
25
+ "./runtime": {
26
+ "types": "./dist/runtime.d.mts",
27
+ "default": "./dist/runtime.mjs"
28
+ }
29
+ },
30
+ "dependencies": {
31
+ "@cssints/motion": "^0.2.0"
32
+ },
33
+ "peerDependencies": {
34
+ "cssints": "^0.2.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=22"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }