@tavosud/sky-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 +357 -0
- package/dist/Skeleton.d.ts +123 -0
- package/dist/SkeletonTheme.d.ts +61 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +146 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +152 -0
- package/dist/index.js.map +1 -0
- package/dist/test/Skeleton.test.d.ts +1 -0
- package/dist/test/setup.d.ts +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# @tavosud/sky-skeleton
|
|
2
|
+
|
|
3
|
+
Lightweight, zero-dependency React skeleton loader with a GPU-accelerated CSS shimmer. Built for performance — under **1.3 KB** (ESM, minified), zero repaints.
|
|
4
|
+
|
|
5
|
+
[](#testing)
|
|
6
|
+
[](https://ko-fi.com/tavosud)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Zero runtime dependencies** — React is a peer dependency
|
|
11
|
+
- **GPU-accelerated shimmer** — uses `transform: translateX()` on a pseudo-element; zero CPU repaints
|
|
12
|
+
- **Pulse effect** — alternative `effect="pulse"` fades the skeleton in and out
|
|
13
|
+
- **CSS custom properties** — colors, speed, and border-radius are fully themeable without JavaScript
|
|
14
|
+
- **Dark mode** — automatic via `prefers-color-scheme` CSS media query
|
|
15
|
+
- **`SkeletonTheme`** — set defaults once for an entire subtree with `display: contents` (layout-safe)
|
|
16
|
+
- **Three variants** — `rect`, `circle`, `text` with multi-line paragraph support via `count`
|
|
17
|
+
- **Staggered animation** — `stagger` prop creates a wave delay across `text` lines
|
|
18
|
+
- **Circle auto-square** — omit `height` on `variant="circle"` and it mirrors `width` automatically
|
|
19
|
+
- **Off-screen pause** — `IntersectionObserver` pauses the animation when the skeleton is not visible
|
|
20
|
+
- **`React.forwardRef`** — full ref forwarding to the underlying DOM element
|
|
21
|
+
- **Semantic HTML** — render any element with the `as` prop
|
|
22
|
+
- **Reduced-motion aware** — respects `prefers-reduced-motion` natively via CSS media query
|
|
23
|
+
- **Accessible** — `role="progressbar"`, `aria-busy`, and a customisable `aria-label` out of the box
|
|
24
|
+
- **Full TypeScript API** — every prop documented with JSDoc for rich IDE autocompletion
|
|
25
|
+
- **33 unit tests** — Vitest + Testing Library
|
|
26
|
+
- **Tree-shakeable** — ESM build with `sideEffects: false`
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @tavosud/sky-skeleton
|
|
32
|
+
# or
|
|
33
|
+
yarn add @tavosud/sky-skeleton
|
|
34
|
+
# or
|
|
35
|
+
pnpm add @tavosud/sky-skeleton
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
React 17 or later is required as a peer dependency.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { Skeleton } from '@tavosud/sky-skeleton';
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Rectangle (default)
|
|
47
|
+
|
|
48
|
+
Use to represent images, banners, or any block-level element.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<Skeleton loading={true} width={320} height={180} />
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Circle
|
|
55
|
+
|
|
56
|
+
Use to represent avatars or profile pictures. When you only provide `width`, the component automatically sets `height` to the same value so you don't have to repeat yourself.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// Auto-square: height mirrors width automatically
|
|
60
|
+
<Skeleton loading={true} variant="circle" width={48} />
|
|
61
|
+
|
|
62
|
+
// Explicit height overrides auto-square when needed
|
|
63
|
+
<Skeleton loading={true} variant="circle" width={64} height={32} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Text lines
|
|
67
|
+
|
|
68
|
+
Use to represent paragraphs. `count` controls how many lines are rendered — the last one is automatically narrowed to 80% width to mimic a real paragraph ending.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// Single line
|
|
72
|
+
<Skeleton loading={true} variant="text" />
|
|
73
|
+
|
|
74
|
+
// Full paragraph (4 lines)
|
|
75
|
+
<Skeleton loading={true} variant="text" count={4} />
|
|
76
|
+
|
|
77
|
+
// Staggered "wave" — each line starts 80 ms after the previous
|
|
78
|
+
<Skeleton loading={true} variant="text" count={4} stagger={0.08} />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Conditional rendering with children
|
|
82
|
+
|
|
83
|
+
When `loading` is `false`, the component renders its `children` instead of the skeleton.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
function UserCard({ user, isLoading }) {
|
|
87
|
+
return (
|
|
88
|
+
<div>
|
|
89
|
+
<Skeleton loading={isLoading} variant="circle" width={48} height={48}>
|
|
90
|
+
<img src={user.avatar} alt={user.name} />
|
|
91
|
+
</Skeleton>
|
|
92
|
+
|
|
93
|
+
<Skeleton loading={isLoading} variant="text" count={2}>
|
|
94
|
+
<p>{user.name}</p>
|
|
95
|
+
<p>{user.bio}</p>
|
|
96
|
+
</Skeleton>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Custom styles via `className`
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<Skeleton loading={true} variant="rect" className="my-custom-skeleton" />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```css
|
|
109
|
+
/* your-styles.css */
|
|
110
|
+
.my-custom-skeleton {
|
|
111
|
+
border-radius: 12px;
|
|
112
|
+
width: 100%;
|
|
113
|
+
height: 200px;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Inline styles via `style`
|
|
118
|
+
|
|
119
|
+
For one-off overrides without a CSS class:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
<Skeleton loading={true} style={{ borderRadius: '12px', width: '100%', height: 200 }} />
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Effect — shimmer vs pulse
|
|
126
|
+
|
|
127
|
+
Choose between two built-in animation styles:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
// Default: a highlight sweeps across the surface (GPU-accelerated)
|
|
131
|
+
<Skeleton effect="shimmer" width={300} height={20} />
|
|
132
|
+
|
|
133
|
+
// Alternative: the element gently fades in and out
|
|
134
|
+
<Skeleton effect="pulse" width={300} height={20} />
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
You can also set a default effect for the entire tree via `SkeletonTheme`:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<SkeletonTheme effect="pulse">
|
|
141
|
+
<Skeleton />
|
|
142
|
+
</SkeletonTheme>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Disable animation
|
|
146
|
+
|
|
147
|
+
Pass `animate={false}` to render a static placeholder — useful in tests or when you need to manage motion yourself:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<Skeleton loading={true} variant="rect" animate={false} />
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> **Note:** the component also reads the OS-level `prefers-reduced-motion` setting via a CSS media query and disables the animation automatically — no `animate` prop needed. Additionally, skeletons that scroll out of the viewport have their animation automatically paused via `IntersectionObserver` to save GPU cycles.
|
|
154
|
+
|
|
155
|
+
### Semantic HTML via `as`
|
|
156
|
+
|
|
157
|
+
By default the skeleton renders a `<span>`. Use `as` to render the semantically correct element:
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<Skeleton as="div" height={200} /> {/* block container */}
|
|
161
|
+
<Skeleton as="p" variant="text" /> {/* paragraph */}
|
|
162
|
+
<Skeleton as="li" variant="text" count={3} /> {/* list items */}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Control animation speed
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
<Skeleton speed={2.5} /> {/* slower */}
|
|
169
|
+
<Skeleton speed={0.8} /> {/* faster */}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### borderRadius per instance
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<Skeleton borderRadius={12} height={180} /> {/* number → px */}
|
|
176
|
+
<Skeleton borderRadius="1rem" height={180} /> {/* or string */}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Custom accessible label
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
<Skeleton loading={true} variant="circle" width={48} aria-label="Loading user avatar" />
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Access the DOM element via ref
|
|
186
|
+
|
|
187
|
+
`Skeleton` is a `forwardRef` component — you can attach a `ref` to reach the underlying DOM node:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
const ref = useRef<HTMLElement>(null);
|
|
191
|
+
|
|
192
|
+
<Skeleton ref={ref} width={300} height={20} />
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
When `variant="text"` with multiple lines, the ref is forwarded to the **first** line.
|
|
196
|
+
|
|
197
|
+
## SkeletonTheme
|
|
198
|
+
|
|
199
|
+
Wrap your app or a section in `<SkeletonTheme>` to set defaults for all `<Skeleton>` children — no need to repeat props on every instance.
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
import { Skeleton, SkeletonTheme } from '@tavosud/sky-skeleton';
|
|
203
|
+
|
|
204
|
+
<SkeletonTheme baseColor="#e0e0e0" highlightColor="#f0f0f0" speed={1.8} borderRadius={8}>
|
|
205
|
+
<Skeleton height={120} />
|
|
206
|
+
<Skeleton variant="circle" width={48} height={48} />
|
|
207
|
+
<Skeleton variant="text" count={3} />
|
|
208
|
+
</SkeletonTheme>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`SkeletonTheme` injects CSS custom properties using a `display: contents` wrapper, so the theming has **zero JavaScript overhead per render** and **never breaks flex, grid, or any other layout context** — children see through it completely.
|
|
212
|
+
|
|
213
|
+
### Dark mode with SkeletonTheme
|
|
214
|
+
|
|
215
|
+
You can provide separate light/dark themes:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
219
|
+
|
|
220
|
+
<SkeletonTheme
|
|
221
|
+
baseColor={isDark ? '#2a2a2a' : '#eee'}
|
|
222
|
+
highlightColor={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.6)'}
|
|
223
|
+
>
|
|
224
|
+
<App />
|
|
225
|
+
</SkeletonTheme>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Or rely on the **automatic CSS dark mode** built into the library (no JavaScript needed — just set a CSS variable in your dark theme):
|
|
229
|
+
|
|
230
|
+
```css
|
|
231
|
+
@media (prefers-color-scheme: dark) {
|
|
232
|
+
:root {
|
|
233
|
+
--skeleton-base-color: #2a2a2a;
|
|
234
|
+
--skeleton-highlight-color: rgba(255, 255, 255, 0.08);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### SkeletonTheme Props
|
|
240
|
+
|
|
241
|
+
| Prop | Type | Default | Description |
|
|
242
|
+
|---|---|---|---|
|
|
243
|
+
| `baseColor` | `string` | `#eee` | Background color of all skeletons in the subtree. |
|
|
244
|
+
| `highlightColor` | `string` | `rgba(255,255,255,0.6)` | Shimmer highlight color. |
|
|
245
|
+
| `speed` | `number` | `1.4` | Animation duration in seconds. |
|
|
246
|
+
| `borderRadius` | `string \| number` | `4` | Border radius. Numbers treated as `px`. |
|
|
247
|
+
| `animate` | `boolean` | `true` | Disable animation for all children. |
|
|
248
|
+
| `effect` | `'shimmer' \| 'pulse'` | `'shimmer'` | Default animation effect for all children. |
|
|
249
|
+
| `variant` | `SkeletonVariant` | `'rect'` | Default variant for all children. |
|
|
250
|
+
|
|
251
|
+
## API
|
|
252
|
+
|
|
253
|
+
| Prop | Type | Default | Description |
|
|
254
|
+
| ------------- | ------------------------------ | -------------- | --------------------------------------------------------------------------- |
|
|
255
|
+
| `loading` | `boolean` | `true` | When `true`, renders the skeleton; `false` renders children. |
|
|
256
|
+
| `variant` | `'rect' \| 'circle' \| 'text'` | `'rect'` | Shape of the skeleton element. |
|
|
257
|
+
| `as` | `ElementType` | `'span'` | HTML element or React component to render as the skeleton root. |
|
|
258
|
+
| `width` | `string \| number` | — | Custom width. Numbers are treated as `px`. |
|
|
259
|
+
| `height` | `string \| number` | — | Custom height. For `circle`, omitting this mirrors `width` automatically. |
|
|
260
|
+
| `borderRadius`| `string \| number` | `4` | Border radius. Numbers treated as `px`. Overrides CSS variable. |
|
|
261
|
+
| `count` | `number` | `1` | Number of lines to render. Only applies to `variant="text"`. |
|
|
262
|
+
| `speed` | `number` | `1.4` | Duration of one animation cycle in seconds. Overrides CSS variable. |
|
|
263
|
+
| `effect` | `'shimmer' \| 'pulse'` | `'shimmer'` | Animation style. `shimmer` sweeps a highlight; `pulse` fades the element. |
|
|
264
|
+
| `stagger` | `number` | `0` | Delay in seconds between each line in `variant="text"`. Creates a wave. |
|
|
265
|
+
| `animate` | `boolean` | `true` | Enables or disables the animation. |
|
|
266
|
+
| `style` | `React.CSSProperties` | — | Inline styles merged onto the element. Takes precedence over `width`/`height` props. |
|
|
267
|
+
| `className` | `string` | — | Extra CSS class appended to the skeleton element. |
|
|
268
|
+
| `aria-label` | `string` | `"Loading..."` | Screen reader label. Override when you know the content being loaded. |
|
|
269
|
+
| `children` | `React.ReactNode` | — | Rendered when `loading` is `false`. |
|
|
270
|
+
| `ref` | `React.Ref<HTMLElement>` | — | Forwarded to the underlying DOM element (`forwardRef`). |
|
|
271
|
+
|
|
272
|
+
## Accessibility
|
|
273
|
+
|
|
274
|
+
When in loading state each skeleton element renders with:
|
|
275
|
+
|
|
276
|
+
```html
|
|
277
|
+
<span role="progressbar" aria-busy="true" aria-label="Loading..."></span>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Screen readers announce the loading state correctly with no extra configuration. Override `aria-label` when you know what content is loading:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
<Skeleton loading={true} aria-label="Loading article content" />
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The shimmer animation is automatically disabled for users who have enabled **Reduce Motion** in their OS accessibility settings, via the native `prefers-reduced-motion` CSS media query.
|
|
287
|
+
|
|
288
|
+
## Mobile support
|
|
289
|
+
|
|
290
|
+
The component is mobile-first by default:
|
|
291
|
+
|
|
292
|
+
- `max-width: 100%` prevents horizontal overflow on narrow viewports
|
|
293
|
+
- `box-sizing: border-box` ensures padding/border never break the layout
|
|
294
|
+
- `rect` and `text` variants use `display: block` so they naturally fill their container without extra CSS
|
|
295
|
+
- The shimmer runs on the GPU compositor thread (`transform: translateX()`), which is equally efficient on low-end mobile hardware
|
|
296
|
+
- `prefers-reduced-motion` is respected automatically — no JavaScript needed
|
|
297
|
+
|
|
298
|
+
For fluid layouts, omit `width` and let the skeleton fill its parent:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
// Fills 100% of the parent — works on any screen size
|
|
302
|
+
<Skeleton loading={true} height={180} />
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## CSS Custom Properties
|
|
306
|
+
|
|
307
|
+
You can override the skeleton’s visual tokens globally by setting CSS variables anywhere in your stylesheet:
|
|
308
|
+
|
|
309
|
+
```css
|
|
310
|
+
:root {
|
|
311
|
+
--skeleton-base-color: #d0d0d0;
|
|
312
|
+
--skeleton-highlight-color: rgba(255, 255, 255, 0.7);
|
|
313
|
+
--skeleton-border-radius: 8px;
|
|
314
|
+
--skeleton-animation-duration: 1.8s;
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
| Variable | Default | Description |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| `--skeleton-base-color` | `#eee` | Background fill color |
|
|
321
|
+
| `--skeleton-highlight-color` | `rgba(255,255,255,0.6)` | Shimmer sweep color |
|
|
322
|
+
| `--skeleton-border-radius` | `4px` | Corner radius for `rect` and `text` |
|
|
323
|
+
| `--skeleton-animation-duration` | `1.4s` | Duration of one animation cycle |
|
|
324
|
+
| `--skeleton-animation-delay` | `0s` | Per-element animation start delay (used internally by `stagger`) |
|
|
325
|
+
|
|
326
|
+
## Testing
|
|
327
|
+
|
|
328
|
+
The library ships with **33 unit tests** covering all props and the `SkeletonTheme` context. Run them with:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
npm test # single run
|
|
332
|
+
npm run test:watch # watch mode
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Tests are written with [Vitest](https://vitest.dev/) and [@testing-library/react](https://testing-library.com/).
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
The shimmer is implemented as a `::after` pseudo-element animated exclusively with `transform: translateX()`. This property is handled entirely by the browser's compositor thread — it never triggers layout or paint, keeping the main thread free and CPU usage near zero regardless of how many skeletons are on screen.
|
|
339
|
+
|
|
340
|
+
`will-change: transform` is set on the pseudo-element so the browser promotes it to its own GPU layer before the animation starts.
|
|
341
|
+
|
|
342
|
+
## Bundle size
|
|
343
|
+
|
|
344
|
+
| Format | Size |
|
|
345
|
+
| ------ | ------- |
|
|
346
|
+
| ESM | ~1.2 KB |
|
|
347
|
+
| CJS | ~1.3 KB |
|
|
348
|
+
|
|
349
|
+
Measured after minification. CSS is injected inline — no separate stylesheet to load.
|
|
350
|
+
|
|
351
|
+
## Browser support
|
|
352
|
+
|
|
353
|
+
All modern browsers. Uses `@keyframes`, `transform`, `::after`, and `linear-gradient` — universally supported since IE 10+.
|
|
354
|
+
|
|
355
|
+
## License
|
|
356
|
+
|
|
357
|
+
MIT © [tavosud](https://github.com/tavosud)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { CSSProperties, ElementType } from 'react';
|
|
2
|
+
import './Skeleton.css';
|
|
3
|
+
/**
|
|
4
|
+
* Shape variant of the skeleton placeholder.
|
|
5
|
+
* - `'rect'` — rectangular block (cards, images, banners)
|
|
6
|
+
* - `'circle'` — circular shape (avatars, profile pictures)
|
|
7
|
+
* - `'text'` — narrow text line; combine with `count` for paragraphs
|
|
8
|
+
*/
|
|
9
|
+
export type SkeletonVariant = 'rect' | 'circle' | 'text';
|
|
10
|
+
/**
|
|
11
|
+
* Visual animation style applied to the skeleton.
|
|
12
|
+
* - `'shimmer'` — an animated highlight sweeps across the surface (GPU-accelerated, default)
|
|
13
|
+
* - `'pulse'` — the element gently fades in and out
|
|
14
|
+
*/
|
|
15
|
+
export type SkeletonEffect = 'shimmer' | 'pulse';
|
|
16
|
+
export interface SkeletonProps {
|
|
17
|
+
/**
|
|
18
|
+
* Controls whether the skeleton or the actual content is displayed.
|
|
19
|
+
* When `false`, the component renders its `children` unchanged.
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
loading?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Shape of the skeleton placeholder.
|
|
25
|
+
* @default 'rect'
|
|
26
|
+
*/
|
|
27
|
+
variant?: SkeletonVariant;
|
|
28
|
+
/**
|
|
29
|
+
* HTML element or React component to render as the skeleton root.
|
|
30
|
+
* Use this to keep the DOM semantically correct.
|
|
31
|
+
* @example `as="div"` | `as="li"` | `as="p"`
|
|
32
|
+
* @default 'span'
|
|
33
|
+
*/
|
|
34
|
+
as?: ElementType;
|
|
35
|
+
/**
|
|
36
|
+
* Width of the skeleton element.
|
|
37
|
+
* - Numbers are converted to pixels: `200` → `"200px"`
|
|
38
|
+
* - Strings are used as-is: `"50%"`, `"12rem"`
|
|
39
|
+
*/
|
|
40
|
+
width?: string | number;
|
|
41
|
+
/**
|
|
42
|
+
* Height of the skeleton element.
|
|
43
|
+
* - Numbers are converted to pixels: `20` → `"20px"`
|
|
44
|
+
* - Strings are used as-is: `"1.5rem"`, `"auto"`
|
|
45
|
+
* - For `variant="circle"`, omitting `height` automatically copies `width`.
|
|
46
|
+
*/
|
|
47
|
+
height?: string | number;
|
|
48
|
+
/**
|
|
49
|
+
* Border radius of the skeleton element.
|
|
50
|
+
* - Numbers are converted to pixels: `8` → `"8px"`
|
|
51
|
+
* - Strings are used as-is: `"50%"`, `"1rem"`
|
|
52
|
+
* - Overrides the `--skeleton-border-radius` CSS variable for this instance.
|
|
53
|
+
*/
|
|
54
|
+
borderRadius?: string | number;
|
|
55
|
+
/**
|
|
56
|
+
* Number of skeleton lines to render. Applies whenever `variant="text"`.
|
|
57
|
+
* - `count={1}` renders a single line (default)
|
|
58
|
+
* - `count={3}` renders three lines, with the last at 80% width to mimic
|
|
59
|
+
* a real paragraph ending
|
|
60
|
+
* @default 1
|
|
61
|
+
*/
|
|
62
|
+
count?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Duration of one animation cycle in seconds.
|
|
65
|
+
* Overrides the `--skeleton-animation-duration` CSS variable for this instance.
|
|
66
|
+
* @example `speed={2}` for a slower, subtler animation
|
|
67
|
+
* @default 1.4
|
|
68
|
+
*/
|
|
69
|
+
speed?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Visual effect for the skeleton animation.
|
|
72
|
+
* - `'shimmer'` — a highlight sweeps across the surface (GPU-accelerated, default)
|
|
73
|
+
* - `'pulse'` — the element fades in and out
|
|
74
|
+
* @default 'shimmer'
|
|
75
|
+
*/
|
|
76
|
+
effect?: SkeletonEffect;
|
|
77
|
+
/**
|
|
78
|
+
* Animation-delay increment (in seconds) between each line when
|
|
79
|
+
* `variant="text"` and `count > 1`. Creates a staggered wave effect.
|
|
80
|
+
* @example `stagger={0.1}` → delays of 0s, 0.1s, 0.2s … per line
|
|
81
|
+
* @default 0
|
|
82
|
+
*/
|
|
83
|
+
stagger?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Enables or disables the skeleton animation.
|
|
86
|
+
* Set to `false` to render a static placeholder.
|
|
87
|
+
* @default true
|
|
88
|
+
*/
|
|
89
|
+
animate?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Additional CSS class name appended to the skeleton element.
|
|
92
|
+
*/
|
|
93
|
+
className?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Inline styles applied directly to the skeleton element.
|
|
96
|
+
* Values provided here take precedence over `width`/`height` props.
|
|
97
|
+
*/
|
|
98
|
+
style?: CSSProperties;
|
|
99
|
+
/**
|
|
100
|
+
* Accessible label read by screen readers while the skeleton is visible.
|
|
101
|
+
* @default "Loading..."
|
|
102
|
+
*/
|
|
103
|
+
'aria-label'?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Content rendered when `loading` is `false`.
|
|
106
|
+
*/
|
|
107
|
+
children?: React.ReactNode;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Animated content placeholder shown while data is loading.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // Single rect (default)
|
|
114
|
+
* <Skeleton width={300} height={20} />
|
|
115
|
+
*
|
|
116
|
+
* // Paragraph of 3 text lines
|
|
117
|
+
* <Skeleton variant="text" count={3} stagger={0.08} />
|
|
118
|
+
*
|
|
119
|
+
* // Avatar circle
|
|
120
|
+
* <Skeleton variant="circle" width={48} />
|
|
121
|
+
*/
|
|
122
|
+
export declare const Skeleton: React.ForwardRefExoticComponent<SkeletonProps & React.RefAttributes<HTMLElement>>;
|
|
123
|
+
export default Skeleton;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SkeletonVariant, SkeletonEffect } from './Skeleton';
|
|
3
|
+
export interface SkeletonThemeContextValue {
|
|
4
|
+
/**
|
|
5
|
+
* Base background color of the skeleton.
|
|
6
|
+
* Overrides the `--skeleton-base-color` CSS variable.
|
|
7
|
+
*/
|
|
8
|
+
baseColor?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Color of the shimmer highlight.
|
|
11
|
+
* Overrides the `--skeleton-highlight-color` CSS variable.
|
|
12
|
+
*/
|
|
13
|
+
highlightColor?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Duration of one animation cycle in seconds.
|
|
16
|
+
* @default 1.4
|
|
17
|
+
*/
|
|
18
|
+
speed?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Border radius applied to `rect` and `text` variants.
|
|
21
|
+
* Numbers are treated as `px`.
|
|
22
|
+
* @default 4
|
|
23
|
+
*/
|
|
24
|
+
borderRadius?: string | number;
|
|
25
|
+
/**
|
|
26
|
+
* When `false`, all skeletons inside the theme render without animation.
|
|
27
|
+
* @default true
|
|
28
|
+
*/
|
|
29
|
+
animate?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Default animation effect for all Skeleton children.
|
|
32
|
+
* @default 'shimmer'
|
|
33
|
+
*/
|
|
34
|
+
effect?: SkeletonEffect;
|
|
35
|
+
/**
|
|
36
|
+
* Default variant for all Skeleton children.
|
|
37
|
+
* @default 'rect'
|
|
38
|
+
*/
|
|
39
|
+
variant?: SkeletonVariant;
|
|
40
|
+
}
|
|
41
|
+
/** Read the nearest SkeletonTheme context. */
|
|
42
|
+
export declare function useSkeletonTheme(): SkeletonThemeContextValue;
|
|
43
|
+
export interface SkeletonThemeProps extends SkeletonThemeContextValue {
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Sets default values for all `<Skeleton>` components in its subtree.
|
|
48
|
+
* Also injects CSS custom properties so colors and speed are applied
|
|
49
|
+
* without any JavaScript per-instance overhead.
|
|
50
|
+
*
|
|
51
|
+
* Uses `display: contents` on the CSS-variable wrapper so it never
|
|
52
|
+
* breaks flex, grid, or any other layout context.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* <SkeletonTheme baseColor="#e0e0e0" highlightColor="#f5f5f5" speed={1.8}>
|
|
56
|
+
* <Skeleton />
|
|
57
|
+
* <Skeleton variant="circle" width={48} />
|
|
58
|
+
* </SkeletonTheme>
|
|
59
|
+
*/
|
|
60
|
+
export declare function SkeletonTheme({ children, baseColor, highlightColor, speed, borderRadius, animate, effect, variant, }: SkeletonThemeProps): React.JSX.Element;
|
|
61
|
+
export default SkeletonTheme;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React, { createContext, useContext, forwardRef, useRef, useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const SkeletonThemeContext = createContext({});
|
|
4
|
+
/** Read the nearest SkeletonTheme context. */
|
|
5
|
+
function useSkeletonTheme() {
|
|
6
|
+
return useContext(SkeletonThemeContext);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Sets default values for all `<Skeleton>` components in its subtree.
|
|
10
|
+
* Also injects CSS custom properties so colors and speed are applied
|
|
11
|
+
* without any JavaScript per-instance overhead.
|
|
12
|
+
*
|
|
13
|
+
* Uses `display: contents` on the CSS-variable wrapper so it never
|
|
14
|
+
* breaks flex, grid, or any other layout context.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <SkeletonTheme baseColor="#e0e0e0" highlightColor="#f5f5f5" speed={1.8}>
|
|
18
|
+
* <Skeleton />
|
|
19
|
+
* <Skeleton variant="circle" width={48} />
|
|
20
|
+
* </SkeletonTheme>
|
|
21
|
+
*/
|
|
22
|
+
function SkeletonTheme({ children, baseColor, highlightColor, speed, borderRadius, animate, effect, variant, }) {
|
|
23
|
+
const cssVars = {};
|
|
24
|
+
if (baseColor !== undefined) {
|
|
25
|
+
cssVars['--skeleton-base-color'] = baseColor;
|
|
26
|
+
}
|
|
27
|
+
if (highlightColor !== undefined) {
|
|
28
|
+
cssVars['--skeleton-highlight-color'] = highlightColor;
|
|
29
|
+
}
|
|
30
|
+
if (speed !== undefined) {
|
|
31
|
+
cssVars['--skeleton-animation-duration'] = `${speed}s`;
|
|
32
|
+
}
|
|
33
|
+
if (borderRadius !== undefined) {
|
|
34
|
+
const r = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;
|
|
35
|
+
cssVars['--skeleton-border-radius'] = r;
|
|
36
|
+
}
|
|
37
|
+
const hasCssVars = Object.keys(cssVars).length > 0;
|
|
38
|
+
return (React.createElement(SkeletonThemeContext.Provider, { value: { baseColor, highlightColor, speed, borderRadius, animate, effect, variant } }, hasCssVars ? (
|
|
39
|
+
// display:contents makes the wrapper invisible to layout (flex/grid),
|
|
40
|
+
// while still propagating inherited CSS custom properties.
|
|
41
|
+
React.createElement("div", { style: Object.assign(Object.assign({}, cssVars), { display: 'contents' }) }, children)) : (React.createElement(React.Fragment, null, children))));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Merges multiple refs into a single callback ref. */
|
|
45
|
+
function mergeRefs(...refs) {
|
|
46
|
+
return (value) => {
|
|
47
|
+
refs.forEach((ref) => {
|
|
48
|
+
if (typeof ref === 'function') {
|
|
49
|
+
ref(value);
|
|
50
|
+
}
|
|
51
|
+
else if (ref != null) {
|
|
52
|
+
ref.current = value;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Returns `true` while the element is intersecting the viewport; pauses shimmer when hidden. */
|
|
58
|
+
function useIsVisible(ref) {
|
|
59
|
+
const [visible, setVisible] = useState(true);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const el = ref.current;
|
|
62
|
+
if (!el || typeof IntersectionObserver === 'undefined')
|
|
63
|
+
return;
|
|
64
|
+
const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting), { threshold: 0 });
|
|
65
|
+
observer.observe(el);
|
|
66
|
+
return () => observer.disconnect();
|
|
67
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
+
}, []);
|
|
69
|
+
return visible;
|
|
70
|
+
}
|
|
71
|
+
function normalizeSize(value) {
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
75
|
+
}
|
|
76
|
+
const SkeletonItem = forwardRef(function SkeletonItem({ variant = 'rect', as: Tag = 'span', width, height, borderRadius, speed, delay, effect = 'shimmer', className, style: styleProp, animate = true, 'aria-label': ariaLabel = 'Loading...', }, forwardedRef) {
|
|
77
|
+
const innerRef = useRef(null);
|
|
78
|
+
const isVisible = useIsVisible(innerRef);
|
|
79
|
+
// Auto-square circle: when only width is given, mirror it as height
|
|
80
|
+
const resolvedHeight = variant === 'circle' && height === undefined && width !== undefined ? width : height;
|
|
81
|
+
const cssVars = {};
|
|
82
|
+
if (speed !== undefined) {
|
|
83
|
+
cssVars['--skeleton-animation-duration'] = `${speed}s`;
|
|
84
|
+
}
|
|
85
|
+
if (borderRadius !== undefined) {
|
|
86
|
+
cssVars['--skeleton-border-radius'] =
|
|
87
|
+
typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;
|
|
88
|
+
}
|
|
89
|
+
if (delay !== undefined && delay > 0) {
|
|
90
|
+
cssVars['--skeleton-animation-delay'] = `${delay}s`;
|
|
91
|
+
}
|
|
92
|
+
const style = Object.assign(Object.assign({ width: normalizeSize(width), height: normalizeSize(resolvedHeight) }, cssVars), styleProp);
|
|
93
|
+
const classes = [
|
|
94
|
+
'skeleton',
|
|
95
|
+
`skeleton--${variant}`,
|
|
96
|
+
`skeleton--${effect}`,
|
|
97
|
+
!animate && 'skeleton--no-animate',
|
|
98
|
+
animate && !isVisible && 'skeleton--paused',
|
|
99
|
+
className,
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(' ');
|
|
103
|
+
const El = Tag;
|
|
104
|
+
return (React.createElement(El, { ref: mergeRefs(innerRef, forwardedRef), role: "progressbar", "aria-busy": "true", "aria-label": ariaLabel, className: classes, style: style }));
|
|
105
|
+
});
|
|
106
|
+
/**
|
|
107
|
+
* Animated content placeholder shown while data is loading.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* // Single rect (default)
|
|
111
|
+
* <Skeleton width={300} height={20} />
|
|
112
|
+
*
|
|
113
|
+
* // Paragraph of 3 text lines
|
|
114
|
+
* <Skeleton variant="text" count={3} stagger={0.08} />
|
|
115
|
+
*
|
|
116
|
+
* // Avatar circle
|
|
117
|
+
* <Skeleton variant="circle" width={48} />
|
|
118
|
+
*/
|
|
119
|
+
const Skeleton = forwardRef(function Skeleton(props, ref) {
|
|
120
|
+
var _a, _b, _c;
|
|
121
|
+
const theme = useSkeletonTheme();
|
|
122
|
+
const { loading = true, variant = (_a = theme.variant) !== null && _a !== void 0 ? _a : 'rect', as, width, height, borderRadius = theme.borderRadius, count = 1, speed = theme.speed, effect = (_b = theme.effect) !== null && _b !== void 0 ? _b : 'shimmer', stagger = 0, animate = (_c = theme.animate) !== null && _c !== void 0 ? _c : true, className, style, 'aria-label': ariaLabel, children, } = props;
|
|
123
|
+
if (!loading) {
|
|
124
|
+
return React.createElement(React.Fragment, null, children);
|
|
125
|
+
}
|
|
126
|
+
const itemProps = {
|
|
127
|
+
variant,
|
|
128
|
+
as,
|
|
129
|
+
width,
|
|
130
|
+
height,
|
|
131
|
+
borderRadius,
|
|
132
|
+
speed,
|
|
133
|
+
effect,
|
|
134
|
+
animate,
|
|
135
|
+
className,
|
|
136
|
+
style,
|
|
137
|
+
'aria-label': ariaLabel,
|
|
138
|
+
};
|
|
139
|
+
if (variant === 'text') {
|
|
140
|
+
return (React.createElement(React.Fragment, null, Array.from({ length: count }, (_, i) => (React.createElement(SkeletonItem, Object.assign({ key: i }, itemProps, { variant: "text", delay: stagger > 0 ? i * stagger : undefined, ref: i === 0 ? ref : undefined }))))));
|
|
141
|
+
}
|
|
142
|
+
return React.createElement(SkeletonItem, Object.assign({}, itemProps, { ref: ref }));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export { Skeleton, SkeletonTheme, Skeleton as default };
|
|
146
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/SkeletonTheme.tsx","../src/Skeleton.tsx"],"sourcesContent":["import React, { createContext, useContext, CSSProperties } from 'react';\r\nimport { SkeletonVariant, SkeletonEffect } from './Skeleton';\r\n\r\nexport interface SkeletonThemeContextValue {\r\n /**\r\n * Base background color of the skeleton.\r\n * Overrides the `--skeleton-base-color` CSS variable.\r\n */\r\n baseColor?: string;\r\n\r\n /**\r\n * Color of the shimmer highlight.\r\n * Overrides the `--skeleton-highlight-color` CSS variable.\r\n */\r\n highlightColor?: string;\r\n\r\n /**\r\n * Duration of one animation cycle in seconds.\r\n * @default 1.4\r\n */\r\n speed?: number;\r\n\r\n /**\r\n * Border radius applied to `rect` and `text` variants.\r\n * Numbers are treated as `px`.\r\n * @default 4\r\n */\r\n borderRadius?: string | number;\r\n\r\n /**\r\n * When `false`, all skeletons inside the theme render without animation.\r\n * @default true\r\n */\r\n animate?: boolean;\r\n\r\n /**\r\n * Default animation effect for all Skeleton children.\r\n * @default 'shimmer'\r\n */\r\n effect?: SkeletonEffect;\r\n\r\n /**\r\n * Default variant for all Skeleton children.\r\n * @default 'rect'\r\n */\r\n variant?: SkeletonVariant;\r\n}\r\n\r\nconst SkeletonThemeContext = createContext<SkeletonThemeContextValue>({});\r\n\r\n/** Read the nearest SkeletonTheme context. */\r\nexport function useSkeletonTheme(): SkeletonThemeContextValue {\r\n return useContext(SkeletonThemeContext);\r\n}\r\n\r\nexport interface SkeletonThemeProps extends SkeletonThemeContextValue {\r\n children: React.ReactNode;\r\n}\r\n\r\n/**\r\n * Sets default values for all `<Skeleton>` components in its subtree.\r\n * Also injects CSS custom properties so colors and speed are applied\r\n * without any JavaScript per-instance overhead.\r\n *\r\n * Uses `display: contents` on the CSS-variable wrapper so it never\r\n * breaks flex, grid, or any other layout context.\r\n *\r\n * @example\r\n * <SkeletonTheme baseColor=\"#e0e0e0\" highlightColor=\"#f5f5f5\" speed={1.8}>\r\n * <Skeleton />\r\n * <Skeleton variant=\"circle\" width={48} />\r\n * </SkeletonTheme>\r\n */\r\nexport function SkeletonTheme({\r\n children,\r\n baseColor,\r\n highlightColor,\r\n speed,\r\n borderRadius,\r\n animate,\r\n effect,\r\n variant,\r\n}: SkeletonThemeProps) {\r\n const cssVars: CSSProperties = {};\r\n\r\n if (baseColor !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-base-color'] = baseColor;\r\n }\r\n if (highlightColor !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-highlight-color'] = highlightColor;\r\n }\r\n if (speed !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-animation-duration'] = `${speed}s`;\r\n }\r\n if (borderRadius !== undefined) {\r\n const r = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;\r\n (cssVars as Record<string, string>)['--skeleton-border-radius'] = r;\r\n }\r\n\r\n const hasCssVars = Object.keys(cssVars).length > 0;\r\n\r\n return (\r\n <SkeletonThemeContext.Provider value={{ baseColor, highlightColor, speed, borderRadius, animate, effect, variant }}>\r\n {hasCssVars ? (\r\n // display:contents makes the wrapper invisible to layout (flex/grid),\r\n // while still propagating inherited CSS custom properties.\r\n <div style={{ ...cssVars, display: 'contents' }}>\r\n {children}\r\n </div>\r\n ) : (\r\n <>{children}</>\r\n )}\r\n </SkeletonThemeContext.Provider>\r\n );\r\n}\r\n\r\nexport default SkeletonTheme;\r\n","import React, { CSSProperties, ElementType, forwardRef, useEffect, useRef, useState } from 'react';\r\nimport './Skeleton.css';\r\nimport { useSkeletonTheme } from './SkeletonTheme';\r\n\r\n/**\r\n * Shape variant of the skeleton placeholder.\r\n * - `'rect'` — rectangular block (cards, images, banners)\r\n * - `'circle'` — circular shape (avatars, profile pictures)\r\n * - `'text'` — narrow text line; combine with `count` for paragraphs\r\n */\r\nexport type SkeletonVariant = 'rect' | 'circle' | 'text';\r\n\r\n/**\r\n * Visual animation style applied to the skeleton.\r\n * - `'shimmer'` — an animated highlight sweeps across the surface (GPU-accelerated, default)\r\n * - `'pulse'` — the element gently fades in and out\r\n */\r\nexport type SkeletonEffect = 'shimmer' | 'pulse';\r\n\r\n/** Merges multiple refs into a single callback ref. */\r\nfunction mergeRefs<T>(...refs: (React.Ref<T> | null | undefined)[]): React.RefCallback<T> {\r\n return (value) => {\r\n refs.forEach((ref) => {\r\n if (typeof ref === 'function') {\r\n ref(value);\r\n } else if (ref != null) {\r\n (ref as { current: T | null }).current = value;\r\n }\r\n });\r\n };\r\n}\r\n\r\n/** Returns `true` while the element is intersecting the viewport; pauses shimmer when hidden. */\r\nfunction useIsVisible(ref: React.RefObject<HTMLElement>): boolean {\r\n const [visible, setVisible] = useState(true);\r\n useEffect(() => {\r\n const el = ref.current;\r\n if (!el || typeof IntersectionObserver === 'undefined') return;\r\n const observer = new IntersectionObserver(\r\n ([entry]) => setVisible(entry.isIntersecting),\r\n { threshold: 0 }\r\n );\r\n observer.observe(el);\r\n return () => observer.disconnect();\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, []);\r\n return visible;\r\n}\r\n\r\nexport interface SkeletonProps {\r\n /**\r\n * Controls whether the skeleton or the actual content is displayed.\r\n * When `false`, the component renders its `children` unchanged.\r\n * @default true\r\n */\r\n loading?: boolean;\r\n\r\n /**\r\n * Shape of the skeleton placeholder.\r\n * @default 'rect'\r\n */\r\n variant?: SkeletonVariant;\r\n\r\n /**\r\n * HTML element or React component to render as the skeleton root.\r\n * Use this to keep the DOM semantically correct.\r\n * @example `as=\"div\"` | `as=\"li\"` | `as=\"p\"`\r\n * @default 'span'\r\n */\r\n as?: ElementType;\r\n\r\n /**\r\n * Width of the skeleton element.\r\n * - Numbers are converted to pixels: `200` → `\"200px\"`\r\n * - Strings are used as-is: `\"50%\"`, `\"12rem\"`\r\n */\r\n width?: string | number;\r\n\r\n /**\r\n * Height of the skeleton element.\r\n * - Numbers are converted to pixels: `20` → `\"20px\"`\r\n * - Strings are used as-is: `\"1.5rem\"`, `\"auto\"`\r\n * - For `variant=\"circle\"`, omitting `height` automatically copies `width`.\r\n */\r\n height?: string | number;\r\n\r\n /**\r\n * Border radius of the skeleton element.\r\n * - Numbers are converted to pixels: `8` → `\"8px\"`\r\n * - Strings are used as-is: `\"50%\"`, `\"1rem\"`\r\n * - Overrides the `--skeleton-border-radius` CSS variable for this instance.\r\n */\r\n borderRadius?: string | number;\r\n\r\n /**\r\n * Number of skeleton lines to render. Applies whenever `variant=\"text\"`.\r\n * - `count={1}` renders a single line (default)\r\n * - `count={3}` renders three lines, with the last at 80% width to mimic\r\n * a real paragraph ending\r\n * @default 1\r\n */\r\n count?: number;\r\n\r\n /**\r\n * Duration of one animation cycle in seconds.\r\n * Overrides the `--skeleton-animation-duration` CSS variable for this instance.\r\n * @example `speed={2}` for a slower, subtler animation\r\n * @default 1.4\r\n */\r\n speed?: number;\r\n\r\n /**\r\n * Visual effect for the skeleton animation.\r\n * - `'shimmer'` — a highlight sweeps across the surface (GPU-accelerated, default)\r\n * - `'pulse'` — the element fades in and out\r\n * @default 'shimmer'\r\n */\r\n effect?: SkeletonEffect;\r\n\r\n /**\r\n * Animation-delay increment (in seconds) between each line when\r\n * `variant=\"text\"` and `count > 1`. Creates a staggered wave effect.\r\n * @example `stagger={0.1}` → delays of 0s, 0.1s, 0.2s … per line\r\n * @default 0\r\n */\r\n stagger?: number;\r\n\r\n /**\r\n * Enables or disables the skeleton animation.\r\n * Set to `false` to render a static placeholder.\r\n * @default true\r\n */\r\n animate?: boolean;\r\n\r\n /**\r\n * Additional CSS class name appended to the skeleton element.\r\n */\r\n className?: string;\r\n\r\n /**\r\n * Inline styles applied directly to the skeleton element.\r\n * Values provided here take precedence over `width`/`height` props.\r\n */\r\n style?: CSSProperties;\r\n\r\n /**\r\n * Accessible label read by screen readers while the skeleton is visible.\r\n * @default \"Loading...\"\r\n */\r\n 'aria-label'?: string;\r\n\r\n /**\r\n * Content rendered when `loading` is `false`.\r\n */\r\n children?: React.ReactNode;\r\n}\r\n\r\nfunction normalizeSize(value: string | number | undefined): string | undefined {\r\n if (value === undefined) return undefined;\r\n return typeof value === 'number' ? `${value}px` : value;\r\n}\r\n\r\ntype SkeletonItemProps = Required<Pick<SkeletonProps, 'variant'>> &\r\n Pick<SkeletonProps, 'as' | 'width' | 'height' | 'borderRadius' | 'speed' | 'effect' | 'className' | 'style' | 'animate' | 'aria-label'> & {\r\n /** Per-item animation-delay in seconds (stagger effect). */\r\n delay?: number;\r\n };\r\n\r\nconst SkeletonItem = forwardRef<HTMLElement, SkeletonItemProps>(function SkeletonItem({\r\n variant = 'rect',\r\n as: Tag = 'span',\r\n width,\r\n height,\r\n borderRadius,\r\n speed,\r\n delay,\r\n effect = 'shimmer',\r\n className,\r\n style: styleProp,\r\n animate = true,\r\n 'aria-label': ariaLabel = 'Loading...',\r\n}, forwardedRef) {\r\n const innerRef = useRef<HTMLElement>(null);\r\n const isVisible = useIsVisible(innerRef);\r\n\r\n // Auto-square circle: when only width is given, mirror it as height\r\n const resolvedHeight =\r\n variant === 'circle' && height === undefined && width !== undefined ? width : height;\r\n\r\n const cssVars: Record<string, string> = {};\r\n if (speed !== undefined) {\r\n cssVars['--skeleton-animation-duration'] = `${speed}s`;\r\n }\r\n if (borderRadius !== undefined) {\r\n cssVars['--skeleton-border-radius'] =\r\n typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;\r\n }\r\n if (delay !== undefined && delay > 0) {\r\n cssVars['--skeleton-animation-delay'] = `${delay}s`;\r\n }\r\n\r\n const style: CSSProperties = {\r\n width: normalizeSize(width),\r\n height: normalizeSize(resolvedHeight),\r\n ...cssVars,\r\n ...styleProp,\r\n };\r\n\r\n const classes = [\r\n 'skeleton',\r\n `skeleton--${variant}`,\r\n `skeleton--${effect}`,\r\n !animate && 'skeleton--no-animate',\r\n animate && !isVisible && 'skeleton--paused',\r\n className,\r\n ]\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const El: any = Tag;\r\n return (\r\n <El\r\n ref={mergeRefs(innerRef, forwardedRef)}\r\n role=\"progressbar\"\r\n aria-busy=\"true\"\r\n aria-label={ariaLabel}\r\n className={classes}\r\n style={style}\r\n />\r\n );\r\n});\r\n\r\n/**\r\n * Animated content placeholder shown while data is loading.\r\n *\r\n * @example\r\n * // Single rect (default)\r\n * <Skeleton width={300} height={20} />\r\n *\r\n * // Paragraph of 3 text lines\r\n * <Skeleton variant=\"text\" count={3} stagger={0.08} />\r\n *\r\n * // Avatar circle\r\n * <Skeleton variant=\"circle\" width={48} />\r\n */\r\nexport const Skeleton = forwardRef<HTMLElement, SkeletonProps>(function Skeleton(props, ref) {\r\n const theme = useSkeletonTheme();\r\n\r\n const {\r\n loading = true,\r\n variant = theme.variant ?? 'rect',\r\n as,\r\n width,\r\n height,\r\n borderRadius = theme.borderRadius,\r\n count = 1,\r\n speed = theme.speed,\r\n effect = theme.effect ?? 'shimmer',\r\n stagger = 0,\r\n animate = theme.animate ?? true,\r\n className,\r\n style,\r\n 'aria-label': ariaLabel,\r\n children,\r\n } = props;\r\n\r\n if (!loading) {\r\n return <>{children}</>;\r\n }\r\n\r\n const itemProps: Omit<SkeletonItemProps, 'delay'> = {\r\n variant,\r\n as,\r\n width,\r\n height,\r\n borderRadius,\r\n speed,\r\n effect,\r\n animate,\r\n className,\r\n style,\r\n 'aria-label': ariaLabel,\r\n };\r\n\r\n if (variant === 'text') {\r\n return (\r\n <>\r\n {Array.from({ length: count }, (_, i) => (\r\n <SkeletonItem\r\n key={i}\r\n {...itemProps}\r\n variant=\"text\"\r\n delay={stagger > 0 ? i * stagger : undefined}\r\n ref={i === 0 ? ref : undefined}\r\n />\r\n ))}\r\n </>\r\n );\r\n }\r\n\r\n return <SkeletonItem {...itemProps} ref={ref} />;\r\n});\r\n\r\nexport default Skeleton;\r\n\r\n"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var React = require('react');
|
|
6
|
+
|
|
7
|
+
const SkeletonThemeContext = React.createContext({});
|
|
8
|
+
/** Read the nearest SkeletonTheme context. */
|
|
9
|
+
function useSkeletonTheme() {
|
|
10
|
+
return React.useContext(SkeletonThemeContext);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Sets default values for all `<Skeleton>` components in its subtree.
|
|
14
|
+
* Also injects CSS custom properties so colors and speed are applied
|
|
15
|
+
* without any JavaScript per-instance overhead.
|
|
16
|
+
*
|
|
17
|
+
* Uses `display: contents` on the CSS-variable wrapper so it never
|
|
18
|
+
* breaks flex, grid, or any other layout context.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <SkeletonTheme baseColor="#e0e0e0" highlightColor="#f5f5f5" speed={1.8}>
|
|
22
|
+
* <Skeleton />
|
|
23
|
+
* <Skeleton variant="circle" width={48} />
|
|
24
|
+
* </SkeletonTheme>
|
|
25
|
+
*/
|
|
26
|
+
function SkeletonTheme({ children, baseColor, highlightColor, speed, borderRadius, animate, effect, variant, }) {
|
|
27
|
+
const cssVars = {};
|
|
28
|
+
if (baseColor !== undefined) {
|
|
29
|
+
cssVars['--skeleton-base-color'] = baseColor;
|
|
30
|
+
}
|
|
31
|
+
if (highlightColor !== undefined) {
|
|
32
|
+
cssVars['--skeleton-highlight-color'] = highlightColor;
|
|
33
|
+
}
|
|
34
|
+
if (speed !== undefined) {
|
|
35
|
+
cssVars['--skeleton-animation-duration'] = `${speed}s`;
|
|
36
|
+
}
|
|
37
|
+
if (borderRadius !== undefined) {
|
|
38
|
+
const r = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;
|
|
39
|
+
cssVars['--skeleton-border-radius'] = r;
|
|
40
|
+
}
|
|
41
|
+
const hasCssVars = Object.keys(cssVars).length > 0;
|
|
42
|
+
return (React.createElement(SkeletonThemeContext.Provider, { value: { baseColor, highlightColor, speed, borderRadius, animate, effect, variant } }, hasCssVars ? (
|
|
43
|
+
// display:contents makes the wrapper invisible to layout (flex/grid),
|
|
44
|
+
// while still propagating inherited CSS custom properties.
|
|
45
|
+
React.createElement("div", { style: Object.assign(Object.assign({}, cssVars), { display: 'contents' }) }, children)) : (React.createElement(React.Fragment, null, children))));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Merges multiple refs into a single callback ref. */
|
|
49
|
+
function mergeRefs(...refs) {
|
|
50
|
+
return (value) => {
|
|
51
|
+
refs.forEach((ref) => {
|
|
52
|
+
if (typeof ref === 'function') {
|
|
53
|
+
ref(value);
|
|
54
|
+
}
|
|
55
|
+
else if (ref != null) {
|
|
56
|
+
ref.current = value;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Returns `true` while the element is intersecting the viewport; pauses shimmer when hidden. */
|
|
62
|
+
function useIsVisible(ref) {
|
|
63
|
+
const [visible, setVisible] = React.useState(true);
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
const el = ref.current;
|
|
66
|
+
if (!el || typeof IntersectionObserver === 'undefined')
|
|
67
|
+
return;
|
|
68
|
+
const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting), { threshold: 0 });
|
|
69
|
+
observer.observe(el);
|
|
70
|
+
return () => observer.disconnect();
|
|
71
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
|
+
}, []);
|
|
73
|
+
return visible;
|
|
74
|
+
}
|
|
75
|
+
function normalizeSize(value) {
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
return undefined;
|
|
78
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
79
|
+
}
|
|
80
|
+
const SkeletonItem = React.forwardRef(function SkeletonItem({ variant = 'rect', as: Tag = 'span', width, height, borderRadius, speed, delay, effect = 'shimmer', className, style: styleProp, animate = true, 'aria-label': ariaLabel = 'Loading...', }, forwardedRef) {
|
|
81
|
+
const innerRef = React.useRef(null);
|
|
82
|
+
const isVisible = useIsVisible(innerRef);
|
|
83
|
+
// Auto-square circle: when only width is given, mirror it as height
|
|
84
|
+
const resolvedHeight = variant === 'circle' && height === undefined && width !== undefined ? width : height;
|
|
85
|
+
const cssVars = {};
|
|
86
|
+
if (speed !== undefined) {
|
|
87
|
+
cssVars['--skeleton-animation-duration'] = `${speed}s`;
|
|
88
|
+
}
|
|
89
|
+
if (borderRadius !== undefined) {
|
|
90
|
+
cssVars['--skeleton-border-radius'] =
|
|
91
|
+
typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;
|
|
92
|
+
}
|
|
93
|
+
if (delay !== undefined && delay > 0) {
|
|
94
|
+
cssVars['--skeleton-animation-delay'] = `${delay}s`;
|
|
95
|
+
}
|
|
96
|
+
const style = Object.assign(Object.assign({ width: normalizeSize(width), height: normalizeSize(resolvedHeight) }, cssVars), styleProp);
|
|
97
|
+
const classes = [
|
|
98
|
+
'skeleton',
|
|
99
|
+
`skeleton--${variant}`,
|
|
100
|
+
`skeleton--${effect}`,
|
|
101
|
+
!animate && 'skeleton--no-animate',
|
|
102
|
+
animate && !isVisible && 'skeleton--paused',
|
|
103
|
+
className,
|
|
104
|
+
]
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join(' ');
|
|
107
|
+
const El = Tag;
|
|
108
|
+
return (React.createElement(El, { ref: mergeRefs(innerRef, forwardedRef), role: "progressbar", "aria-busy": "true", "aria-label": ariaLabel, className: classes, style: style }));
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* Animated content placeholder shown while data is loading.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* // Single rect (default)
|
|
115
|
+
* <Skeleton width={300} height={20} />
|
|
116
|
+
*
|
|
117
|
+
* // Paragraph of 3 text lines
|
|
118
|
+
* <Skeleton variant="text" count={3} stagger={0.08} />
|
|
119
|
+
*
|
|
120
|
+
* // Avatar circle
|
|
121
|
+
* <Skeleton variant="circle" width={48} />
|
|
122
|
+
*/
|
|
123
|
+
const Skeleton = React.forwardRef(function Skeleton(props, ref) {
|
|
124
|
+
var _a, _b, _c;
|
|
125
|
+
const theme = useSkeletonTheme();
|
|
126
|
+
const { loading = true, variant = (_a = theme.variant) !== null && _a !== void 0 ? _a : 'rect', as, width, height, borderRadius = theme.borderRadius, count = 1, speed = theme.speed, effect = (_b = theme.effect) !== null && _b !== void 0 ? _b : 'shimmer', stagger = 0, animate = (_c = theme.animate) !== null && _c !== void 0 ? _c : true, className, style, 'aria-label': ariaLabel, children, } = props;
|
|
127
|
+
if (!loading) {
|
|
128
|
+
return React.createElement(React.Fragment, null, children);
|
|
129
|
+
}
|
|
130
|
+
const itemProps = {
|
|
131
|
+
variant,
|
|
132
|
+
as,
|
|
133
|
+
width,
|
|
134
|
+
height,
|
|
135
|
+
borderRadius,
|
|
136
|
+
speed,
|
|
137
|
+
effect,
|
|
138
|
+
animate,
|
|
139
|
+
className,
|
|
140
|
+
style,
|
|
141
|
+
'aria-label': ariaLabel,
|
|
142
|
+
};
|
|
143
|
+
if (variant === 'text') {
|
|
144
|
+
return (React.createElement(React.Fragment, null, Array.from({ length: count }, (_, i) => (React.createElement(SkeletonItem, Object.assign({ key: i }, itemProps, { variant: "text", delay: stagger > 0 ? i * stagger : undefined, ref: i === 0 ? ref : undefined }))))));
|
|
145
|
+
}
|
|
146
|
+
return React.createElement(SkeletonItem, Object.assign({}, itemProps, { ref: ref }));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
exports.Skeleton = Skeleton;
|
|
150
|
+
exports.SkeletonTheme = SkeletonTheme;
|
|
151
|
+
exports.default = Skeleton;
|
|
152
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/SkeletonTheme.tsx","../src/Skeleton.tsx"],"sourcesContent":["import React, { createContext, useContext, CSSProperties } from 'react';\r\nimport { SkeletonVariant, SkeletonEffect } from './Skeleton';\r\n\r\nexport interface SkeletonThemeContextValue {\r\n /**\r\n * Base background color of the skeleton.\r\n * Overrides the `--skeleton-base-color` CSS variable.\r\n */\r\n baseColor?: string;\r\n\r\n /**\r\n * Color of the shimmer highlight.\r\n * Overrides the `--skeleton-highlight-color` CSS variable.\r\n */\r\n highlightColor?: string;\r\n\r\n /**\r\n * Duration of one animation cycle in seconds.\r\n * @default 1.4\r\n */\r\n speed?: number;\r\n\r\n /**\r\n * Border radius applied to `rect` and `text` variants.\r\n * Numbers are treated as `px`.\r\n * @default 4\r\n */\r\n borderRadius?: string | number;\r\n\r\n /**\r\n * When `false`, all skeletons inside the theme render without animation.\r\n * @default true\r\n */\r\n animate?: boolean;\r\n\r\n /**\r\n * Default animation effect for all Skeleton children.\r\n * @default 'shimmer'\r\n */\r\n effect?: SkeletonEffect;\r\n\r\n /**\r\n * Default variant for all Skeleton children.\r\n * @default 'rect'\r\n */\r\n variant?: SkeletonVariant;\r\n}\r\n\r\nconst SkeletonThemeContext = createContext<SkeletonThemeContextValue>({});\r\n\r\n/** Read the nearest SkeletonTheme context. */\r\nexport function useSkeletonTheme(): SkeletonThemeContextValue {\r\n return useContext(SkeletonThemeContext);\r\n}\r\n\r\nexport interface SkeletonThemeProps extends SkeletonThemeContextValue {\r\n children: React.ReactNode;\r\n}\r\n\r\n/**\r\n * Sets default values for all `<Skeleton>` components in its subtree.\r\n * Also injects CSS custom properties so colors and speed are applied\r\n * without any JavaScript per-instance overhead.\r\n *\r\n * Uses `display: contents` on the CSS-variable wrapper so it never\r\n * breaks flex, grid, or any other layout context.\r\n *\r\n * @example\r\n * <SkeletonTheme baseColor=\"#e0e0e0\" highlightColor=\"#f5f5f5\" speed={1.8}>\r\n * <Skeleton />\r\n * <Skeleton variant=\"circle\" width={48} />\r\n * </SkeletonTheme>\r\n */\r\nexport function SkeletonTheme({\r\n children,\r\n baseColor,\r\n highlightColor,\r\n speed,\r\n borderRadius,\r\n animate,\r\n effect,\r\n variant,\r\n}: SkeletonThemeProps) {\r\n const cssVars: CSSProperties = {};\r\n\r\n if (baseColor !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-base-color'] = baseColor;\r\n }\r\n if (highlightColor !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-highlight-color'] = highlightColor;\r\n }\r\n if (speed !== undefined) {\r\n (cssVars as Record<string, string>)['--skeleton-animation-duration'] = `${speed}s`;\r\n }\r\n if (borderRadius !== undefined) {\r\n const r = typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;\r\n (cssVars as Record<string, string>)['--skeleton-border-radius'] = r;\r\n }\r\n\r\n const hasCssVars = Object.keys(cssVars).length > 0;\r\n\r\n return (\r\n <SkeletonThemeContext.Provider value={{ baseColor, highlightColor, speed, borderRadius, animate, effect, variant }}>\r\n {hasCssVars ? (\r\n // display:contents makes the wrapper invisible to layout (flex/grid),\r\n // while still propagating inherited CSS custom properties.\r\n <div style={{ ...cssVars, display: 'contents' }}>\r\n {children}\r\n </div>\r\n ) : (\r\n <>{children}</>\r\n )}\r\n </SkeletonThemeContext.Provider>\r\n );\r\n}\r\n\r\nexport default SkeletonTheme;\r\n","import React, { CSSProperties, ElementType, forwardRef, useEffect, useRef, useState } from 'react';\r\nimport './Skeleton.css';\r\nimport { useSkeletonTheme } from './SkeletonTheme';\r\n\r\n/**\r\n * Shape variant of the skeleton placeholder.\r\n * - `'rect'` — rectangular block (cards, images, banners)\r\n * - `'circle'` — circular shape (avatars, profile pictures)\r\n * - `'text'` — narrow text line; combine with `count` for paragraphs\r\n */\r\nexport type SkeletonVariant = 'rect' | 'circle' | 'text';\r\n\r\n/**\r\n * Visual animation style applied to the skeleton.\r\n * - `'shimmer'` — an animated highlight sweeps across the surface (GPU-accelerated, default)\r\n * - `'pulse'` — the element gently fades in and out\r\n */\r\nexport type SkeletonEffect = 'shimmer' | 'pulse';\r\n\r\n/** Merges multiple refs into a single callback ref. */\r\nfunction mergeRefs<T>(...refs: (React.Ref<T> | null | undefined)[]): React.RefCallback<T> {\r\n return (value) => {\r\n refs.forEach((ref) => {\r\n if (typeof ref === 'function') {\r\n ref(value);\r\n } else if (ref != null) {\r\n (ref as { current: T | null }).current = value;\r\n }\r\n });\r\n };\r\n}\r\n\r\n/** Returns `true` while the element is intersecting the viewport; pauses shimmer when hidden. */\r\nfunction useIsVisible(ref: React.RefObject<HTMLElement>): boolean {\r\n const [visible, setVisible] = useState(true);\r\n useEffect(() => {\r\n const el = ref.current;\r\n if (!el || typeof IntersectionObserver === 'undefined') return;\r\n const observer = new IntersectionObserver(\r\n ([entry]) => setVisible(entry.isIntersecting),\r\n { threshold: 0 }\r\n );\r\n observer.observe(el);\r\n return () => observer.disconnect();\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, []);\r\n return visible;\r\n}\r\n\r\nexport interface SkeletonProps {\r\n /**\r\n * Controls whether the skeleton or the actual content is displayed.\r\n * When `false`, the component renders its `children` unchanged.\r\n * @default true\r\n */\r\n loading?: boolean;\r\n\r\n /**\r\n * Shape of the skeleton placeholder.\r\n * @default 'rect'\r\n */\r\n variant?: SkeletonVariant;\r\n\r\n /**\r\n * HTML element or React component to render as the skeleton root.\r\n * Use this to keep the DOM semantically correct.\r\n * @example `as=\"div\"` | `as=\"li\"` | `as=\"p\"`\r\n * @default 'span'\r\n */\r\n as?: ElementType;\r\n\r\n /**\r\n * Width of the skeleton element.\r\n * - Numbers are converted to pixels: `200` → `\"200px\"`\r\n * - Strings are used as-is: `\"50%\"`, `\"12rem\"`\r\n */\r\n width?: string | number;\r\n\r\n /**\r\n * Height of the skeleton element.\r\n * - Numbers are converted to pixels: `20` → `\"20px\"`\r\n * - Strings are used as-is: `\"1.5rem\"`, `\"auto\"`\r\n * - For `variant=\"circle\"`, omitting `height` automatically copies `width`.\r\n */\r\n height?: string | number;\r\n\r\n /**\r\n * Border radius of the skeleton element.\r\n * - Numbers are converted to pixels: `8` → `\"8px\"`\r\n * - Strings are used as-is: `\"50%\"`, `\"1rem\"`\r\n * - Overrides the `--skeleton-border-radius` CSS variable for this instance.\r\n */\r\n borderRadius?: string | number;\r\n\r\n /**\r\n * Number of skeleton lines to render. Applies whenever `variant=\"text\"`.\r\n * - `count={1}` renders a single line (default)\r\n * - `count={3}` renders three lines, with the last at 80% width to mimic\r\n * a real paragraph ending\r\n * @default 1\r\n */\r\n count?: number;\r\n\r\n /**\r\n * Duration of one animation cycle in seconds.\r\n * Overrides the `--skeleton-animation-duration` CSS variable for this instance.\r\n * @example `speed={2}` for a slower, subtler animation\r\n * @default 1.4\r\n */\r\n speed?: number;\r\n\r\n /**\r\n * Visual effect for the skeleton animation.\r\n * - `'shimmer'` — a highlight sweeps across the surface (GPU-accelerated, default)\r\n * - `'pulse'` — the element fades in and out\r\n * @default 'shimmer'\r\n */\r\n effect?: SkeletonEffect;\r\n\r\n /**\r\n * Animation-delay increment (in seconds) between each line when\r\n * `variant=\"text\"` and `count > 1`. Creates a staggered wave effect.\r\n * @example `stagger={0.1}` → delays of 0s, 0.1s, 0.2s … per line\r\n * @default 0\r\n */\r\n stagger?: number;\r\n\r\n /**\r\n * Enables or disables the skeleton animation.\r\n * Set to `false` to render a static placeholder.\r\n * @default true\r\n */\r\n animate?: boolean;\r\n\r\n /**\r\n * Additional CSS class name appended to the skeleton element.\r\n */\r\n className?: string;\r\n\r\n /**\r\n * Inline styles applied directly to the skeleton element.\r\n * Values provided here take precedence over `width`/`height` props.\r\n */\r\n style?: CSSProperties;\r\n\r\n /**\r\n * Accessible label read by screen readers while the skeleton is visible.\r\n * @default \"Loading...\"\r\n */\r\n 'aria-label'?: string;\r\n\r\n /**\r\n * Content rendered when `loading` is `false`.\r\n */\r\n children?: React.ReactNode;\r\n}\r\n\r\nfunction normalizeSize(value: string | number | undefined): string | undefined {\r\n if (value === undefined) return undefined;\r\n return typeof value === 'number' ? `${value}px` : value;\r\n}\r\n\r\ntype SkeletonItemProps = Required<Pick<SkeletonProps, 'variant'>> &\r\n Pick<SkeletonProps, 'as' | 'width' | 'height' | 'borderRadius' | 'speed' | 'effect' | 'className' | 'style' | 'animate' | 'aria-label'> & {\r\n /** Per-item animation-delay in seconds (stagger effect). */\r\n delay?: number;\r\n };\r\n\r\nconst SkeletonItem = forwardRef<HTMLElement, SkeletonItemProps>(function SkeletonItem({\r\n variant = 'rect',\r\n as: Tag = 'span',\r\n width,\r\n height,\r\n borderRadius,\r\n speed,\r\n delay,\r\n effect = 'shimmer',\r\n className,\r\n style: styleProp,\r\n animate = true,\r\n 'aria-label': ariaLabel = 'Loading...',\r\n}, forwardedRef) {\r\n const innerRef = useRef<HTMLElement>(null);\r\n const isVisible = useIsVisible(innerRef);\r\n\r\n // Auto-square circle: when only width is given, mirror it as height\r\n const resolvedHeight =\r\n variant === 'circle' && height === undefined && width !== undefined ? width : height;\r\n\r\n const cssVars: Record<string, string> = {};\r\n if (speed !== undefined) {\r\n cssVars['--skeleton-animation-duration'] = `${speed}s`;\r\n }\r\n if (borderRadius !== undefined) {\r\n cssVars['--skeleton-border-radius'] =\r\n typeof borderRadius === 'number' ? `${borderRadius}px` : borderRadius;\r\n }\r\n if (delay !== undefined && delay > 0) {\r\n cssVars['--skeleton-animation-delay'] = `${delay}s`;\r\n }\r\n\r\n const style: CSSProperties = {\r\n width: normalizeSize(width),\r\n height: normalizeSize(resolvedHeight),\r\n ...cssVars,\r\n ...styleProp,\r\n };\r\n\r\n const classes = [\r\n 'skeleton',\r\n `skeleton--${variant}`,\r\n `skeleton--${effect}`,\r\n !animate && 'skeleton--no-animate',\r\n animate && !isVisible && 'skeleton--paused',\r\n className,\r\n ]\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const El: any = Tag;\r\n return (\r\n <El\r\n ref={mergeRefs(innerRef, forwardedRef)}\r\n role=\"progressbar\"\r\n aria-busy=\"true\"\r\n aria-label={ariaLabel}\r\n className={classes}\r\n style={style}\r\n />\r\n );\r\n});\r\n\r\n/**\r\n * Animated content placeholder shown while data is loading.\r\n *\r\n * @example\r\n * // Single rect (default)\r\n * <Skeleton width={300} height={20} />\r\n *\r\n * // Paragraph of 3 text lines\r\n * <Skeleton variant=\"text\" count={3} stagger={0.08} />\r\n *\r\n * // Avatar circle\r\n * <Skeleton variant=\"circle\" width={48} />\r\n */\r\nexport const Skeleton = forwardRef<HTMLElement, SkeletonProps>(function Skeleton(props, ref) {\r\n const theme = useSkeletonTheme();\r\n\r\n const {\r\n loading = true,\r\n variant = theme.variant ?? 'rect',\r\n as,\r\n width,\r\n height,\r\n borderRadius = theme.borderRadius,\r\n count = 1,\r\n speed = theme.speed,\r\n effect = theme.effect ?? 'shimmer',\r\n stagger = 0,\r\n animate = theme.animate ?? true,\r\n className,\r\n style,\r\n 'aria-label': ariaLabel,\r\n children,\r\n } = props;\r\n\r\n if (!loading) {\r\n return <>{children}</>;\r\n }\r\n\r\n const itemProps: Omit<SkeletonItemProps, 'delay'> = {\r\n variant,\r\n as,\r\n width,\r\n height,\r\n borderRadius,\r\n speed,\r\n effect,\r\n animate,\r\n className,\r\n style,\r\n 'aria-label': ariaLabel,\r\n };\r\n\r\n if (variant === 'text') {\r\n return (\r\n <>\r\n {Array.from({ length: count }, (_, i) => (\r\n <SkeletonItem\r\n key={i}\r\n {...itemProps}\r\n variant=\"text\"\r\n delay={stagger > 0 ? i * stagger : undefined}\r\n ref={i === 0 ? ref : undefined}\r\n />\r\n ))}\r\n </>\r\n );\r\n }\r\n\r\n return <SkeletonItem {...itemProps} ref={ref} />;\r\n});\r\n\r\nexport default Skeleton;\r\n\r\n"],"names":["createContext","useContext","useState","useEffect","forwardRef","useRef"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tavosud/sky-skeleton",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight zero-dependency React skeleton loader with shimmer effect",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.esm.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "rollup -c",
|
|
13
|
+
"dev": "rollup -c -w",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react",
|
|
20
|
+
"skeleton",
|
|
21
|
+
"loader",
|
|
22
|
+
"shimmer",
|
|
23
|
+
"placeholder",
|
|
24
|
+
"zero-dependency"
|
|
25
|
+
],
|
|
26
|
+
"author": "tavosud",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=17.0.0",
|
|
30
|
+
"react-dom": ">=17.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@rollup/plugin-commonjs": "^25.0.8",
|
|
34
|
+
"@rollup/plugin-node-resolve": "^15.3.1",
|
|
35
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
36
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
37
|
+
"@testing-library/react": "^16.3.2",
|
|
38
|
+
"@types/react": "^18.3.28",
|
|
39
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
40
|
+
"jsdom": "^29.0.1",
|
|
41
|
+
"rollup": "^4.60.0",
|
|
42
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
43
|
+
"tslib": "^2.8.1",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vitest": "^4.1.0"
|
|
46
|
+
},
|
|
47
|
+
"sideEffects": false
|
|
48
|
+
}
|