react-instagram-stories 1.0.5 → 1.1.1

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 CHANGED
@@ -5,6 +5,22 @@ A high-performance, fully customizable Instagram-style Stories component for Rea
5
5
  [![NPM Version](https://img.shields.io/npm/v/react-instagram-stories.svg)](https://www.npmjs.com/package/react-instagram-stories)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
+ ## Preview
9
+
10
+ <p align="center">
11
+ <img src="public/screenshots/demo.gif" alt="React Instagram Stories — live demo" width="320" />
12
+ </p>
13
+
14
+ <p align="center">
15
+ <img src="public/screenshots/avatar-list.png" alt="Avatar list with story rings" width="100%" />
16
+ </p>
17
+
18
+ <p align="center">
19
+ <img src="public/screenshots/story-viewer.png" alt="Story viewer — image story" width="32%" />
20
+ &nbsp;&nbsp;
21
+ <img src="public/screenshots/story-poll.png" alt="Story viewer — interactive poll" width="32%" />
22
+ </p>
23
+
8
24
  ## Features
9
25
 
10
26
  - **Multiple Content Types**: Images, videos (with audio), text, and fully custom components
@@ -14,11 +30,14 @@ A high-performance, fully customizable Instagram-style Stories component for Rea
14
30
  - **Keyboard Navigation**: Full keyboard support for accessibility
15
31
  - **TypeScript**: Complete type definitions included
16
32
  - **Accessible**: ARIA labels and keyboard navigation
33
+ - **Tailwind CSS Support**: Customize every sub-element via `classNames` prop with Tailwind or custom CSS classes
34
+ - **3D Cube Transition**: Instagram-style 3D cube drag transition when switching between users
35
+ - **Story Resume**: Dragging back to a previous user resumes their story from where you left off
17
36
  - **Lightweight**: ~30 KB with zero runtime dependencies
18
37
  - **No Router Required**: Works with native browser history API
19
38
  - **URL Navigation**: Built-in query parameter support (`?user=userId&story=storyId`)
20
39
  - **Auto Progress**: Smart progress bar that pauses during video buffering
21
- - **Smooth Transitions**: Beautiful animations between stories and users
40
+ - **Smooth Transitions**: 3D cube drag transitions between users, smooth animations between stories
22
41
 
23
42
  ## Installation
24
43
 
@@ -37,8 +56,8 @@ pnpm add react-instagram-stories
37
56
  ### Simple Usage (Recommended)
38
57
 
39
58
  ```tsx
40
- import { Stories, demoUsers } from 'react-instagram-stories';
41
- import 'react-instagram-stories/styles.css';
59
+ import { Stories, demoUsers } from "react-instagram-stories";
60
+ import "react-instagram-stories/styles.css";
42
61
 
43
62
  function App() {
44
63
  return <Stories users={demoUsers} />;
@@ -52,8 +71,13 @@ That's it! Click on any avatar to open the story viewer. The URL automatically u
52
71
  ### Using Separate Components
53
72
 
54
73
  ```tsx
55
- import { AvatarList, StoryViewer, demoUsers, navigateWithParams } from 'react-instagram-stories';
56
- import 'react-instagram-stories/styles.css';
74
+ import {
75
+ AvatarList,
76
+ StoryViewer,
77
+ demoUsers,
78
+ navigateWithParams,
79
+ } from "react-instagram-stories";
80
+ import "react-instagram-stories/styles.css";
57
81
 
58
82
  function App() {
59
83
  const handleAvatarClick = (userIndex: number) => {
@@ -76,20 +100,20 @@ function App() {
76
100
 
77
101
  The StoryViewer supports URL-based navigation using query parameters with **user ID** and **story ID**:
78
102
 
79
- | URL | Result |
80
- |-----|--------|
81
- | `?user=user-travel&story=travel-1` | Opens Travel user's first story |
82
- | `?user=user-polls&story=poll-1` | Opens Interactive user's poll story |
83
- | `?user=user-launch&story=launch-2` | Opens Events user's second story |
84
- | No query params | Viewer stays closed |
103
+ | URL | Result |
104
+ | ---------------------------------- | ----------------------------------- |
105
+ | `?user=user-travel&story=travel-1` | Opens Travel user's first story |
106
+ | `?user=user-polls&story=poll-1` | Opens Interactive user's poll story |
107
+ | `?user=user-launch&story=launch-2` | Opens Events user's second story |
108
+ | No query params | Viewer stays closed |
85
109
 
86
110
  ### Navigation Helpers
87
111
 
88
112
  ```tsx
89
- import { navigateWithParams, clearQueryParams } from 'react-instagram-stories';
113
+ import { navigateWithParams, clearQueryParams } from "react-instagram-stories";
90
114
 
91
115
  // Open story viewer with user ID and story ID
92
- navigateWithParams({ user: 'user-travel', story: 'travel-1' });
116
+ navigateWithParams({ user: "user-travel", story: "travel-1" });
93
117
 
94
118
  // Close story viewer (clear params)
95
119
  clearQueryParams();
@@ -102,20 +126,22 @@ clearQueryParams();
102
126
  The all-in-one component with avatar list and story viewer.
103
127
 
104
128
  ```tsx
105
- import { Stories } from 'react-instagram-stories';
129
+ import { Stories } from "react-instagram-stories";
106
130
 
107
- <Stories users={myUsers} />
131
+ <Stories users={myUsers} />;
108
132
  ```
109
133
 
110
- | Prop | Type | Default | Description |
111
- |------|------|---------|-------------|
112
- | `users` | `User[]` | **required** | Array of user objects with their stories |
134
+ | Prop | Type | Default | Description |
135
+ | ------------ | ------------------- | ------------ | ---------------------------------------------------------------- |
136
+ | `users` | `User[]` | **required** | Array of user objects with their stories |
137
+ | `classNames` | `StoriesClassNames` | - | Object to customize sub-element CSS classes (Tailwind or custom) |
113
138
 
114
139
  ### `<StoryViewer />` Component
115
140
 
116
141
  The story viewer component. Supports two modes:
117
142
 
118
143
  #### Query Param Mode (Default)
144
+
119
145
  When `isOpen` is not provided, reads from URL query params:
120
146
 
121
147
  ```tsx
@@ -123,6 +149,7 @@ When `isOpen` is not provided, reads from URL query params:
123
149
  ```
124
150
 
125
151
  #### Controlled Mode
152
+
126
153
  When `isOpen` is provided, you control the viewer state:
127
154
 
128
155
  ```tsx
@@ -131,19 +158,20 @@ When `isOpen` is provided, you control the viewer state:
131
158
  isOpen={true}
132
159
  initialUserIndex={0}
133
160
  initialStoryIndex={0}
134
- onClose={() => console.log('closed')}
161
+ onClose={() => console.log("closed")}
135
162
  onStoryChange={(userIndex, storyIndex) => console.log(userIndex, storyIndex)}
136
163
  />
137
164
  ```
138
165
 
139
- | Prop | Type | Default | Description |
140
- |------|------|---------|-------------|
141
- | `users` | `User[]` | **required** | Array of user objects |
142
- | `isOpen` | `boolean` | - | Controls viewer visibility (enables controlled mode) |
143
- | `initialUserIndex` | `number` | `0` | Starting user index |
144
- | `initialStoryIndex` | `number` | `0` | Starting story index |
145
- | `onClose` | `() => void` | - | Called when viewer closes |
146
- | `onStoryChange` | `(userIndex, storyIndex) => void` | - | Called when story changes |
166
+ | Prop | Type | Default | Description |
167
+ | ------------------- | --------------------------------- | ------------ | ---------------------------------------------------------------- |
168
+ | `users` | `User[]` | **required** | Array of user objects |
169
+ | `isOpen` | `boolean` | - | Controls viewer visibility (enables controlled mode) |
170
+ | `initialUserIndex` | `number` | `0` | Starting user index |
171
+ | `initialStoryIndex` | `number` | `0` | Starting story index |
172
+ | `onClose` | `() => void` | - | Called when viewer closes |
173
+ | `onStoryChange` | `(userIndex, storyIndex) => void` | - | Called when story changes |
174
+ | `classNames` | `StoryViewerClassNames` | - | Object to customize sub-element CSS classes (Tailwind or custom) |
147
175
 
148
176
  ### `<AvatarList />` Component
149
177
 
@@ -156,10 +184,11 @@ The horizontal scrollable avatar list.
156
184
  />
157
185
  ```
158
186
 
159
- | Prop | Type | Description |
160
- |------|------|-------------|
161
- | `users` | `User[]` | Array of user objects |
162
- | `onAvatarClick` | `(userIndex: number) => void` | Called when avatar is clicked |
187
+ | Prop | Type | Description |
188
+ | --------------- | ----------------------------- | ---------------------------------------------------------------- |
189
+ | `users` | `User[]` | Array of user objects |
190
+ | `onAvatarClick` | `(userIndex: number) => void` | Called when avatar is clicked |
191
+ | `classNames` | `AvatarListClassNames` | Object to customize sub-element CSS classes (Tailwind or custom) |
163
192
 
164
193
  ## Story Types
165
194
 
@@ -187,6 +216,7 @@ The horizontal scrollable avatar list.
187
216
  ```
188
217
 
189
218
  **Features:**
219
+
190
220
  - Audio enabled by default
191
221
  - Progress bar pauses during buffering
192
222
  - Auto-detects video duration
@@ -234,6 +264,7 @@ const MyCustomStory: React.FC<StoryItemControls> = ({
234
264
  ```
235
265
 
236
266
  **Control Methods Available:**
267
+
237
268
  - `pause()` - Pause the story timer
238
269
  - `resume()` - Resume the story timer
239
270
  - `next()` - Go to next story
@@ -247,10 +278,14 @@ Build interactive experiences! Here are examples included in `demoUsers`:
247
278
  ### Poll Component
248
279
 
249
280
  ```tsx
250
- const PollComponent: React.FC<StoryItemControls> = ({ pause, resume, next }) => {
281
+ const PollComponent: React.FC<StoryItemControls> = ({
282
+ pause,
283
+ resume,
284
+ next,
285
+ }) => {
251
286
  const [selected, setSelected] = React.useState<number | null>(null);
252
287
  const [votes, setVotes] = React.useState([42, 28, 18, 12]);
253
- const options = ['React', 'Vue', 'Angular', 'Svelte'];
288
+ const options = ["React", "Vue", "Angular", "Svelte"];
254
289
 
255
290
  React.useEffect(() => {
256
291
  pause(); // Pause timer during interaction
@@ -272,14 +307,16 @@ const PollComponent: React.FC<StoryItemControls> = ({ pause, resume, next }) =>
272
307
  const total = votes.reduce((a, b) => a + b, 0);
273
308
 
274
309
  return (
275
- <div style={{
276
- display: 'flex',
277
- flexDirection: 'column',
278
- height: '100%',
279
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
280
- padding: '20px'
281
- }}>
282
- <h2 style={{ color: 'white', marginBottom: '20px' }}>
310
+ <div
311
+ style={{
312
+ display: "flex",
313
+ flexDirection: "column",
314
+ height: "100%",
315
+ background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
316
+ padding: "20px",
317
+ }}
318
+ >
319
+ <h2 style={{ color: "white", marginBottom: "20px" }}>
283
320
  What's your favorite framework?
284
321
  </h2>
285
322
  {options.map((option, index) => (
@@ -288,17 +325,18 @@ const PollComponent: React.FC<StoryItemControls> = ({ pause, resume, next }) =>
288
325
  onClick={() => handleVote(index)}
289
326
  disabled={selected !== null}
290
327
  style={{
291
- margin: '8px 0',
292
- padding: '15px',
293
- background: selected === index ? '#4CAF50' : 'rgba(255,255,255,0.2)',
294
- color: 'white',
295
- border: 'none',
296
- borderRadius: '12px',
297
- fontSize: '16px',
298
- cursor: selected !== null ? 'default' : 'pointer'
328
+ margin: "8px 0",
329
+ padding: "15px",
330
+ background:
331
+ selected === index ? "#4CAF50" : "rgba(255,255,255,0.2)",
332
+ color: "white",
333
+ border: "none",
334
+ borderRadius: "12px",
335
+ fontSize: "16px",
336
+ cursor: selected !== null ? "default" : "pointer",
299
337
  }}
300
338
  >
301
- <div style={{ display: 'flex', justifyContent: 'space-between' }}>
339
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
302
340
  <span>{option}</span>
303
341
  {selected !== null && (
304
342
  <span>{((votes[index] / total) * 100).toFixed(0)}%</span>
@@ -318,7 +356,26 @@ const PollComponent: React.FC<StoryItemControls> = ({ pause, resume, next }) =>
318
356
  Import the default styles:
319
357
 
320
358
  ```tsx
321
- import 'react-instagram-stories/styles.css';
359
+ import "react-instagram-stories/styles.css";
360
+ ```
361
+
362
+ **With Tailwind CSS**: Import the library styles **before** your Tailwind CSS so that utility classes can override them:
363
+
364
+ ```css
365
+ /* Your global CSS file */
366
+ @import "react-instagram-stories/styles.css";
367
+
368
+ @tailwind base;
369
+ @tailwind components;
370
+ @tailwind utilities;
371
+ ```
372
+
373
+ Or in JS, import the library CSS before your app's CSS:
374
+
375
+ ```tsx
376
+ // main.tsx
377
+ import "react-instagram-stories/styles.css"; // first
378
+ import "./index.css"; // your Tailwind CSS — after
322
379
  ```
323
380
 
324
381
  Override with custom CSS:
@@ -350,13 +407,60 @@ Override with custom CSS:
350
407
  }
351
408
  ```
352
409
 
410
+ ## Customization with classNames
411
+
412
+ Every component accepts a `classNames` prop for fine-grained styling using Tailwind CSS or custom CSS classes. The prop is a typed object that maps to specific sub-elements.
413
+
414
+ ```tsx
415
+ <Stories
416
+ users={users}
417
+ classNames={{
418
+ avatarList: {
419
+ root: "bg-gray-900 p-4",
420
+ avatar: { ring: "border-pink-500", username: "text-xs text-gray-400" },
421
+ },
422
+ storyViewer: {
423
+ overlay: "bg-black/90",
424
+ closeButton: "hover:text-gray-300",
425
+ progressBars: {
426
+ bar: { fill: "bg-gradient-to-r from-pink-500 to-purple-500" },
427
+ },
428
+ },
429
+ }}
430
+ />
431
+ ```
432
+
433
+ You can also pass `classNames` directly to `StoryViewer` or `AvatarList` when using them separately:
434
+
435
+ ```tsx
436
+ <AvatarList
437
+ users={users}
438
+ onAvatarClick={handleClick}
439
+ classNames={{
440
+ root: "bg-gray-900 p-4",
441
+ avatar: { ring: "border-pink-500", username: "text-xs text-gray-400" },
442
+ }}
443
+ />
444
+
445
+ <StoryViewer
446
+ users={users}
447
+ classNames={{
448
+ overlay: "bg-black/90",
449
+ closeButton: "hover:text-gray-300",
450
+ progressBars: { bar: { fill: "bg-gradient-to-r from-pink-500 to-purple-500" } },
451
+ }}
452
+ />
453
+ ```
454
+
455
+ All `classNames` types are exported from the package. See the [TypeScript Types](#typescript-types) section for the full list.
456
+
353
457
  ## Advanced Usage
354
458
 
355
459
  ### Using Demo Data
356
460
 
357
461
  ```tsx
358
- import { Stories, demoUsers } from 'react-instagram-stories';
359
- import 'react-instagram-stories/styles.css';
462
+ import { Stories, demoUsers } from "react-instagram-stories";
463
+ import "react-instagram-stories/styles.css";
360
464
 
361
465
  function App() {
362
466
  return <Stories users={demoUsers} />;
@@ -368,7 +472,7 @@ The `demoUsers` includes examples of all story types including interactive polls
368
472
  ### Generate Demo Users
369
473
 
370
474
  ```tsx
371
- import { generateDemoUsers } from 'react-instagram-stories';
475
+ import { generateDemoUsers } from "react-instagram-stories";
372
476
 
373
477
  const users = generateDemoUsers(10); // 10 users with random stories
374
478
  ```
@@ -376,39 +480,39 @@ const users = generateDemoUsers(10); // 10 users with random stories
376
480
  ### Create Custom Stories
377
481
 
378
482
  ```tsx
379
- import type { User } from 'react-instagram-stories';
483
+ import type { User } from "react-instagram-stories";
380
484
 
381
485
  const myUsers: User[] = [
382
486
  {
383
- id: '1',
384
- username: 'johndoe',
385
- avatarUrl: 'https://example.com/avatar.jpg',
487
+ id: "1",
488
+ username: "johndoe",
489
+ avatarUrl: "https://example.com/avatar.jpg",
386
490
  hasUnreadStories: true, // Shows ring around avatar
387
491
  stories: [
388
492
  {
389
- id: 'story-1',
390
- type: 'image',
391
- src: 'https://example.com/photo.jpg',
392
- alt: 'Beach sunset',
493
+ id: "story-1",
494
+ type: "image",
495
+ src: "https://example.com/photo.jpg",
496
+ alt: "Beach sunset",
393
497
  duration: 5000,
394
498
  },
395
499
  {
396
- id: 'story-2',
397
- type: 'video',
398
- src: 'https://example.com/video.mp4',
500
+ id: "story-2",
501
+ type: "video",
502
+ src: "https://example.com/video.mp4",
399
503
  // duration auto-detected
400
504
  },
401
505
  {
402
- id: 'story-3',
403
- type: 'text',
404
- text: 'Hello from my story!',
405
- backgroundColor: '#FF6B6B',
406
- textColor: '#FFFFFF',
506
+ id: "story-3",
507
+ type: "text",
508
+ text: "Hello from my story!",
509
+ backgroundColor: "#FF6B6B",
510
+ textColor: "#FFFFFF",
407
511
  duration: 5000,
408
512
  },
409
513
  {
410
- id: 'story-4',
411
- type: 'custom_component',
514
+ id: "story-4",
515
+ type: "custom_component",
412
516
  component: MyPollComponent,
413
517
  duration: 10000,
414
518
  },
@@ -422,8 +526,8 @@ const myUsers: User[] = [
422
526
  If you don't want URL navigation, use controlled mode:
423
527
 
424
528
  ```tsx
425
- import { useState } from 'react';
426
- import { AvatarList, StoryViewer } from 'react-instagram-stories';
529
+ import { useState } from "react";
530
+ import { AvatarList, StoryViewer } from "react-instagram-stories";
427
531
 
428
532
  function App() {
429
533
  const [viewerState, setViewerState] = useState({
@@ -435,7 +539,9 @@ function App() {
435
539
  <>
436
540
  <AvatarList
437
541
  users={myUsers}
438
- onAvatarClick={(index) => setViewerState({ isOpen: true, userIndex: index })}
542
+ onAvatarClick={(index) =>
543
+ setViewerState({ isOpen: true, userIndex: index })
544
+ }
439
545
  />
440
546
  <StoryViewer
441
547
  users={myUsers}
@@ -458,6 +564,7 @@ function App() {
458
564
 
459
565
  - **Tap Left/Right** - Navigate stories
460
566
  - **Swipe Left/Right** - Change users
567
+ - **Drag Left/Right** - 3D cube transition between users (peek at next/previous user, snaps on release)
461
568
  - **Swipe Down** - Close
462
569
  - **Hold/Hover** - Pause
463
570
 
@@ -472,11 +579,18 @@ import type {
472
579
  ImageStoryItem,
473
580
  VideoStoryItem,
474
581
  TextStoryItem,
475
- CustomComponentStoryItem
476
- } from 'react-instagram-stories';
582
+ CustomComponentStoryItem,
583
+ StoriesClassNames,
584
+ StoryViewerClassNames,
585
+ AvatarListClassNames,
586
+ AvatarClassNames,
587
+ StoryProgressBarsClassNames,
588
+ ProgressBarClassNames,
589
+ StoryItemClassNames,
590
+ } from "react-instagram-stories";
477
591
 
478
592
  // Core Types
479
- type StoryItemType = 'image' | 'video' | 'text' | 'custom_component';
593
+ type StoryItemType = "image" | "video" | "text" | "custom_component";
480
594
 
481
595
  interface StoryItemControls {
482
596
  pause: () => void;
@@ -493,19 +607,58 @@ interface User {
493
607
  stories: StoryItem[];
494
608
  hasUnreadStories?: boolean;
495
609
  }
610
+
611
+ // ClassNames Types (for Tailwind / custom CSS customization)
612
+ interface StoriesClassNames {
613
+ avatarList?: AvatarListClassNames;
614
+ storyViewer?: StoryViewerClassNames;
615
+ }
616
+
617
+ interface AvatarListClassNames {
618
+ root?: string;
619
+ avatar?: AvatarClassNames;
620
+ }
621
+
622
+ interface AvatarClassNames {
623
+ root?: string;
624
+ ring?: string;
625
+ image?: string;
626
+ username?: string;
627
+ }
628
+
629
+ interface StoryViewerClassNames {
630
+ overlay?: string;
631
+ closeButton?: string;
632
+ progressBars?: StoryProgressBarsClassNames;
633
+ storyItem?: StoryItemClassNames;
634
+ }
635
+
636
+ interface StoryProgressBarsClassNames {
637
+ root?: string;
638
+ bar?: ProgressBarClassNames;
639
+ }
640
+
641
+ interface ProgressBarClassNames {
642
+ root?: string;
643
+ fill?: string;
644
+ }
645
+
646
+ interface StoryItemClassNames {
647
+ root?: string;
648
+ }
496
649
  ```
497
650
 
498
651
  ## Package Exports
499
652
 
500
653
  ```tsx
501
654
  // Components
502
- import { Stories, StoryViewer, AvatarList } from 'react-instagram-stories';
655
+ import { Stories, StoryViewer, AvatarList } from "react-instagram-stories";
503
656
 
504
657
  // Navigation helpers
505
- import { navigateWithParams, clearQueryParams } from 'react-instagram-stories';
658
+ import { navigateWithParams, clearQueryParams } from "react-instagram-stories";
506
659
 
507
660
  // Demo data
508
- import { demoUsers, generateDemoUsers } from 'react-instagram-stories';
661
+ import { demoUsers, generateDemoUsers } from "react-instagram-stories";
509
662
 
510
663
  // Types
511
664
  import type {
@@ -516,18 +669,25 @@ import type {
516
669
  ImageStoryItem,
517
670
  VideoStoryItem,
518
671
  TextStoryItem,
519
- CustomComponentStoryItem
520
- } from 'react-instagram-stories';
672
+ CustomComponentStoryItem,
673
+ StoriesClassNames,
674
+ StoryViewerClassNames,
675
+ AvatarListClassNames,
676
+ AvatarClassNames,
677
+ StoryProgressBarsClassNames,
678
+ ProgressBarClassNames,
679
+ StoryItemClassNames,
680
+ } from "react-instagram-stories";
521
681
 
522
682
  // Styles
523
- import 'react-instagram-stories/styles.css';
683
+ import "react-instagram-stories/styles.css";
524
684
  ```
525
685
 
526
686
  ## Performance
527
687
 
528
688
  - **Bundle Size**: ~30 KB (minified)
529
689
  - **Gzipped**: ~10 KB
530
- - **Zero Runtime Dependencies**
690
+ - **Zero Runtime Dependencies**: No production dependencies at all (framer-motion and tailwindcss-animate are dev-only)
531
691
  - **Smart Preloading**: Preloads adjacent stories
532
692
  - **Optimized Rendering**: Uses React.memo
533
693
  - **Video Buffering Detection**: Pauses progress during buffering
@@ -536,6 +696,7 @@ import 'react-instagram-stories/styles.css';
536
696
 
537
697
  - React 18+
538
698
  - TypeScript
699
+ - CSS 3D Transforms (cube transition between users)
539
700
  - Native Browser History API (no router needed)
540
701
  - tsup (bundler)
541
702