shimmer-from-structure 1.0.0 → 1.0.2

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.
Files changed (2) hide show
  1. package/README.md +474 -0
  2. package/package.json +41 -39
package/README.md ADDED
@@ -0,0 +1,474 @@
1
+ # ✨ Shimmer From Structure
2
+
3
+ A **React & Vue** shimmer/skeleton library that **automatically adapts to your component's runtime structure**. Unlike traditional shimmer libraries that require pre-defined skeleton structures, this library analyzes your actual component's DOM at runtime and generates a shimmer effect that perfectly matches its layout.
4
+
5
+ ![Shimmer From Structure Demo](./example/preview.gif)
6
+
7
+ ## Why This Library?
8
+
9
+ Traditional shimmer libraries require you to:
10
+
11
+ - Manually create skeleton components that mirror your real components
12
+ - Maintain two versions of each component (real + skeleton)
13
+ - Update skeletons every time your layout changes
14
+
15
+ **Shimmer From Structure** eliminates all of that:
16
+
17
+ - ✅ **Works with React & Vue** - Simple, framework-specific drivers
18
+ - ✅ Automatically measures your component's structure at runtime
19
+ - ✅ Generates shimmer effects that match actual dimensions
20
+ - ✅ Zero maintenance - works with any layout changes
21
+ - ✅ Works with complex nested structures
22
+ - ✅ Supports dynamic data with `templateProps`
23
+ - ✅ Preserves container backgrounds during loading
24
+ - ✅ Auto-detects border-radius from your CSS
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install shimmer-from-structure
30
+ # or
31
+ yarn add shimmer-from-structure
32
+ # or
33
+ pnpm add shimmer-from-structure
34
+ ```
35
+
36
+ ## 🎯 Framework Support
37
+
38
+ Shimmer From Structure provides dedicated packages for **React and Vue**.
39
+
40
+ ### React
41
+
42
+ React support is built-in to the main package for backward compatibility:
43
+
44
+ ```javascript
45
+ // React projects (or @shimmer-from-structure/react)
46
+ import { Shimmer } from 'shimmer-from-structure';
47
+ ```
48
+
49
+ ### Vue 3
50
+
51
+ Vue support requires importing from the specific adapter:
52
+
53
+ ```javascript
54
+ // Vue 3 projects
55
+ import { Shimmer } from '@shimmer-from-structure/vue';
56
+ ```
57
+
58
+ ---
59
+
60
+ # 📖 Basic Usage
61
+
62
+ ## React
63
+
64
+ ### Static Content
65
+
66
+ For components with hardcoded/static content:
67
+
68
+ ```tsx
69
+ import { Shimmer } from 'shimmer-from-structure';
70
+
71
+ function UserCard() {
72
+ return (
73
+ <Shimmer loading={isLoading}>
74
+ <div className="card">
75
+ <img src="avatar.jpg" className="avatar" />
76
+ <h2>John Doe</h2>
77
+ <p>Software Engineer</p>
78
+ </div>
79
+ </Shimmer>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ## Vue
85
+
86
+ ### Static Content
87
+
88
+ ```vue
89
+ <script setup>
90
+ import { ref } from 'vue';
91
+ import { Shimmer } from '@shimmer-from-structure/vue';
92
+
93
+ const isLoading = ref(true);
94
+ </script>
95
+
96
+ <template>
97
+ <Shimmer :loading="isLoading">
98
+ <div class="card">
99
+ <img src="avatar.jpg" class="avatar" />
100
+ <h2>John Doe</h2>
101
+ <p>Software Engineer</p>
102
+ </div>
103
+ </Shimmer>
104
+ </template>
105
+ ```
106
+
107
+ ---
108
+
109
+ ### Dynamic Content with `templateProps`
110
+
111
+ For components that receive dynamic data via props, use `templateProps` to provide mock data for skeleton generation:
112
+
113
+ ```tsx
114
+ import { Shimmer } from 'shimmer-from-structure';
115
+
116
+ // Your component that accepts props
117
+ const UserCard = ({ user }) => (
118
+ <div className="card">
119
+ <img src={user.avatar} className="avatar" />
120
+ <h2>{user.name}</h2>
121
+ <p>{user.role}</p>
122
+ </div>
123
+ );
124
+
125
+ // Template data for the skeleton
126
+ const userTemplate = {
127
+ name: 'Loading...',
128
+ role: 'Loading role...',
129
+ avatar: 'placeholder.jpg',
130
+ };
131
+
132
+ function App() {
133
+ const [loading, setLoading] = useState(true);
134
+ const [user, setUser] = useState(null);
135
+
136
+ return (
137
+ <Shimmer loading={loading} templateProps={{ user: userTemplate }}>
138
+ <UserCard user={user || userTemplate} />
139
+ </Shimmer>
140
+ );
141
+ }
142
+ ```
143
+
144
+ The `templateProps` object is spread onto the first child component when loading, allowing it to render with mock data for measurement.
145
+
146
+ ## 🎨 API Reference
147
+
148
+ ### `<Shimmer>` Props
149
+
150
+ | Prop | Type | Default | Description |
151
+ | ---------------------- | ------------------------- | -------------------------- | --------------------------------------------------------- |
152
+ | `loading` | `boolean` | `true` | Whether to show shimmer effect or actual content |
153
+ | `children` | `React.ReactNode` | required | The content to render/measure |
154
+ | `shimmerColor` | `string` | `'rgba(255,255,255,0.15)'` | Color of the shimmer wave |
155
+ | `backgroundColor` | `string` | `'rgba(255,255,255,0.08)'` | Background color of shimmer blocks |
156
+ | `duration` | `number` | `1.5` | Animation duration in seconds |
157
+ | `fallbackBorderRadius` | `number` | `4` | Border radius (px) for elements with no CSS border-radius |
158
+ | `templateProps` | `Record<string, unknown>` | - | Props to inject into first child for skeleton rendering |
159
+
160
+ ### Example with All Props
161
+
162
+ ```tsx
163
+ <Shimmer
164
+ loading={isLoading}
165
+ shimmerColor="rgba(255, 255, 255, 0.2)"
166
+ backgroundColor="rgba(255, 255, 255, 0.1)"
167
+ duration={2}
168
+ fallbackBorderRadius={8}
169
+ templateProps={{
170
+ user: userTemplate,
171
+ settings: settingsTemplate,
172
+ }}
173
+ >
174
+ <MyComponent user={user} settings={settings} />
175
+ </Shimmer>
176
+ ```
177
+
178
+ ## 🔧 How It Works
179
+
180
+ 1. **Visible Container Rendering**: When `loading={true}`, your component renders with transparent text but **visible container backgrounds**
181
+ 2. **Template Props Injection**: If `templateProps` is provided, it's spread onto the first child so dynamic components can render
182
+ 3. **DOM Measurement**: Uses `useLayoutEffect` to synchronously measure all leaf elements via `getBoundingClientRect()`
183
+ 4. **Border Radius Detection**: Automatically captures each element's computed `border-radius` from CSS
184
+ 5. **Shimmer Generation**: Creates absolutely-positioned shimmer blocks matching measured dimensions
185
+ 6. **Animation**: Applies smooth gradient animation that sweeps across each block
186
+
187
+ ### Key Features
188
+
189
+ - **Container backgrounds visible**: Unlike `opacity: 0`, we use `color: transparent` so card backgrounds/borders show during loading
190
+ - **Auto border-radius**: Circular avatars get circular shimmer blocks automatically
191
+ - **Fallback radius**: Text elements (which have `border-radius: 0`) use `fallbackBorderRadius` to avoid sharp rectangles
192
+ - **Dark-mode friendly**: Default colors use semi-transparent whites that work on any background
193
+
194
+ ## Examples
195
+
196
+ ### Dashboard with Multiple Sections
197
+
198
+ Each section can have its own independent loading state:
199
+
200
+ ```tsx
201
+ function Dashboard() {
202
+ const [loadingUser, setLoadingUser] = useState(true);
203
+ const [loadingStats, setLoadingStats] = useState(true);
204
+
205
+ return (
206
+ <>
207
+ {/* User profile section */}
208
+ <Shimmer loading={loadingUser} templateProps={{ user: userTemplate }}>
209
+ <UserProfile user={user} />
210
+ </Shimmer>
211
+
212
+ {/* Stats section - with custom colors */}
213
+ <Shimmer
214
+ loading={loadingStats}
215
+ templateProps={{ stats: statsTemplate }}
216
+ shimmerColor="rgba(20, 184, 166, 0.2)"
217
+ >
218
+ <StatsGrid stats={stats} />
219
+ </Shimmer>
220
+ </>
221
+ );
222
+ }
223
+ ```
224
+
225
+ ### Transactions List
226
+
227
+ ```tsx
228
+ <Shimmer loading={loadingTransactions} templateProps={{ transactions: transactionsTemplate }}>
229
+ <TransactionsList transactions={transactions} />
230
+ </Shimmer>
231
+ ```
232
+
233
+ ### Team Members Grid
234
+
235
+ ```tsx
236
+ <Shimmer loading={loadingTeam} templateProps={{ members: teamTemplate }}>
237
+ <TeamMembers members={team} />
238
+ </Shimmer>
239
+ ```
240
+
241
+ ## 🔄 Using with React Suspense
242
+
243
+ Shimmer works seamlessly as a Suspense fallback. When used this way, `loading` is always `true` because React automatically unmounts the fallback and replaces it with the resolved component.
244
+
245
+ ### Basic Suspense Pattern
246
+
247
+ ```tsx
248
+ import { Suspense, lazy } from 'react';
249
+ import { Shimmer } from 'shimmer-from-structure';
250
+
251
+ const UserProfile = lazy(() => import('./UserProfile'));
252
+
253
+ function App() {
254
+ return (
255
+ <Suspense
256
+ fallback={
257
+ <Shimmer loading={true} templateProps={{ user: userTemplate }}>
258
+ <UserProfile />
259
+ </Shimmer>
260
+ }
261
+ >
262
+ <UserProfile userId="123" />
263
+ </Suspense>
264
+ );
265
+ }
266
+ ```
267
+
268
+ ### Why `loading={true}` is Always Set
269
+
270
+ When using Shimmer as a Suspense fallback:
271
+
272
+ 1. **Suspend**: React renders the fallback → Shimmer shows with `loading={true}`
273
+ 2. **Resolve**: React **replaces** the entire fallback with the real component
274
+ 3. The Shimmer is **unmounted**, not updated — so you never need to toggle `loading`
275
+
276
+ ### Performance Tips for Suspense
277
+
278
+ **Memoize the fallback** to prevent re-renders:
279
+
280
+ ```tsx
281
+ const ShimmerFallback = React.memo(() => (
282
+ <Shimmer loading={true} templateProps={{ user: userTemplate }}>
283
+ <UserProfile />
284
+ </Shimmer>
285
+ ));
286
+
287
+ // Usage
288
+ <Suspense fallback={<ShimmerFallback />}>
289
+ <UserProfile userId="123" />
290
+ </Suspense>;
291
+ ```
292
+
293
+ **Keep templates lightweight** — the DOM is measured synchronously via `useLayoutEffect`, so avoid complex logic in your template.
294
+
295
+ ## Global Configuration (Context API)
296
+
297
+ You can set default configuration for your entire app (or specific sections) using the context/provider pattern. This is perfect for maintaining consistent themes without repeating props.
298
+
299
+ ### React
300
+
301
+ ```tsx
302
+ import { Shimmer, ShimmerProvider } from '@shimmer-from-structure/react';
303
+
304
+ function App() {
305
+ return (
306
+ // Set global defaults
307
+ <ShimmerProvider
308
+ config={{
309
+ shimmerColor: 'rgba(56, 189, 248, 0.4)', // Blue shimmer
310
+ backgroundColor: 'rgba(56, 189, 248, 0.1)', // Blue background
311
+ duration: 2.5,
312
+ fallbackBorderRadius: 8,
313
+ }}
314
+ >
315
+ <Dashboard />
316
+ </ShimmerProvider>
317
+ );
318
+ }
319
+ ```
320
+
321
+ ### Vue
322
+
323
+ ```vue
324
+ <!-- App.vue -->
325
+ <script setup>
326
+ import { Shimmer, provideShimmerConfig } from '@shimmer-from-structure/vue';
327
+
328
+ provideShimmerConfig({
329
+ shimmerColor: 'rgba(255, 0, 0, 0.1)',
330
+ duration: 2,
331
+ });
332
+ </script>
333
+
334
+ <template>
335
+ <router-view />
336
+ </template>
337
+ ```
338
+
339
+ ---
340
+
341
+ Components inside the provider automatically inherit values. You can still override them locally:
342
+
343
+ ```tsx
344
+ // Inherits blue theme from provider
345
+ <Shimmer loading={true}><UserCard /></Shimmer>
346
+
347
+ // Overrides provider settings
348
+ <Shimmer loading={true} duration={0.5}><FastCard /></Shimmer>
349
+ ```
350
+
351
+ ### Accessing Config in Hooks
352
+
353
+ If you need to access the current configuration in your own components:
354
+
355
+ ```tsx
356
+ import { useShimmerConfig } from 'shimmer-from-structure';
357
+
358
+ function MyComponent() {
359
+ const config = useShimmerConfig();
360
+ return <div style={{ background: config.backgroundColor }}>...</div>;
361
+ }
362
+ ```
363
+
364
+ ## Best Practices
365
+
366
+ ### 1. Use `templateProps` for Dynamic Data
367
+
368
+ When your component receives data via props, always provide `templateProps` with mock data that matches the expected structure.
369
+
370
+ ### 2. Match Template Structure to Real Data
371
+
372
+ Ensure your template data has the same array length and property structure as real data for accurate shimmer layout.
373
+
374
+ ### 3. Use Individual Shimmer Components
375
+
376
+ Wrap each section in its own Shimmer for independent loading states:
377
+
378
+ ```tsx
379
+ // ✅ Good - independent loading
380
+ <Shimmer loading={loadingUsers}><UserList /></Shimmer>
381
+ <Shimmer loading={loadingPosts}><PostList /></Shimmer>
382
+
383
+ // ❌ Avoid - all-or-nothing loading
384
+ <Shimmer loading={loadingUsers || loadingPosts}>
385
+ <UserList />
386
+ <PostList />
387
+ </Shimmer>
388
+ ```
389
+
390
+ ### 4. Consider Element Widths
391
+
392
+ Block elements like `<h1>`, `<p>` take full container width. If you want shimmer to match text width:
393
+
394
+ ```css
395
+ .title {
396
+ width: fit-content;
397
+ }
398
+ ```
399
+
400
+ ### 5. Provide Container Dimensions
401
+
402
+ For async components (like charts), ensure containers have explicit dimensions so shimmer has something to measure.
403
+
404
+ ## ⚡ Performance Considerations
405
+
406
+ - Measurement happens only when `loading` changes to `true`
407
+ - Uses `useLayoutEffect` for synchronous measurement (no flicker)
408
+ - Minimal re-renders - only updates when loading state or children change
409
+ - Lightweight DOM measurements using native browser APIs
410
+
411
+ - Lightweight DOM measurements using native browser APIs
412
+
413
+ ## 🛠️ Development
414
+
415
+ This is a monorepo managed with npm workspaces. Each package can be built independently:
416
+
417
+ ```bash
418
+ # Install dependencies
419
+ npm install
420
+
421
+ # Build all packages
422
+ npm run build
423
+
424
+ # Build individual packages
425
+ npm run build:core
426
+ npm run build:react
427
+ npm run build:vue
428
+ npm run build:main
429
+
430
+ # Run tests
431
+ npm test
432
+ ```
433
+
434
+ ## 📝 License
435
+
436
+ MIT
437
+
438
+ ## 🤝 Contributing
439
+
440
+ Contributions are welcome! Please feel free to submit a Pull Request.
441
+
442
+ ## 🐛 Known Limitations
443
+
444
+ - **Async components**: Components that render asynchronously (like charts using `ResponsiveContainer`) may need explicit container dimensions
445
+ - **Zero-dimension elements**: Elements with `display: none` or zero dimensions won't be captured
446
+ - **SVG internals**: Only the outer `<svg>` element is captured, not internal paths/shapes
447
+
448
+ ## 🏗️ Monorepo Structure
449
+
450
+ This library is organized as a monorepo with four packages:
451
+
452
+ | Package | Description | Size |
453
+ | ------------------------------- | ------------------------------------------- | -------- |
454
+ | `@shimmer-from-structure/core` | Framework-agnostic DOM utilities | 1.44 kB |
455
+ | `@shimmer-from-structure/react` | React adapter | 12.84 kB |
456
+ | `@shimmer-from-structure/vue` | Vue 3 adapter | 3.89 kB |
457
+ | `shimmer-from-structure` | Main package (React backward compatibility) | 0.93 kB |
458
+
459
+ The core package contains all DOM measurement logic, while React and Vue packages are thin wrappers that provide framework-specific APIs.
460
+
461
+ ## 🚧 Roadmap
462
+
463
+ - [x] Dynamic data support via `templateProps`
464
+ - [x] Auto border-radius detection
465
+ - [x] Container background visibility
466
+ - [x] **Vue.js adapter** ✨
467
+ - [ ] Better async component support
468
+ - [ ] Customizable shimmer direction (vertical, diagonal)
469
+ - [ ] React Native support
470
+ - [ ] Svelte adapter
471
+
472
+ ---
473
+
474
+ Made with ❤️ for developers tired of maintaining skeleton screens
package/package.json CHANGED
@@ -1,42 +1,44 @@
1
1
  {
2
- "name": "shimmer-from-structure",
3
- "version": "1.0.0",
4
- "description": "React shimmer library that automatically adapts to your component's runtime structure",
5
- "main": "index.js",
6
- "types": "index.d.ts",
7
- "exports": {
8
- ".": {
9
- "types": "./index.d.ts",
10
- "default": "./index.js"
2
+ "name": "shimmer-from-structure",
3
+ "version": "1.0.2",
4
+ "description": "Universal UI skeleton generator for React and Vue. Auto-adapts to your component structure with zero maintenance and pixel-perfect accuracy.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "default": "./index.js"
11
+ },
12
+ "./vue": "@shimmer-from-structure/vue"
11
13
  },
12
- "./vue": "@shimmer-from-structure/vue"
13
- },
14
- "scripts": {
15
- "build": "tsc"
16
- },
17
- "dependencies": {
18
- "@shimmer-from-structure/react": "*",
19
- "@shimmer-from-structure/vue": "*"
20
- },
21
- "peerDependencies": {
22
- "react": "^18.0.0 || ^19.0.0",
23
- "react-dom": "^18.0.0 || ^19.0.0"
24
- },
25
- "peerDependenciesMeta": {
26
- "react": {
27
- "optional": true
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "format": "prettier --write .",
17
+ "prepublishOnly": "cp ../../README.md ."
28
18
  },
29
- "react-dom": {
30
- "optional": true
31
- }
32
- },
33
- "keywords": [
34
- "react",
35
- "vue",
36
- "shimmer",
37
- "skeleton",
38
- "loading"
39
- ],
40
- "author": "Olebogeng Mbedzi",
41
- "license": "MIT"
42
- }
19
+ "dependencies": {
20
+ "@shimmer-from-structure/react": "*",
21
+ "@shimmer-from-structure/vue": "*"
22
+ },
23
+ "peerDependencies": {
24
+ "react": "^18.0.0 || ^19.0.0",
25
+ "react-dom": "^18.0.0 || ^19.0.0"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "react": {
29
+ "optional": true
30
+ },
31
+ "react-dom": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "keywords": [
36
+ "react",
37
+ "vue",
38
+ "shimmer",
39
+ "skeleton",
40
+ "loading"
41
+ ],
42
+ "author": "Olebogeng Mbedzi",
43
+ "license": "MIT"
44
+ }