@_gmdev/react-gradient-text 1.0.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 Gabriele Marconetta
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,119 @@
1
+ # react-gradient-text
2
+
3
+ A lightweight React component to render text with CSS gradient colors.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install react-gradient-text
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { ReactGradientText } from "react-gradient-text";
15
+
16
+ function App() {
17
+ return (
18
+ <ReactGradientText
19
+ text="Hello Gradient!"
20
+ colors={["#ff0000", "#00ff00", "#0000ff"]}
21
+ />
22
+ );
23
+ }
24
+ ```
25
+
26
+ ## Props
27
+
28
+ | Prop | Type | Default | Description |
29
+ |-------------|---------------|--------------|------------------------------------------------|
30
+ | `text` | `string` | *required* | Text to render |
31
+ | `colors` | `string[]` | *required* | Array of CSS color strings for the gradient |
32
+ | `direction` | `string` | `"to right"` | CSS gradient direction (e.g. `"45deg"`) |
33
+ | `as` | `ElementType` | `"span"` | HTML element to render |
34
+ | `className` | `string` | `undefined` | Additional CSS class |
35
+ | `style` | `CSSProperties` | `undefined` | Additional inline styles (merged with gradient) |
36
+ | `animation` | `AnimationType` | `undefined` | Animation triggered on viewport entry (`fade-in-*`, `scramble`) |
37
+ | `duration` | `number` | `500` | Animation duration in milliseconds |
38
+
39
+ ## Animations
40
+
41
+ Animations are triggered automatically when the element enters the viewport via `IntersectionObserver`.
42
+
43
+ **Available animations:**
44
+ - `fade-in-up`, `fade-in-down`, `fade-in-left`, `fade-in-right`
45
+ - `fade-in-top-left`, `fade-in-top-right`, `fade-in-bottom-left`, `fade-in-bottom-right`
46
+ - `scramble` — characters shuffle progressively until the final text is revealed
47
+
48
+ ### Fade-in example
49
+
50
+ ```tsx
51
+ <ReactGradientText
52
+ text="Hello!"
53
+ colors={["#667eea", "#764ba2"]}
54
+ animation="fade-in-up"
55
+ duration={800}
56
+ />
57
+ ```
58
+
59
+ ### Scramble example
60
+
61
+ ```tsx
62
+ <ReactGradientText
63
+ text="Scramble!"
64
+ colors={["#ff6b6b", "#4ecdc4"]}
65
+ animation="scramble"
66
+ duration={1000}
67
+ />
68
+ ```
69
+
70
+ ## Examples
71
+
72
+ ### Custom direction
73
+
74
+ ```tsx
75
+ <ReactGradientText
76
+ text="Diagonal!"
77
+ colors={["#667eea", "#764ba2"]}
78
+ direction="45deg"
79
+ />
80
+ ```
81
+
82
+ ### As heading
83
+
84
+ ```tsx
85
+ <ReactGradientText
86
+ text="I am a heading"
87
+ colors={["#f093fb", "#f5576c"]}
88
+ as="h1"
89
+ />
90
+ ```
91
+
92
+ ### Rainbow
93
+
94
+ ```tsx
95
+ <ReactGradientText
96
+ text="Rainbow Text"
97
+ colors={["#ff0000", "#ff7f00", "#ffff00", "#00ff00", "#0000ff", "#4b0082", "#9400d3"]}
98
+ />
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ # Install dependencies
105
+ npm install
106
+
107
+ # Run tests
108
+ npm test
109
+
110
+ # Run Storybook
111
+ npm run storybook
112
+
113
+ # Build the library
114
+ npm run build
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,27 @@
1
+ import { CSSProperties } from 'react';
2
+ import { AnimationType } from './ReactGradientText.types';
3
+ /**
4
+ * Checks whether the given animation is a fade-in variant.
5
+ * @param animation - The animation type to check
6
+ * @returns True if the animation is a fade-in type
7
+ */
8
+ declare function isFadeIn(animation: AnimationType): boolean;
9
+ /**
10
+ * Returns the initial CSS styles for a fade-in animation (hidden state).
11
+ * @param animation - A fade-in animation type
12
+ * @param duration - Transition duration in milliseconds
13
+ * @returns CSSProperties for the hidden state
14
+ */
15
+ declare function getFadeInStartStyle({ animation, duration, }: {
16
+ animation: AnimationType;
17
+ duration: number;
18
+ }): CSSProperties;
19
+ /**
20
+ * Returns the final CSS styles for a fade-in animation (visible state).
21
+ * @param duration - Transition duration in milliseconds
22
+ * @returns CSSProperties for the visible state
23
+ */
24
+ declare function getFadeInEndStyle({ duration }: {
25
+ duration: number;
26
+ }): CSSProperties;
27
+ export { isFadeIn, getFadeInStartStyle, getFadeInEndStyle };
@@ -0,0 +1,20 @@
1
+ import { ReactGradientTextProps } from './ReactGradientText.types';
2
+ /**
3
+ * Builds the linear-gradient CSS value from colors distributed equally.
4
+ * @param colors - Array of CSS color strings
5
+ * @param direction - CSS gradient direction
6
+ * @returns CSS linear-gradient string
7
+ */
8
+ declare function buildGradient({ colors, direction, }: {
9
+ colors: string[];
10
+ direction: string;
11
+ }): string;
12
+ /**
13
+ * ReactGradientText renders text with a CSS gradient applied.
14
+ * Colors are distributed equally across the gradient.
15
+ * Supports fade-in and scramble animations triggered by IntersectionObserver.
16
+ * @param props - Component props
17
+ * @returns JSX element with gradient text
18
+ */
19
+ declare function ReactGradientText({ text, colors, direction, as: Component, className, style, animation, duration, ...rest }: ReactGradientTextProps): import("react/jsx-runtime").JSX.Element;
20
+ export { ReactGradientText, buildGradient };
@@ -0,0 +1,16 @@
1
+ import { ElementType, HTMLAttributes } from 'react';
2
+ export type AnimationType = "fade-in-up" | "fade-in-down" | "fade-in-left" | "fade-in-right" | "fade-in-top-left" | "fade-in-top-right" | "fade-in-bottom-left" | "fade-in-bottom-right" | "scramble";
3
+ export interface ReactGradientTextProps extends HTMLAttributes<HTMLElement> {
4
+ /** Text to render with gradient */
5
+ text: string;
6
+ /** Array of CSS color strings for the gradient */
7
+ colors: string[];
8
+ /** CSS gradient direction (e.g. "to right", "45deg") */
9
+ direction?: string;
10
+ /** HTML element type to render */
11
+ as?: ElementType;
12
+ /** Animation to apply when the element enters the viewport */
13
+ animation?: AnimationType;
14
+ /** Animation duration in milliseconds */
15
+ duration?: number;
16
+ }
@@ -0,0 +1,12 @@
1
+ interface UseIntersectionObserverResult<T extends HTMLElement> {
2
+ ref: React.RefCallback<T>;
3
+ isVisible: boolean;
4
+ }
5
+ /**
6
+ * Custom hook that detects when an element enters the viewport using IntersectionObserver.
7
+ * Triggers only once and disconnects after the first intersection.
8
+ * @returns An object with a ref callback and a boolean indicating visibility
9
+ */
10
+ declare function useIntersectionObserver<T extends HTMLElement = HTMLElement>(): UseIntersectionObserverResult<T>;
11
+ export { useIntersectionObserver };
12
+ export type { UseIntersectionObserverResult };
@@ -0,0 +1,14 @@
1
+ interface UseScrambleAnimationParams {
2
+ text: string;
3
+ duration: number;
4
+ isActive: boolean;
5
+ }
6
+ /**
7
+ * Custom hook that animates text by progressively revealing characters
8
+ * from a scrambled state to the final text.
9
+ * Uses the same characters from the original text as scramble noise.
10
+ * @param params - Object with text, duration (ms), and isActive flag
11
+ * @returns The current display text during the animation
12
+ */
13
+ declare function useScrambleAnimation({ text, duration, isActive, }: UseScrambleAnimationParams): string;
14
+ export { useScrambleAnimation };
@@ -0,0 +1,2 @@
1
+ export { ReactGradientText } from './ReactGradientText';
2
+ export type { ReactGradientTextProps, AnimationType } from './ReactGradientText.types';
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("react/jsx-runtime"),c=require("react"),s=20,S={"fade-in-up":{x:0,y:s},"fade-in-down":{x:0,y:-s},"fade-in-left":{x:s,y:0},"fade-in-right":{x:-s,y:0},"fade-in-top-left":{x:s,y:s},"fade-in-top-right":{x:-s,y:s},"fade-in-bottom-left":{x:s,y:-s},"fade-in-bottom-right":{x:-s,y:-s}};function F(e){return e.startsWith("fade-in-")}function R({animation:e,duration:n}){const t=S[e];return t?{display:"inline-block",opacity:0,transform:`translate(${t.x}px, ${t.y}px)`,transition:`opacity ${n}ms ease-out, transform ${n}ms ease-out`}:{}}function I({duration:e}){return{display:"inline-block",opacity:1,transform:"translate(0, 0)",transition:`opacity ${e}ms ease-out, transform ${e}ms ease-out`}}const E=.1;function $(){const[e,n]=c.useState(!1),t=c.useRef(null),u=c.useRef(null);return c.useEffect(()=>()=>{var r;(r=t.current)==null||r.disconnect()},[]),{ref:c.useCallback(r=>{t.current&&t.current.disconnect(),u.current=r,r&&(t.current=new IntersectionObserver(a=>{var f;const o=a[0];o!=null&&o.isIntersecting&&(n(!0),(f=t.current)==null||f.disconnect())},{threshold:E}),t.current.observe(r))},[]),isVisible:e}}function A(e){for(let n=e.length-1;n>0;n--){const t=Math.floor(Math.random()*(n+1));[e[n],e[t]]=[e[t],e[n]]}return e}function v({text:e,duration:n,isActive:t}){const[u,i]=c.useState(e),r=c.useRef(0),a=c.useRef(0);return c.useEffect(()=>{if(!t){i(e);return}const o=e.length;if(o===0){i("");return}const f=e.split("");a.current=performance.now();function l(d){const m=d-a.current,g=Math.min(m/n,1),p=Math.floor(g*o);if(g>=1){i(e);return}const b=f.slice(0,p),y=f.slice(p),x=A([...y]);i(b.join("")+x.join("")),r.current=requestAnimationFrame(l)}return r.current=requestAnimationFrame(l),()=>{cancelAnimationFrame(r.current)}},[t,e,n]),u}const C="to right",O=500;function k({colors:e,direction:n}){if(e.length===0)return"none";if(e.length===1)return`linear-gradient(${n}, ${e[0]} 0%, ${e[0]} 100%)`;const t=e.map((u,i)=>{const r=i/(e.length-1)*100;return`${u} ${r}%`}).join(", ");return`linear-gradient(${n}, ${t})`}function D({animation:e,duration:n,isVisible:t}){return e?F(e)?t?I({duration:n}):R({animation:e,duration:n}):{}:{}}function j({text:e,colors:n,direction:t=C,as:u="span",className:i,style:r,animation:a,duration:o=O,...f}){const{ref:l,isVisible:d}=$(),m=a==="scramble",p=v({text:e,duration:o,isActive:m&&d}),b=k({colors:n,direction:t}),y=D({animation:a,duration:o,isVisible:d}),x={backgroundImage:b,WebkitBackgroundClip:"text",backgroundClip:"text",WebkitTextFillColor:"transparent",color:"transparent",width:"fit-content",...y,...r},T=m?p:e;return h.jsx(u,{ref:a?l:void 0,className:i,style:x,...f,children:T})}exports.ReactGradientText=j;
@@ -0,0 +1,150 @@
1
+ import { jsx as F } from "react/jsx-runtime";
2
+ import { useState as h, useRef as p, useEffect as T, useCallback as I } from "react";
3
+ const i = 20, S = {
4
+ "fade-in-up": { x: 0, y: i },
5
+ "fade-in-down": { x: 0, y: -i },
6
+ "fade-in-left": { x: i, y: 0 },
7
+ "fade-in-right": { x: -i, y: 0 },
8
+ "fade-in-top-left": { x: i, y: i },
9
+ "fade-in-top-right": { x: -i, y: i },
10
+ "fade-in-bottom-left": { x: i, y: -i },
11
+ "fade-in-bottom-right": { x: -i, y: -i }
12
+ };
13
+ function $(t) {
14
+ return t.startsWith("fade-in-");
15
+ }
16
+ function A({
17
+ animation: t,
18
+ duration: n
19
+ }) {
20
+ const e = S[t];
21
+ return e ? {
22
+ display: "inline-block",
23
+ opacity: 0,
24
+ transform: `translate(${e.x}px, ${e.y}px)`,
25
+ transition: `opacity ${n}ms ease-out, transform ${n}ms ease-out`
26
+ } : {};
27
+ }
28
+ function E({ duration: t }) {
29
+ return {
30
+ display: "inline-block",
31
+ opacity: 1,
32
+ transform: "translate(0, 0)",
33
+ transition: `opacity ${t}ms ease-out, transform ${t}ms ease-out`
34
+ };
35
+ }
36
+ const R = 0.1;
37
+ function v() {
38
+ const [t, n] = h(!1), e = p(null), a = p(null);
39
+ return T(() => () => {
40
+ var r;
41
+ (r = e.current) == null || r.disconnect();
42
+ }, []), { ref: I((r) => {
43
+ e.current && e.current.disconnect(), a.current = r, r && (e.current = new IntersectionObserver(
44
+ (c) => {
45
+ var f;
46
+ const o = c[0];
47
+ o != null && o.isIntersecting && (n(!0), (f = e.current) == null || f.disconnect());
48
+ },
49
+ { threshold: R }
50
+ ), e.current.observe(r));
51
+ }, []), isVisible: t };
52
+ }
53
+ function C(t) {
54
+ for (let n = t.length - 1; n > 0; n--) {
55
+ const e = Math.floor(Math.random() * (n + 1));
56
+ [t[n], t[e]] = [t[e], t[n]];
57
+ }
58
+ return t;
59
+ }
60
+ function k({
61
+ text: t,
62
+ duration: n,
63
+ isActive: e
64
+ }) {
65
+ const [a, s] = h(t), r = p(0), c = p(0);
66
+ return T(() => {
67
+ if (!e) {
68
+ s(t);
69
+ return;
70
+ }
71
+ const o = t.length;
72
+ if (o === 0) {
73
+ s("");
74
+ return;
75
+ }
76
+ const f = t.split("");
77
+ c.current = performance.now();
78
+ function u(l) {
79
+ const d = l - c.current, g = Math.min(d / n, 1), m = Math.floor(g * o);
80
+ if (g >= 1) {
81
+ s(t);
82
+ return;
83
+ }
84
+ const b = f.slice(0, m), y = f.slice(m), x = C([...y]);
85
+ s(b.join("") + x.join("")), r.current = requestAnimationFrame(u);
86
+ }
87
+ return r.current = requestAnimationFrame(u), () => {
88
+ cancelAnimationFrame(r.current);
89
+ };
90
+ }, [e, t, n]), a;
91
+ }
92
+ const D = "to right", O = 500;
93
+ function _({
94
+ colors: t,
95
+ direction: n
96
+ }) {
97
+ if (t.length === 0) return "none";
98
+ if (t.length === 1) return `linear-gradient(${n}, ${t[0]} 0%, ${t[0]} 100%)`;
99
+ const e = t.map((a, s) => {
100
+ const r = s / (t.length - 1) * 100;
101
+ return `${a} ${r}%`;
102
+ }).join(", ");
103
+ return `linear-gradient(${n}, ${e})`;
104
+ }
105
+ function j({
106
+ animation: t,
107
+ duration: n,
108
+ isVisible: e
109
+ }) {
110
+ return t ? $(t) ? e ? E({ duration: n }) : A({ animation: t, duration: n }) : {} : {};
111
+ }
112
+ function M({
113
+ text: t,
114
+ colors: n,
115
+ direction: e = D,
116
+ as: a = "span",
117
+ className: s,
118
+ style: r,
119
+ animation: c,
120
+ duration: o = O,
121
+ ...f
122
+ }) {
123
+ const { ref: u, isVisible: l } = v(), d = c === "scramble", m = k({
124
+ text: t,
125
+ duration: o,
126
+ isActive: d && l
127
+ }), b = _({ colors: n, direction: e }), y = j({ animation: c, duration: o, isVisible: l }), x = {
128
+ backgroundImage: b,
129
+ WebkitBackgroundClip: "text",
130
+ backgroundClip: "text",
131
+ WebkitTextFillColor: "transparent",
132
+ color: "transparent",
133
+ width: "fit-content",
134
+ ...y,
135
+ ...r
136
+ };
137
+ return /* @__PURE__ */ F(
138
+ a,
139
+ {
140
+ ref: c ? u : void 0,
141
+ className: s,
142
+ style: x,
143
+ ...f,
144
+ children: d ? m : t
145
+ }
146
+ );
147
+ }
148
+ export {
149
+ M as ReactGradientText
150
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@_gmdev/react-gradient-text",
3
+ "version": "1.0.0",
4
+ "description": "A React component to render text with CSS gradient colors",
5
+ "author": "Gabriele Marconetta <gabriele.marconetta@gmail.com>",
6
+ "type": "module",
7
+ "main": "./dist/react-gradient-text.cjs",
8
+ "module": "./dist/react-gradient-text.js",
9
+ "types": "./dist/index.d.ts",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/react-gradient-text.js",
17
+ "require": "./dist/react-gradient-text.cjs"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "dev": "vite",
25
+ "build": "tsc --emitDeclarationOnly && vite build",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "storybook": "storybook dev -p 6006",
29
+ "build-storybook": "storybook build",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "peerDependencies": {
33
+ "react": ">=18.0.0",
34
+ "react-dom": ">=18.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@storybook/addon-essentials": "^8.4.0",
38
+ "@storybook/react": "^8.4.0",
39
+ "@storybook/react-vite": "^8.4.0",
40
+ "@testing-library/jest-dom": "^6.6.3",
41
+ "@testing-library/react": "^16.1.0",
42
+ "@types/node": "^25.9.1",
43
+ "@types/react": "^18.3.0",
44
+ "@types/react-dom": "^18.3.0",
45
+ "jsdom": "^25.0.1",
46
+ "react": "^18.3.1",
47
+ "react-dom": "^18.3.1",
48
+ "storybook": "^8.4.0",
49
+ "typescript": "^5.6.0",
50
+ "vite": "^6.0.0",
51
+ "vite-plugin-dts": "^4.3.0",
52
+ "vitest": "^2.1.0"
53
+ },
54
+ "keywords": [
55
+ "react",
56
+ "gradient",
57
+ "text",
58
+ "css",
59
+ "component"
60
+ ],
61
+ "license": "MIT",
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "https://github.com/gab87/react-gradient-text"
65
+ }
66
+ }