npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@farzullali/react-native-story-viewer

v0.1.7

Published

A fully customizable Instagram-like story viewer component for React Native with gesture support, animations, and flexible rendering options

Readme

React Native Story Viewer

A fully customizable Instagram-like story viewer component for React Native with gesture support, animations, and flexible rendering options.

Features

  • 📱 Instagram-style story viewer with swipe gestures
  • ⏸️ Tap and hold to pause stories
  • 🎨 Fully customizable components (header, progress, content, footer)
  • 🔄 Horizontal swipe between users
  • ⬇️ Vertical swipe to close
  • ⚡ Smooth animations with Reanimated 4
  • 🎯 TypeScript support
  • 📦 Zero external UI dependencies

Requirements

| Package | Version | | ---------------------------- | -------- | | React Native | >=0.70.0 | | React | >=18.0.0 | | react-native-gesture-handler | >=2.0.0 | | react-native-reanimated | >=4.0.0 |

Tested With

  • React Native: 0.82.1
  • React: 19.1.1
  • react-native-gesture-handler: ^2.29.1
  • react-native-reanimated: ^4.1.5
  • react-native-worklets: 0.6.1

New Architecture

This component is compatible with React Native's New Architecture but does not require it.

Installation

# Using npm
npm install react-native-gesture-handler react-native-reanimated

# Using yarn
yarn add react-native-gesture-handler react-native-reanimated

# Using pnpm
pnpm add react-native-gesture-handler react-native-reanimated

iOS Setup

cd ios && pod install

Babel Configuration

Add Reanimated's babel plugin to your babel.config.js:

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    'react-native-worklets/plugin', // Must be last!
  ],
};

Android Setup

Make sure gesture handler is properly set up in MainActivity.java:

package com.yourapp;

import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
import android.os.Bundle; // Add this
import com.facebook.react.ReactRootView; // Add this

public class MainActivity extends ReactActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(null); // Add this for gesture handler
  }

  // ... rest of your code
}

Basic Usage

import React, { useState } from 'react';
import { View, Button } from 'react-native';
import { StoryViewer } from './src/StoryViewer';
import type { StoryUser } from './src/StoryViewer';

export default function App() {
  const [visible, setVisible] = useState(false);

  const users: StoryUser[] = [
    {
      id: '1',
      name: 'John Doe',
      avatar: 'https://example.com/avatar1.jpg',
      stories: [
        {
          id: 's1',
          type: 'image',
          url: 'https://example.com/story1.jpg',
          duration: 5000, // Optional: 5 seconds
        },
        {
          id: 's2',
          type: 'image',
          url: 'https://example.com/story2.jpg',
        },
      ],
    },
    {
      id: '2',
      name: 'Jane Smith',
      avatar: 'https://example.com/avatar2.jpg',
      stories: [
        {
          id: 's3',
          type: 'image',
          url: 'https://example.com/story3.jpg',
        },
      ],
    },
  ];

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Button title="Show Stories" onPress={() => setVisible(true)} />

      <StoryViewer
        users={users}
        visible={visible}
        onClose={() => setVisible(false)}
        onStoryView={(userId, storyId) => {
          console.log(`Viewed story ${storyId} from user ${userId}`);
        }}
      />
    </View>
  );
}

Props

StoryViewerProps

| Prop | Type | Required | Default | Description | | ------------------------ | ---------------------------------------------------------- | -------- | ------- | --------------------------------- | | users | StoryUser[] | Yes | - | Array of users with their stories | | visible | boolean | Yes | - | Controls modal visibility | | onClose | () => void | Yes | - | Callback when viewer closes | | initialUserIndex | number | No | 0 | Index of user to start with | | onStoryView | (userId: string, storyId: string) => void | No | - | Callback when a story is viewed | | defaultStoryDuration | number | No | 5000 | Default duration for stories (ms) | | renderHeader | (props: StoryRenderProps) => ReactNode | No | - | Custom header component | | renderProgress | (props: StoryRenderProps) => ReactNode | No | - | Custom progress bar | | renderContent | (props: StoryRenderProps) => ReactNode | No | - | Custom story content | | renderFooter | (props: StoryRenderProps) => ReactNode | No | - | Custom footer component | | renderItem | (props: StoryRenderProps & {index: number}) => ReactNode | No | - | Complete custom rendering | | containerStyle | ViewStyle | No | - | Style for main container | | progressContainerStyle | ViewStyle | No | - | Style for progress container | | headerContainerStyle | ViewStyle | No | - | Style for header container | | footerContainerStyle | ViewStyle | No | - | Style for footer container | | swipeAnimationConfig | SwipeAnimationConfig | No | - | Custom swipe animation config |

StoryUser Type

interface StoryUser {
  id: string;
  name: string;
  avatar: string;
  stories: Story[];
}

Story Type

interface Story {
  id: string;
  type: 'image' | 'video'; // Video support coming soon
  url: string;
  duration?: number; // Duration in milliseconds
}

StoryRenderProps Type

