react-ez-skeleton 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/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # react-ez-skeleton
2
+
3
+ **Simple. Accessible. Zero-config React skeletons.**
4
+
5
+ `react-ez-skeleton` is a lightweight React skeleton loader library focused on excellent developer experience (DX). It works out of the box, is SSR-safe, respects accessibility and motion preferences, and is easy to theme using CSS variables.
6
+
7
+ No CSS imports. No heavy dependencies. No surprises.
8
+
9
+ ## Why react-ez-skeleton?
10
+
11
+ - **Zero configuration**: works immediately
12
+ - **SSR & hydration safe**: Next.js, Remix, etc.
13
+ - **Accessible by default**: `aria-hidden`
14
+ - **Reduced-motion friendly**: respects `prefers-reduced-motion`
15
+ - **Themeable**: via CSS variables
16
+ - **Test-friendly**: `dataTestId` -> `data-testid`
17
+ - **Tiny API**: no lock-in
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install react-ez-skeleton
23
+ ```
24
+
25
+ or
26
+
27
+ ```bash
28
+ yarn add react-ez-skeleton
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```tsx
34
+ import { Skeleton } from "react-ez-skeleton";
35
+
36
+ export function CardLoading() {
37
+ return <Skeleton width={200} height={20} />;
38
+ }
39
+ ```
40
+
41
+ That’s it.
42
+
43
+ - No CSS imports
44
+ - No providers
45
+ - No setup
46
+
47
+ ## Components
48
+
49
+ ### `Skeleton`
50
+
51
+ The base building block.
52
+
53
+ ```tsx
54
+ <Skeleton width={120} height={16} />
55
+ <Skeleton width="100%" height={24} radius={8} />
56
+ ```
57
+
58
+ #### Props
59
+
60
+ | Prop | Type | Default | Description |
61
+ | --- | --- | --- | --- |
62
+ | `width` | `number \| string` | `"100%"` | Width (px, %, etc.) |
63
+ | `height` | `number \| string` | `16` | Height |
64
+ | `radius` | `number \| string` | `4` | Border radius |
65
+ | `animate` | `boolean` | `true` | Enable shimmer animation |
66
+ | `respectReducedMotion` | `boolean` | `true` | Respects OS motion settings |
67
+ | `ariaHidden` | `boolean` | `true` | Decorative by default |
68
+ | `injectStyles` | `boolean` | `true` | Auto-inject required styles |
69
+ | `dataTestId` | `string` | `—` | Adds `data-testid` |
70
+ | `className` | `string` | `—` | Custom class |
71
+ | `style` | `React.CSSProperties` | `—` | Inline styles |
72
+
73
+ ### `SkeletonText`
74
+
75
+ Perfect for paragraphs and content blocks.
76
+
77
+ ```tsx
78
+ <SkeletonText lines={3} />
79
+ ```
80
+
81
+ Advanced usage:
82
+
83
+ ```tsx
84
+ <SkeletonText
85
+ lines={4}
86
+ randomizeLineWidths
87
+ randomizeMin={60}
88
+ randomizeMax={95}
89
+ dataTestId="post-skeleton"
90
+ />
91
+ ```
92
+
93
+ #### Props (in addition to `SkeletonProps`)
94
+
95
+ | Prop | Type | Default |
96
+ | --- | --- | --- |
97
+ | `lines` | `number` | `3` |
98
+ | `lineHeight` | `number \| string` | `16` |
99
+ | `gap` | `number \| string` | `8` |
100
+ | `lineWidths` | `(number \| string)[]` | `—` |
101
+ | `randomizeLineWidths` | `boolean` | `false` |
102
+ | `randomizeMin` | `number` | `60` |
103
+ | `randomizeMax` | `number` | `100` |
104
+ | `randomizeSeed` | `number` | `—` |
105
+
106
+ Seeded randomness means deterministic layouts (nice for SSR and tests).
107
+
108
+ ### `SkeletonCircle`
109
+
110
+ Ideal for avatars and icons.
111
+
112
+ ```tsx
113
+ <SkeletonCircle size={40} />
114
+ ```
115
+
116
+ ## Theming (DX Highlight)
117
+
118
+ No theme providers. No props explosion.
119
+
120
+ Just CSS variables:
121
+
122
+ ```css
123
+ :root {
124
+ --ez-skeleton-color-start: #2a2a2a;
125
+ --ez-skeleton-color-middle: #3a3a3a;
126
+ --ez-skeleton-color-end: #2a2a2a;
127
+ }
128
+ ```
129
+
130
+ Works automatically across your app.
131
+ Perfect for dark mode and design systems.
132
+
133
+ ## Accessibility & Motion
134
+
135
+ - Skeletons are decorative by default
136
+ - Automatically hidden from screen readers
137
+ - Honors `prefers-reduced-motion`
138
+
139
+ ```tsx
140
+ <Skeleton animate={false} />
141
+ ```
142
+
143
+ ## Testing Support
144
+
145
+ ```tsx
146
+ <Skeleton dataTestId="profile-loading" />
147
+
148
+ expect(screen.getByTestId("profile-loading")).toBeInTheDocument();
149
+ ```
150
+
151
+ `SkeletonText` automatically appends line indexes:
152
+
153
+ - `profile-loading-0`
154
+ - `profile-loading-1`
155
+
156
+ ## SSR & Framework Support
157
+
158
+ Fully safe for:
159
+
160
+ - Next.js (App Router & Pages)
161
+ - Remix
162
+ - Vite SSR
163
+ - Astro
164
+ - CRA
165
+
166
+ Style injection is:
167
+
168
+ - Singleton-based
169
+ - Guarded
170
+ - Client-only
171
+ - No hydration mismatches
172
+
173
+ ## Opt-out Style Injection
174
+
175
+ If you want full control:
176
+
177
+ ```tsx
178
+ <Skeleton injectStyles={false} />
179
+ ```
180
+
181
+ Then provide your own CSS:
182
+
183
+ ```css
184
+ @keyframes react-ez-skeleton-pulse {
185
+ from {
186
+ background-position: 100% 50%;
187
+ }
188
+ to {
189
+ background-position: 0 50%;
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## Bundle Philosophy
195
+
196
+ - No runtime dependencies
197
+ - Tree-shakable
198
+ - Side-effect safe
199
+ - Dual ESM + CJS builds
200
+ - TypeScript first
201
+
202
+ ## Contributing
203
+
204
+ PRs and issues are welcome.
205
+ Please keep changes DX-focused and minimal.
206
+
207
+ ## License
208
+
209
+ MIT © Md. Shafiul Alam
210
+
211
+ Final note: `react-ez-skeleton` is designed for developers who care about correctness, accessibility, and simplicity — without sacrificing control.
package/dist/index.cjs ADDED
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.tsx
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Skeleton: () => Skeleton,
24
+ SkeletonCircle: () => SkeletonCircle,
25
+ SkeletonText: () => SkeletonText,
26
+ default: () => index_default,
27
+ injectSkeletonStyles: () => injectSkeletonStyles
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+ var import_jsx_runtime = require("react/jsx-runtime");
31
+ var stylesInjected = false;
32
+ var toCssSize = (value) => {
33
+ if (value === void 0) return void 0;
34
+ if (typeof value === "number") return `${value}px`;
35
+ return value;
36
+ };
37
+ var Skeleton = ({
38
+ width = "100%",
39
+ height = 16,
40
+ radius = 4,
41
+ className = "",
42
+ style,
43
+ animate = true,
44
+ respectReducedMotion = true,
45
+ ariaHidden = true,
46
+ injectStyles = true,
47
+ dataTestId
48
+ }) => {
49
+ if (injectStyles) injectSkeletonStyles();
50
+ const prefersReducedMotion = respectReducedMotion && typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
51
+ const inlineStyle = {
52
+ display: "inline-block",
53
+ background: "linear-gradient(90deg, var(--ez-skeleton-color-start, #f2f2f2) 25%, var(--ez-skeleton-color-middle, #e6e6e6) 37%, var(--ez-skeleton-color-end, #f2f2f2) 63%)",
54
+ backgroundSize: "400% 100%",
55
+ animation: animate && !prefersReducedMotion ? "var(--ez-skeleton-animation, react-ez-skeleton-pulse 1.4s ease-in-out infinite)" : "none",
56
+ borderRadius: toCssSize(radius),
57
+ width: toCssSize(width),
58
+ height: toCssSize(height),
59
+ ...style
60
+ };
61
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
62
+ "span",
63
+ {
64
+ className,
65
+ style: inlineStyle,
66
+ "aria-hidden": ariaHidden,
67
+ "data-testid": dataTestId
68
+ }
69
+ );
70
+ };
71
+ var injectSkeletonStyles = () => {
72
+ if (stylesInjected) return;
73
+ if (typeof document === "undefined") return;
74
+ const id = "react-ez-skeleton-keyframes";
75
+ if (document.getElementById(id)) {
76
+ stylesInjected = true;
77
+ return;
78
+ }
79
+ const style = document.createElement("style");
80
+ style.id = id;
81
+ style.innerHTML = `:root{--ez-skeleton-color-start:#f2f2f2;--ez-skeleton-color-middle:#e6e6e6;--ez-skeleton-color-end:#f2f2f2;--ez-skeleton-animation:react-ez-skeleton-pulse 1.4s ease-in-out infinite;}@media (prefers-reduced-motion: reduce){:root{--ez-skeleton-animation:none;}}@keyframes react-ez-skeleton-pulse{0%{background-position:100% 50%;}100%{background-position:0 50%;}}`;
82
+ document.head.appendChild(style);
83
+ stylesInjected = true;
84
+ };
85
+ var SkeletonText = ({
86
+ lines = 3,
87
+ lineHeight = 16,
88
+ gap = 8,
89
+ width = "100%",
90
+ radius = 4,
91
+ className = "",
92
+ style,
93
+ lineWidths,
94
+ randomizeLineWidths = false,
95
+ randomizeMin = 60,
96
+ randomizeMax = 100,
97
+ randomizeSeed,
98
+ animate,
99
+ respectReducedMotion,
100
+ ariaHidden,
101
+ injectStyles,
102
+ dataTestId
103
+ }) => {
104
+ const clamp = (n, min2, max2) => Math.min(max2, Math.max(min2, n));
105
+ const createRng = (seed) => {
106
+ let s = seed >>> 0;
107
+ return () => {
108
+ s = 1664525 * s + 1013904223 >>> 0;
109
+ return s / 4294967296;
110
+ };
111
+ };
112
+ const rng = randomizeLineWidths ? createRng(randomizeSeed != null ? randomizeSeed : lines) : null;
113
+ const min = clamp(randomizeMin, 0, 100);
114
+ const max = clamp(randomizeMax, min, 100);
115
+ const getLineWidth = (index) => {
116
+ if (lineWidths && lineWidths[index] !== void 0) return lineWidths[index];
117
+ if (randomizeLineWidths && rng) {
118
+ const p = min + (max - min) * rng();
119
+ return `${Math.round(p)}%`;
120
+ }
121
+ if (index === lines - 1) return width;
122
+ return "100%";
123
+ };
124
+ const items = Array.from({ length: lines });
125
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style, children: items.map((_, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
126
+ Skeleton,
127
+ {
128
+ width: getLineWidth(index),
129
+ height: lineHeight,
130
+ radius,
131
+ animate,
132
+ respectReducedMotion,
133
+ ariaHidden,
134
+ injectStyles,
135
+ dataTestId: dataTestId ? `${dataTestId}-${index}` : void 0,
136
+ style: { marginBottom: index === lines - 1 ? 0 : toCssSize(gap) }
137
+ },
138
+ index
139
+ )) });
140
+ };
141
+ var SkeletonCircle = ({ size = 40, className = "", style, animate, respectReducedMotion, ariaHidden, injectStyles, dataTestId }) => {
142
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
143
+ Skeleton,
144
+ {
145
+ width: size,
146
+ height: size,
147
+ radius: "50%",
148
+ className,
149
+ style,
150
+ animate,
151
+ respectReducedMotion,
152
+ ariaHidden,
153
+ injectStyles,
154
+ dataTestId
155
+ }
156
+ );
157
+ };
158
+ var index_default = Skeleton;
159
+ // Annotate the CommonJS export names for ESM import in node:
160
+ 0 && (module.exports = {
161
+ Skeleton,
162
+ SkeletonCircle,
163
+ SkeletonText,
164
+ injectSkeletonStyles
165
+ });
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+
3
+ type SkeletonProps = {
4
+ width?: number | string;
5
+ height?: number | string;
6
+ radius?: number | string;
7
+ className?: string;
8
+ style?: React.CSSProperties;
9
+ animate?: boolean;
10
+ respectReducedMotion?: boolean;
11
+ ariaHidden?: boolean;
12
+ injectStyles?: boolean;
13
+ dataTestId?: string;
14
+ };
15
+ declare const Skeleton: React.FC<SkeletonProps>;
16
+ declare const injectSkeletonStyles: () => void;
17
+ declare const SkeletonText: React.FC<Omit<SkeletonProps, "height"> & {
18
+ lines?: number;
19
+ lineHeight?: number | string;
20
+ gap?: number | string;
21
+ lineWidths?: Array<number | string>;
22
+ randomizeLineWidths?: boolean;
23
+ randomizeMin?: number;
24
+ randomizeMax?: number;
25
+ randomizeSeed?: number;
26
+ }>;
27
+ declare const SkeletonCircle: React.FC<Omit<SkeletonProps, "radius" | "height" | "width"> & {
28
+ size?: number | string;
29
+ }>;
30
+
31
+ export { Skeleton, SkeletonCircle, type SkeletonProps, SkeletonText, Skeleton as default, injectSkeletonStyles };
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+
3
+ type SkeletonProps = {
4
+ width?: number | string;
5
+ height?: number | string;
6
+ radius?: number | string;
7
+ className?: string;
8
+ style?: React.CSSProperties;
9
+ animate?: boolean;
10
+ respectReducedMotion?: boolean;
11
+ ariaHidden?: boolean;
12
+ injectStyles?: boolean;
13
+ dataTestId?: string;
14
+ };
15
+ declare const Skeleton: React.FC<SkeletonProps>;
16
+ declare const injectSkeletonStyles: () => void;
17
+ declare const SkeletonText: React.FC<Omit<SkeletonProps, "height"> & {
18
+ lines?: number;
19
+ lineHeight?: number | string;
20
+ gap?: number | string;
21
+ lineWidths?: Array<number | string>;
22
+ randomizeLineWidths?: boolean;
23
+ randomizeMin?: number;
24
+ randomizeMax?: number;
25
+ randomizeSeed?: number;
26
+ }>;
27
+ declare const SkeletonCircle: React.FC<Omit<SkeletonProps, "radius" | "height" | "width"> & {
28
+ size?: number | string;
29
+ }>;
30
+
31
+ export { Skeleton, SkeletonCircle, type SkeletonProps, SkeletonText, Skeleton as default, injectSkeletonStyles };
package/dist/index.js ADDED
@@ -0,0 +1,137 @@
1
+ // src/index.tsx
2
+ import { jsx } from "react/jsx-runtime";
3
+ var stylesInjected = false;
4
+ var toCssSize = (value) => {
5
+ if (value === void 0) return void 0;
6
+ if (typeof value === "number") return `${value}px`;
7
+ return value;
8
+ };
9
+ var Skeleton = ({
10
+ width = "100%",
11
+ height = 16,
12
+ radius = 4,
13
+ className = "",
14
+ style,
15
+ animate = true,
16
+ respectReducedMotion = true,
17
+ ariaHidden = true,
18
+ injectStyles = true,
19
+ dataTestId
20
+ }) => {
21
+ if (injectStyles) injectSkeletonStyles();
22
+ const prefersReducedMotion = respectReducedMotion && typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
23
+ const inlineStyle = {
24
+ display: "inline-block",
25
+ background: "linear-gradient(90deg, var(--ez-skeleton-color-start, #f2f2f2) 25%, var(--ez-skeleton-color-middle, #e6e6e6) 37%, var(--ez-skeleton-color-end, #f2f2f2) 63%)",
26
+ backgroundSize: "400% 100%",
27
+ animation: animate && !prefersReducedMotion ? "var(--ez-skeleton-animation, react-ez-skeleton-pulse 1.4s ease-in-out infinite)" : "none",
28
+ borderRadius: toCssSize(radius),
29
+ width: toCssSize(width),
30
+ height: toCssSize(height),
31
+ ...style
32
+ };
33
+ return /* @__PURE__ */ jsx(
34
+ "span",
35
+ {
36
+ className,
37
+ style: inlineStyle,
38
+ "aria-hidden": ariaHidden,
39
+ "data-testid": dataTestId
40
+ }
41
+ );
42
+ };
43
+ var injectSkeletonStyles = () => {
44
+ if (stylesInjected) return;
45
+ if (typeof document === "undefined") return;
46
+ const id = "react-ez-skeleton-keyframes";
47
+ if (document.getElementById(id)) {
48
+ stylesInjected = true;
49
+ return;
50
+ }
51
+ const style = document.createElement("style");
52
+ style.id = id;
53
+ style.innerHTML = `:root{--ez-skeleton-color-start:#f2f2f2;--ez-skeleton-color-middle:#e6e6e6;--ez-skeleton-color-end:#f2f2f2;--ez-skeleton-animation:react-ez-skeleton-pulse 1.4s ease-in-out infinite;}@media (prefers-reduced-motion: reduce){:root{--ez-skeleton-animation:none;}}@keyframes react-ez-skeleton-pulse{0%{background-position:100% 50%;}100%{background-position:0 50%;}}`;
54
+ document.head.appendChild(style);
55
+ stylesInjected = true;
56
+ };
57
+ var SkeletonText = ({
58
+ lines = 3,
59
+ lineHeight = 16,
60
+ gap = 8,
61
+ width = "100%",
62
+ radius = 4,
63
+ className = "",
64
+ style,
65
+ lineWidths,
66
+ randomizeLineWidths = false,
67
+ randomizeMin = 60,
68
+ randomizeMax = 100,
69
+ randomizeSeed,
70
+ animate,
71
+ respectReducedMotion,
72
+ ariaHidden,
73
+ injectStyles,
74
+ dataTestId
75
+ }) => {
76
+ const clamp = (n, min2, max2) => Math.min(max2, Math.max(min2, n));
77
+ const createRng = (seed) => {
78
+ let s = seed >>> 0;
79
+ return () => {
80
+ s = 1664525 * s + 1013904223 >>> 0;
81
+ return s / 4294967296;
82
+ };
83
+ };
84
+ const rng = randomizeLineWidths ? createRng(randomizeSeed != null ? randomizeSeed : lines) : null;
85
+ const min = clamp(randomizeMin, 0, 100);
86
+ const max = clamp(randomizeMax, min, 100);
87
+ const getLineWidth = (index) => {
88
+ if (lineWidths && lineWidths[index] !== void 0) return lineWidths[index];
89
+ if (randomizeLineWidths && rng) {
90
+ const p = min + (max - min) * rng();
91
+ return `${Math.round(p)}%`;
92
+ }
93
+ if (index === lines - 1) return width;
94
+ return "100%";
95
+ };
96
+ const items = Array.from({ length: lines });
97
+ return /* @__PURE__ */ jsx("div", { className, style, children: items.map((_, index) => /* @__PURE__ */ jsx(
98
+ Skeleton,
99
+ {
100
+ width: getLineWidth(index),
101
+ height: lineHeight,
102
+ radius,
103
+ animate,
104
+ respectReducedMotion,
105
+ ariaHidden,
106
+ injectStyles,
107
+ dataTestId: dataTestId ? `${dataTestId}-${index}` : void 0,
108
+ style: { marginBottom: index === lines - 1 ? 0 : toCssSize(gap) }
109
+ },
110
+ index
111
+ )) });
112
+ };
113
+ var SkeletonCircle = ({ size = 40, className = "", style, animate, respectReducedMotion, ariaHidden, injectStyles, dataTestId }) => {
114
+ return /* @__PURE__ */ jsx(
115
+ Skeleton,
116
+ {
117
+ width: size,
118
+ height: size,
119
+ radius: "50%",
120
+ className,
121
+ style,
122
+ animate,
123
+ respectReducedMotion,
124
+ ariaHidden,
125
+ injectStyles,
126
+ dataTestId
127
+ }
128
+ );
129
+ };
130
+ var index_default = Skeleton;
131
+ export {
132
+ Skeleton,
133
+ SkeletonCircle,
134
+ SkeletonText,
135
+ index_default as default,
136
+ injectSkeletonStyles
137
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "react-ez-skeleton",
3
+ "version": "1.0.0",
4
+ "description": "react-ez-skeleton provides simple, flexible skeleton components for React applications, enabling fast setup and smooth loading states without unnecessary complexity.",
5
+ "license": "MIT",
6
+ "author": "Md. Shafiul Alam",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "sideEffects": false,
22
+ "scripts": {
23
+ "build": "tsup src/index.tsx --format cjs,esm --dts --clean",
24
+ "dev": "tsup src/index.tsx --format esm --dts --watch",
25
+ "test": "echo \"Error: no test specified\" && exit 1"
26
+ },
27
+ "peerDependencies": {
28
+ "react": "^17.0.0 || ^18.0.0",
29
+ "react-dom": "^17.0.0 || ^18.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/react": "^18.0.0",
33
+ "@types/react-dom": "^18.0.0",
34
+ "tsup": "^8.0.0",
35
+ "typescript": "^5.0.0"
36
+ }
37
+ }