scroll-text-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/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # scroll-text-reveal
2
+
3
+ A tiny TypeScript library that reveals text line by line when it enters the
4
+ viewport. Built for static websites with GSAP, ScrollTrigger, and SplitText.
5
+
6
+ ## Features
7
+
8
+ - Line-by-line masked text reveal
9
+ - One small function and seven practical options
10
+ - Configurable duration, trigger delay, line stagger, distance, start position,
11
+ and easing
12
+ - Optional animation for elements visible during initialization
13
+ - Responsive line re-splitting after font loading and width changes
14
+ - Automatic screen-reader attributes through SplitText
15
+ - Automatic `prefers-reduced-motion` safeguard
16
+ - Duplicate initialization protection
17
+ - ESM, CommonJS, and TypeScript declarations
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pnpm add scroll-text-reveal gsap
23
+ ```
24
+
25
+ ```bash
26
+ npm install scroll-text-reveal gsap
27
+ ```
28
+
29
+ ```bash
30
+ yarn add scroll-text-reveal gsap
31
+ ```
32
+
33
+ GSAP 3.13 or newer is required.
34
+
35
+ ## Quick start
36
+
37
+ Add a selector to the text you want to reveal:
38
+
39
+ ```html
40
+ <h2 data-scroll-text-reveal>
41
+ A heading that reveals one line at a time.
42
+ </h2>
43
+ ```
44
+
45
+ Initialize it once after the page markup is available:
46
+
47
+ ```ts
48
+ import { scrollTextReveal } from "scroll-text-reveal";
49
+
50
+ scrollTextReveal("[data-scroll-text-reveal]");
51
+ ```
52
+
53
+ The function accepts a CSS selector, one `HTMLElement`, or an iterable
54
+ collection of `HTMLElement`s.
55
+
56
+ ## Configuration
57
+
58
+ ```ts
59
+ scrollTextReveal(".reveal-heading", {
60
+ duration: 0.85,
61
+ delay: 0,
62
+ stagger: 0.1,
63
+ distance: 115,
64
+ start: 86,
65
+ ease: "power3.out",
66
+ animateInitiallyVisible: false,
67
+ });
68
+ ```
69
+
70
+ | Option | Type | Default | Description |
71
+ | --- | --- | --- | --- |
72
+ | `duration` | `number` | `0.85` | Animation duration in seconds |
73
+ | `delay` | `number` | `0` | Pause after the element reaches its viewport start position |
74
+ | `stagger` | `number` | `0.1` | Delay between consecutive lines in seconds |
75
+ | `distance` | `number` | `115` | Initial vertical offset as a percentage of each line |
76
+ | `start` | `number` | `86` | Viewport percentage at which the reveal starts |
77
+ | `ease` | `string` | `power3.out` | GSAP easing name |
78
+ | `animateInitiallyVisible` | `boolean` | `false` | Animate text already visible during initialization |
79
+
80
+ `start: 86` means that the animation is triggered when the top of the element
81
+ reaches a point 86% down the viewport.
82
+
83
+ ### Delayed reveal
84
+
85
+ `delay` starts after ScrollTrigger activates the animation:
86
+
87
+ ```ts
88
+ scrollTextReveal(".delayed-heading", {
89
+ delay: 0.5,
90
+ });
91
+ ```
92
+
93
+ ### Initially visible text
94
+
95
+ By default, text already visible when `scrollTextReveal()` runs is left
96
+ untouched. Enable its animation explicitly:
97
+
98
+ ```ts
99
+ scrollTextReveal(".hero-heading", {
100
+ animateInitiallyVisible: true,
101
+ delay: 0.2,
102
+ });
103
+ ```
104
+
105
+ ### Direct elements
106
+
107
+ ```ts
108
+ const heading = document.querySelector<HTMLElement>(".heading");
109
+
110
+ if (heading) {
111
+ scrollTextReveal(heading);
112
+ }
113
+ ```
114
+
115
+ ```ts
116
+ scrollTextReveal(document.querySelectorAll<HTMLElement>(".reveal"));
117
+ ```
118
+
119
+ Calling `scrollTextReveal()` more than once for an element is safe. An element
120
+ that has already been initialized is ignored.
121
+
122
+ ## Accessibility
123
+
124
+ When a visitor requests reduced motion, the function returns before splitting
125
+ or animating any text.
126
+
127
+ SplitText uses `aria: "auto"` so the original text is exposed as a single
128
+ accessible label while generated lines are hidden from screen readers. For text
129
+ containing interactive nested elements such as links, review the alternative
130
+ accessibility strategy in the
131
+ [SplitText documentation](https://gsap.com/docs/v3/Plugins/SplitText/).
132
+
133
+ ## Browser support
134
+
135
+ The library targets modern browsers supported by GSAP and requires a browser
136
+ DOM. Call it after the relevant HTML has been parsed, either from a module at
137
+ the end of `body` or after `DOMContentLoaded`.
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ pnpm install
143
+ pnpm typecheck
144
+ pnpm test
145
+ pnpm build
146
+ pnpm build:docs
147
+ ```
148
+
149
+ Use `pnpm dev` to open the demo.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A CSS selector, HTML element, or iterable collection of HTML elements.
3
+ */
4
+ export type ScrollTextRevealTarget = string | HTMLElement | Iterable<HTMLElement>;
5
+ /**
6
+ * Controls the line reveal animation.
7
+ */
8
+ export interface ScrollTextRevealOptions {
9
+ /** Animation duration in seconds. */
10
+ duration?: number;
11
+ /** Delay in seconds after the element reaches its viewport start position. */
12
+ delay?: number;
13
+ /** Delay in seconds between consecutive lines. */
14
+ stagger?: number;
15
+ /** Initial vertical offset as a percentage of each line. */
16
+ distance?: number;
17
+ /** Viewport percentage at which the animation starts. */
18
+ start?: number;
19
+ /** Any GSAP ease name accepted by gsap.from(). */
20
+ ease?: string;
21
+ /** Animate elements already visible when scrollTextReveal() is called. */
22
+ animateInitiallyVisible?: boolean;
23
+ }
24
+ export interface ScrollTextRevealDefaults {
25
+ readonly duration: number;
26
+ readonly delay: number;
27
+ readonly stagger: number;
28
+ readonly distance: number;
29
+ readonly start: number;
30
+ readonly ease: string;
31
+ readonly animateInitiallyVisible: boolean;
32
+ }
33
+ export declare const scrollTextRevealDefaults: ScrollTextRevealDefaults;
34
+ /**
35
+ * Reveals text line by line when it enters the viewport.
36
+ *
37
+ * Calling the function more than once for the same element is safe: an element
38
+ * is initialized only once.
39
+ *
40
+ * @example
41
+ * scrollTextReveal("[data-scroll-text-reveal]", {
42
+ * duration: 0.9,
43
+ * delay: 0.15,
44
+ * stagger: 0.08,
45
+ * });
46
+ */
47
+ export declare const scrollTextReveal: (target: ScrollTextRevealTarget, options?: ScrollTextRevealOptions) => void;
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var g=Object.create;var i=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var R=Object.getPrototypeOf,w=Object.prototype.hasOwnProperty;var S=(t,e)=>{for(var r in e)i(t,r,{get:e[r],enumerable:!0})},d=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let l of x(e))!w.call(t,l)&&l!==r&&i(t,l,{get:()=>e[l],enumerable:!(n=v(e,l))||n.enumerable});return t};var E=(t,e,r)=>(r=t!=null?g(R(t)):{},d(e||!t||!t.__esModule?i(r,"default",{value:t,enumerable:!0}):r,t)),M=t=>d(i({},"__esModule",{value:!0}),t);var L={};S(L,{scrollTextReveal:()=>y,scrollTextRevealDefaults:()=>c});module.exports=M(L);var a=E(require("gsap"),1),p=require("gsap/ScrollTrigger"),s=require("gsap/SplitText"),c=Object.freeze({duration:.85,delay:0,stagger:.1,distance:115,start:86,ease:"power3.out",animateInitiallyVisible:!1}),m=new WeakSet,T=!1,h=()=>{T||(a.default.registerPlugin(p.ScrollTrigger,s.SplitText),T=!0)},f=t=>typeof HTMLElement<"u"&&t instanceof HTMLElement,H=t=>{if(typeof document>"u")throw new Error("scrollTextReveal requires a browser DOM.");if(typeof t=="string")try{return Array.from(document.querySelectorAll(t))}catch{throw new TypeError(`scrollTextReveal received an invalid selector: "${t}".`)}if(f(t))return[t];if(t&&typeof t[Symbol.iterator]=="function"){let e=Array.from(t);if(e.every(f))return e}throw new TypeError("scrollTextReveal target must be a CSS selector, HTMLElement, or iterable of HTMLElements.")},o=(t,e,r,n=Number.POSITIVE_INFINITY)=>{if(!Number.isFinite(e)||e<r||e>n){let l=Number.isFinite(n)?`between ${r} and ${n}`:`greater than or equal to ${r}`;throw new RangeError(`scrollTextReveal option "${t}" must be ${l}.`)}return e},I=t=>{let e={...c,...t};if(o("duration",e.duration,0),o("delay",e.delay,0),o("stagger",e.stagger,0),o("distance",e.distance,0),o("start",e.start,0,100),typeof e.ease!="string"||!e.ease.trim())throw new TypeError('scrollTextReveal option "ease" must be a non-empty string.');if(typeof e.animateInitiallyVisible!="boolean")throw new TypeError('scrollTextReveal option "animateInitiallyVisible" must be a boolean.');return e},O=t=>{let e=t.getBoundingClientRect();return e.top<window.innerHeight&&e.bottom>0},y=(t,e={})=>{if(typeof window>"u")throw new Error("scrollTextReveal requires a browser DOM.");let r=I(e),n=H(t);if(!(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches||n.length===0)){h();for(let l of n){if(m.has(l))continue;let u=O(l);u&&!r.animateInitiallyVisible||(s.SplitText.create(l,{type:"lines",mask:"lines",linesClass:"scroll-text-reveal-line",autoSplit:!0,aria:"auto",onSplit:b=>a.default.from(b.lines,{yPercent:r.distance,opacity:0,duration:r.duration,delay:r.delay,stagger:r.stagger,ease:r.ease,...u?{}:{scrollTrigger:{trigger:l,start:`top ${r.start}%`,once:!0}}})}),m.add(l))}}};0&&(module.exports={scrollTextReveal,scrollTextRevealDefaults});
@@ -0,0 +1,2 @@
1
+ export { scrollTextReveal, scrollTextRevealDefaults, } from "./ScrollTextReveal.js";
2
+ export type { ScrollTextRevealDefaults, ScrollTextRevealOptions, ScrollTextRevealTarget, } from "./ScrollTextReveal.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import u from"gsap";import{ScrollTrigger as f}from"gsap/ScrollTrigger";import{SplitText as d}from"gsap/SplitText";var m=Object.freeze({duration:.85,delay:0,stagger:.1,distance:115,start:86,ease:"power3.out",animateInitiallyVisible:!1}),a=new WeakSet,s=!1,p=()=>{s||(u.registerPlugin(f,d),s=!0)},c=t=>typeof HTMLElement<"u"&&t instanceof HTMLElement,y=t=>{if(typeof document>"u")throw new Error("scrollTextReveal requires a browser DOM.");if(typeof t=="string")try{return Array.from(document.querySelectorAll(t))}catch{throw new TypeError(`scrollTextReveal received an invalid selector: "${t}".`)}if(c(t))return[t];if(t&&typeof t[Symbol.iterator]=="function"){let e=Array.from(t);if(e.every(c))return e}throw new TypeError("scrollTextReveal target must be a CSS selector, HTMLElement, or iterable of HTMLElements.")},o=(t,e,r,n=Number.POSITIVE_INFINITY)=>{if(!Number.isFinite(e)||e<r||e>n){let l=Number.isFinite(n)?`between ${r} and ${n}`:`greater than or equal to ${r}`;throw new RangeError(`scrollTextReveal option "${t}" must be ${l}.`)}return e},b=t=>{let e={...m,...t};if(o("duration",e.duration,0),o("delay",e.delay,0),o("stagger",e.stagger,0),o("distance",e.distance,0),o("start",e.start,0,100),typeof e.ease!="string"||!e.ease.trim())throw new TypeError('scrollTextReveal option "ease" must be a non-empty string.');if(typeof e.animateInitiallyVisible!="boolean")throw new TypeError('scrollTextReveal option "animateInitiallyVisible" must be a boolean.');return e},g=t=>{let e=t.getBoundingClientRect();return e.top<window.innerHeight&&e.bottom>0},v=(t,e={})=>{if(typeof window>"u")throw new Error("scrollTextReveal requires a browser DOM.");let r=b(e),n=y(t);if(!(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches||n.length===0)){p();for(let l of n){if(a.has(l))continue;let i=g(l);i&&!r.animateInitiallyVisible||(d.create(l,{type:"lines",mask:"lines",linesClass:"scroll-text-reveal-line",autoSplit:!0,aria:"auto",onSplit:T=>u.from(T.lines,{yPercent:r.distance,opacity:0,duration:r.duration,delay:r.delay,stagger:r.stagger,ease:r.ease,...i?{}:{scrollTrigger:{trigger:l,start:`top ${r.start}%`,once:!0}}})}),a.add(l))}}};export{v as scrollTextReveal,m as scrollTextRevealDefaults};
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "scroll-text-reveal",
3
+ "version": "0.1.0",
4
+ "description": "A tiny GSAP-powered line reveal for text entering the viewport.",
5
+ "keywords": [
6
+ "text-reveal",
7
+ "scroll-animation",
8
+ "split-text",
9
+ "gsap",
10
+ "typescript"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/dmitry-conquer/scroll-text-reveal.git"
15
+ },
16
+ "homepage": "https://github.com/dmitry-conquer/scroll-text-reveal#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/dmitry-conquer/scroll-text-reveal/issues"
19
+ },
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "packageManager": "pnpm@11.10.0",
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "registry": "https://registry.npmjs.org/"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "require": "./dist/index.cjs"
38
+ }
39
+ },
40
+ "scripts": {
41
+ "dev": "vite --config vite.docs.config.ts --open",
42
+ "build": "tsup src/index.ts --format esm,cjs --clean --minify && tsc -p tsconfig.build.json",
43
+ "build:docs": "vite build --config vite.docs.config.ts",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "prepack": "pnpm run build",
48
+ "prepublishOnly": "pnpm run typecheck && pnpm test"
49
+ },
50
+ "peerDependencies": {
51
+ "gsap": ">=3.13.0 <4"
52
+ },
53
+ "devDependencies": {
54
+ "gsap": "^3.15.0",
55
+ "happy-dom": "^20.11.0",
56
+ "tsup": "^8.5.1",
57
+ "typescript": "^7.0.2",
58
+ "vite": "^8.1.5",
59
+ "vitest": "^4.1.10"
60
+ },
61
+ "engines": {
62
+ "node": ">=18"
63
+ }
64
+ }