interface StoryRenderProps {
  user: StoryUser;
  story: Story;
  currentStoryIndex: number;
  totalStories: number;
  progress: number; // 0 to 1
  isCurrentUser: boolean;
  onClose: () => void;
  onNext: () => void;
  onPrev: () => void;
  onPause: () => void;
  onResume: () => void;
}

SwipeAnimationConfig Type

type SwipeAnimationType = 'default' | 'fade' | 'scale' | 'cube' | 'custom';

interface SwipeAnimationConfig {
  type?: SwipeAnimationType;
  duration?: number; // Animation duration in milliseconds (default: 250)
  customAnimation?: (
    index: number,
    scrollOffset: number,
    itemWidth: number,
  ) => {
    opacity?: number;
    transform?: Array<{
      scale?: number;
      translateX?: number;
      rotateY?: string;
    }>;
  };
}

Advanced Usage

Custom Duration

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  defaultStoryDuration={3000} // 3 seconds per story
/>

Custom Header

import { Text, View, TouchableOpacity } from 'react-native';

const CustomHeader = ({ user, onClose }) => (
  <View style={{ flexDirection: 'row', padding: 16 }}>
    <Text style={{ color: '#fff', fontSize: 18 }}>{user.name}</Text>
    <TouchableOpacity onPress={onClose} style={{ marginLeft: 'auto' }}>
      <Text style={{ color: '#fff', fontSize: 24 }}>×</Text>
    </TouchableOpacity>
  </View>
);

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  renderHeader={CustomHeader}
/>;

Custom Progress Bar

const CustomProgress = ({ totalStories, currentStoryIndex, progress }) => (
  <View style={{ flexDirection: 'row', gap: 4, paddingHorizontal: 16 }}>
    {Array.from({ length: totalStories }).map((_, index) => (
      <View
        key={index}
        style={{
          flex: 1,
          height: 3,
          backgroundColor: 'rgba(255,255,255,0.3)',
          borderRadius: 2,
        }}
      >
        <View
          style={{
            width:
              index < currentStoryIndex
                ? '100%'
                : index === currentStoryIndex
                ? `${progress * 100}%`
                : '0%',
            height: '100%',
            backgroundColor: '#fff',
            borderRadius: 2,
          }}
        />
      </View>
    ))}
  </View>
);

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  renderProgress={CustomProgress}
/>;

Custom Footer

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  renderFooter={({ user, story }) => (
    <View style={{ padding: 20, backgroundColor: 'rgba(0,0,0,0.5)' }}>
      <Text style={{ color: '#fff' }}>Posted by {user.name}</Text>
      <Text style={{ color: '#aaa', fontSize: 12 }}>2 hours ago</Text>
    </View>
  )}
/>

Complete Custom Rendering

const CustomStoryItem = ({ user, story, onClose, onNext, onPrev, index }) => (
  <View style={{ flex: 1, backgroundColor: '#1a1a1a' }}>
    {/* Your completely custom layout */}
    <Image source={{ uri: story.url }} style={{ flex: 1 }} />
    <View style={{ padding: 20 }}>
      <Text style={{ color: '#fff' }}>{user.name}</Text>
    </View>
  </View>
);

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  renderItem={CustomStoryItem}
/>;

Custom Styles

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  containerStyle={{ backgroundColor: '#1a1a1a' }}
  headerContainerStyle={{ top: 80 }}
  progressContainerStyle={{ paddingHorizontal: 20 }}
  footerContainerStyle={{ bottom: 40 }}
/>

Custom Properties in Stories and Users

You can add any custom properties to your Story and StoryUser objects, and they will be accessible in all render methods. This is useful for passing additional metadata, configuration, or custom data.

Example: Adding Custom Properties

const users: StoryUser[] = [
  {
    id: '1',
    name: 'John Doe',
    avatar: 'https://example.com/avatar.jpg',
    // Custom user-level properties
    verified: true,
    followers: 1234,
    customUserData: { role: 'admin' },
    stories: [
      {
        id: 's1',
        type: 'image',
        url: 'https://example.com/story1.jpg',
        duration: 5000,
        // Custom story-level properties
        test: 'Custom test value',
        location: 'New York',
        likes: 42,
        tags: ['travel', 'food'],
        customData: {
          productId: '12345',
          price: 99.99,
          inStock: true,
        },
      },
    ],
  },
];

Accessing Custom Properties in Render Methods

All custom properties are available through the story and user objects in your render methods:

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  // Access custom properties in any render method
  renderFooter={({ story, user }) => {
    // Access custom story properties
    const testProp = story.test;
    const location = story.location;
    const customData = story.customData;

    // Access custom user properties
    const isVerified = user.verified;
    const followers = user.followers;

    return (
      <View style={{ padding: 20, backgroundColor: 'rgba(0,0,0,0.7)' }}>
        {testProp && <Text style={{ color: '#fff' }}>Test: {testProp}</Text>}
        {location && <Text style={{ color: '#fff' }}>📍 {location}</Text>}
        {customData && (
          <View>
            <Text style={{ color: '#fff' }}>Product: ${customData.price}</Text>
            <Text style={{ color: customData.inStock ? '#0f0' : '#f00' }}>
              {customData.inStock ? 'In Stock' : 'Out of Stock'}
            </Text>
          </View>
        )}
        <Text style={{ color: '#aaa' }}>
          {user.name} {isVerified && '✓'} • {followers} followers
        </Text>
      </View>
    );
  }}
  // Also available in renderHeader, renderContent, renderProgress, and renderItem
  renderHeader={({ story, user }) => (
    <View>
      <Text style={{ color: '#fff' }}>
        {user.name} {user.verified && '✓'}
      </Text>
      {story.tags && (
        <Text style={{ color: '#aaa' }}>{story.tags.join(' • ')}</Text>
      )}
    </View>
  )}
/>

Use Cases for Custom Properties

  • E-commerce: Add product IDs, prices, inventory status
  • Analytics: Track engagement metrics, view counts, likes
  • Metadata: Location tags, timestamps, categories
  • Features: Verification badges, premium content flags
  • Deep linking: URLs, navigation parameters
  • Localization: Language-specific content, translations
  • A/B Testing: Experiment IDs, variant information

TypeScript Support

The Story and StoryUser interfaces support index signatures, allowing any additional properties while maintaining type safety for the required fields:

interface Story {
  id: string;
  type: 'image' | 'video';
  url: string;
  duration?: number;
  [key: string]: any; // Allows any custom properties
}

interface StoryUser {
  id: string;
  name: string;
  avatar: string;
  stories: Story[];
  [key: string]: any; // Allows any custom properties
}

Custom Swipe Animations

The library provides built-in swipe animation types and supports custom animations when transitioning between users.

Built-in Animation Types

Default (no animation)

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  swipeAnimationConfig={{ type: 'default' }}
/>

Fade Animation

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  swipeAnimationConfig={{
    type: 'fade',
    duration: 300,
  }}
/>

Scale Animation

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  swipeAnimationConfig={{
    type: 'scale',
    duration: 250,
  }}
/>

Cube Animation (3D flip effect)

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  swipeAnimationConfig={{
    type: 'cube',
    duration: 400,
  }}
/>

Custom Animation

Create your own animation by providing a custom function. Important: The custom animation function must be a worklet (add 'worklet'; directive).

<StoryViewer
  users={users}
  visible={visible}
  onClose={() => setVisible(false)}
  swipeAnimationConfig={{
    type: 'custom',
    customAnimation: (index, scrollOffset, itemWidth) => {
      'worklet';
      // Calculate the position relative to the current view
      const position = scrollOffset - index * itemWidth;
      const progress = position / itemWidth;

      // Example: Combined fade and scale effect
      const opacity = 1 - Math.abs(progress) * 0.8;
      const scale = 1 - Math.abs(progress) * 0.3;

      return {
        opacity: Math.max(0.2, Math.min(1, opacity)),
        transform: [{ scale: Math.max(0.7, Math.min(1, scale)) }],
      };
    },
  }}
/>

Custom Animation with Rotation

swipeAnimationConfig={{
  type: 'custom',
  customAnimation: (index, scrollOffset, itemWidth) => {
    'worklet';
    const position = scrollOffset - index * itemWidth;
    const progress = position / itemWidth;

    return {
      transform: [
        { perspective: 1000 },
        { rotateY: `${progress * 45}deg` },
        { scale: 1 - Math.abs(progress) * 0.1 },
      ],
    };
  },
}}

Gestures

| Gesture | Action | | ---------------- | ---------------------- | | Tap right side | Next story | | Tap left side | Previous story | | Hold anywhere | Pause story | | Swipe left/right | Navigate between users | | Swipe down | Close viewer |

Examples

Check out the src/StoryViewer/examples/ directory for more examples:

  • CustomRenderExample.tsx - Various customization examples

Troubleshooting

ERESOLVE dependency conflict during installation

If you encounter a dependency resolution error during installation:

npm error ERESOLVE unable to resolve dependency tree

You can install the package using one of these approaches:

Option 1: Use legacy peer deps (recommended)

npm install @farzullali/react-native-story-viewer --legacy-peer-deps

Option 2: Force installation

npm install @farzullali/react-native-story-viewer --force

Option 3: Update react-native-worklets

If you're using React Native Reanimated 4.x, updating to the latest worklets version may resolve conflicts:

npm install react-native-worklets@^0.7.1

Stories not animating

Make sure you've added the Reanimated babel plugin as the last plugin in your babel.config.js.

Gestures not working

Ensure react-native-gesture-handler is properly installed and configured in your MainActivity.java (Android) and AppDelegate.mm (iOS).

TypeScript errors

Make sure you're using TypeScript 4.0 or higher and have @types/react and @types/react-native installed.

License

MIT

Contributing

Contributions are welcome! Please open an issue or submit a pull request.