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 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-spin-roulette

v1.0.7

Published

A lightweight, performant, and type-safe linear roulette/spinner component for React with headless UI support

Readme

🎰 React Spin Roulette

A lightweight, performant, and fully type-safe linear roulette/spinner component for React with headless UI support.

npm version License: MIT TypeScript Bundle Size

🎮 Try Interactive Examples →

react-spin-roulette.pages.dev - Play with live demos and copy code snippets!


📋 Table of Contents


🎯 Use Cases

Perfect for creating engaging interactive experiences:

  • 🎁 Prize Wheels - Raffle draws, giveaways, contests
  • 🎰 Slot Machines - Game mechanics, gambling simulations
  • 🏆 Lottery Systems - Number pickers, random selections
  • 🎮 Game Elements - Rewards, loot boxes, item drops
  • 📱 Marketing - Promotional campaigns, discount wheels
  • 🎓 Education - Random student selector, quiz games

✨ Features

  • 🎨 Headless UI - Full control over styling (Tailwind, CSS-in-JS, or plain CSS)
  • 📐 Horizontal & Vertical - Support for both orientations
  • Performance First - Handles 30,000+ items with smart sampling
  • 🛡️ 100% Type-Safe - Built with TypeScript strict mode
  • 🪶 Lightweight - Zero dependencies (except React)
  • 🎯 Custom Indicators - Add your own pointer/marker design
  • 🔄 Auto Spin Control - Intelligent rotation calculation
  • Accessible - ARIA labels and semantic HTML
  • 🧪 Well Tested - Comprehensive test coverage
  • 📦 Tree-Shakeable - Import only what you need
  • 🚀 Easy to Use - Simple API with smart defaults

📦 Installation

# npm
npm install react-spin-roulette

# yarn
yarn add react-spin-roulette

# pnpm
pnpm add react-spin-roulette

Requirements: React 17+ or 18+

🚀 Quick Start

Basic Example

import { SpinRoulette } from 'react-spin-roulette';

const prizes = [
  { id: '1', label: 'Prize 1', image: '/prize1.png' },
  { id: '2', label: 'Prize 2', image: '/prize2.png' },
  { id: '3', label: 'Prize 3', image: '/prize3.png' },
  { id: '4', label: 'Prize 4', image: '/prize4.png' },
];

function App() {
  const [winningIndex, setWinningIndex] = useState(0);
  const [isSpinning, setIsSpinning] = useState(false);

  const handleSpin = () => {
    // Generate random winning index
    const randomIndex = Math.floor(Math.random() * prizes.length);
    setWinningIndex(randomIndex);
    setIsSpinning(true);
  };

  const handleComplete = () => {
    setIsSpinning(false);
    console.log('Winner:', prizes[winningIndex]);
  };

  return (
    <div className="flex flex-col items-center gap-4">
      <SpinRoulette
        prizes={prizes}
        winningIndex={winningIndex}
        isSpinning={isSpinning}
        onComplete={handleComplete}
        orientation="horizontal"
        className="w-full h-32 border-2 border-gray-300 rounded-lg"
      />
      <button
        onClick={handleSpin}
        disabled={isSpinning}
        className="px-6 py-3 bg-blue-600 text-white rounded-lg"
      >
        {isSpinning ? 'Spinning...' : 'Spin!'}
      </button>
    </div>
  );
}

With Custom Styling

<SpinRoulette
  prizes={prizes}
  winningIndex={winningIndex}
  isSpinning={isSpinning}
  onComplete={handleComplete}
  className="w-full h-40 bg-gray-900 rounded-2xl"
  prizeClassName="flex items-center justify-center bg-gradient-to-r from-purple-600 to-pink-600 text-white font-bold border-r-2 border-white/20"
  duration={3000}
  minSpins={3}
/>

🎨 How It Works

Unlike circular wheel spinners, React Spin Roulette works like a slot machine or linear slider:

  • Prizes scroll horizontally (left/right) or vertically (up/down)
  • The winning prize stops in the center viewport
  • Multiple complete rotations before landing (like a real lottery)
  • Smart sampling automatically handles huge datasets (30k+ items)
  • Smooth CSS transform-based animations with GPU acceleration

🎬 Live Demo & Examples

🌟 Try Interactive Examples → 🌟

Visit react-spin-roulette.pages.dev to:

  • 🎮 Test all features - Interactive playground with live examples
  • 🎨 See styling options - Horizontal/Vertical orientations, custom styles
  • 🎯 Try custom indicators - Different pointer designs
  • 📊 Performance demos - Test with small and large datasets
  • 💡 Copy code snippets - Ready-to-use examples for your project

📂 View Example Source Code → - Complete example app with all demos

🎨 Styling

With Tailwind CSS

<SpinRoulette
  prizes={prizes}
  winningIndex={winningIndex}
  isSpinning={isSpinning}
  onComplete={handleComplete}
  orientation="horizontal"
  className="w-full h-32 border-2 border-gray-300 rounded-lg overflow-hidden"
  prizeClassName="flex items-center justify-center bg-gradient-to-r from-purple-500 to-pink-500 text-white font-bold"
  indicatorClassName="absolute top-0 left-1/2 -translate-x-1/2 w-1 h-full bg-red-500"
/>

With Custom CSS

<SpinRoulette
  prizes={prizes}
  winningIndex={winningIndex}
  isSpinning={isSpinning}
  onComplete={handleComplete}
  orientation="vertical"
  className="custom-roulette"
  prizeClassName="custom-prize"
/>
.custom-roulette {
  width: 300px;
  height: 400px;
  border: 2px solid #e5e7eb;
  border-radius: 12px;
  overflow: hidden;
  position: relative;
}

.custom-prize {
  display: flex;
  align-items: center;
  justify-center;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  font-weight: 600;
  padding: 1rem;
}

📖 API Reference

SpinRoulette Props

| Prop | Type | Required | Default | Description | | -------------------- | ----------------------------- | -------- | ---------------------------------- | ----------------------------------------------------------------------------- | | prizes | Prize[] | ✅ | - | Array of prize objects | | winningIndex | number | ✅ | - | Index of the winning prize (must be valid) | | isSpinning | boolean | ✅ | - | Whether the roulette is currently spinning | | onComplete | () => void | ❌ | undefined | Callback when spin animation completes | | onSpinStart | () => void | ❌ | undefined | Callback when spin animation starts | | duration | number | ❌ | 5000 | Spin duration in milliseconds | | easing | string | ❌ | cubic-bezier(0.25, 0.1, 0.25, 1) | CSS easing function | | orientation | 'horizontal' \| 'vertical' | ❌ | 'horizontal' | Scroll direction | | prizeSize | number | ❌ | 150 | Size of each prize item (width for horizontal, height for vertical) in pixels | | minSpins | number | ❌ | 5 | Number of complete rotations before landing on winner | | className | string | ❌ | undefined | CSS class for the container | | prizeClassName | string | ❌ | undefined | CSS class for each prize item | | indicatorClassName | string | ❌ | undefined | CSS class for the center indicator/pointer | | renderPrize | (prize: Prize) => ReactNode | ❌ | undefined | Custom prize renderer | | renderIndicator | () => ReactNode | ❌ | undefined | Custom indicator/marker renderer |

Prize Type

interface Prize {
  id: string; // Unique identifier (required)
  label: string; // Display text (required)
  image?: string; // Optional image URL
  value?: any; // Optional value associated with the prize
  className?: string; // Optional CSS class for this specific prize
  style?: CSSProperties; // Optional inline styles for this prize
}

🎯 Advanced Examples

Custom Prize Rendering

<SpinRoulette
  prizes={prizes}
  winningIndex={winningIndex}
  isSpinning={isSpinning}
  onComplete={handleComplete}
  renderPrize={(prize) => (
    <div className="flex flex-col items-center p-4 gap-2">
      <img
        src={prize.image}
        alt={prize.label}
        className="w-16 h-16 rounded-full object-cover"
      />
      <span className="text-sm font-bold text-center">{prize.label}</span>
      {prize.value && <span className="text-xs text-gray-500">{prize.value} points</span>}
    </div>
  )}
/>

Vertical Orientation

<SpinRoulette
  prizes={prizes}
  winningIndex={winningIndex}
  isSpinning={isSpinning}
  onComplete={handleComplete}
  orientation="vertical"
  prizeSize={120}
  className="w-64 h-96"
/>

Custom Indicator/Marker

Create your own pointer design:

<SpinRoulette
  prizes={prizes}
  winningIndex={winningIndex}
  isSpinning={isSpinning}
  onComplete={handleComplete}
  renderIndicator={() => (
    <div
      style={{
        position: 'absolute',
        left: '50%',
        top: 0,
        transform: 'translateX(-50%)',
        zIndex: 20,
      }}
    >
      {/* Custom arrow pointing down */}
      <div
        style={{
          width: 0,
          height: 0,
          borderLeft: '24px solid transparent',
          borderRight: '24px solid transparent',
          borderTop: '36px solid #f97316',
          filter: 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3))',
        }}
      />
    </div>
  )}
/>

Control Spin Rotations

Customize how many times the roulette spins before stopping:

<SpinRoulette
  prizes={prizes}
  winningIndex={winningIndex}
  isSpinning={isSpinning}
  onComplete={handleComplete}
  minSpins={3} // 3 complete rotations (default: 5)
  duration={3000} // Faster 3-second spin
/>

With Sound Effects

import useSound from 'use-sound';

function App() {
  const [playSpinSound] = useSound('/sounds/spin.mp3');
  const [playWinSound] = useSound('/sounds/win.mp3');

  const handleSpinStart = () => {
    playSpinSound();
  };

  const handleComplete = () => {
    playWinSound();
    setIsSpinning(false);
  };

  return (
    <SpinRoulette
      prizes={prizes}
      winningIndex={winningIndex}
      isSpinning={isSpinning}
      onSpinStart={handleSpinStart}
      onComplete={handleComplete}
    />
  );
}

Huge Datasets - Performance Optimization

React Spin Roulette automatically handles massive datasets efficiently! ✨

// Works perfectly with 30,000+ items!
const hugePrizes = Array.from({ length: 30000 }, (_, i) => ({
  id: `prize-${i}`,
  label: `Item ${i + 1}`,
}));

<SpinRoulette
  prizes={hugePrizes} // 🚀 No performance issues!
  winningIndex={randomIndex}
  isSpinning={isSpinning}
  onComplete={handleComplete}
  duration={5000}
  minSpins={3}
/>;

How it works:

  • 🎯 Smart Sampling: Automatically creates a visual subset for large datasets
  • 💡 Intelligent Rendering: Only renders what's needed for the animation
  • Zero Lag: Maintains smooth 60fps animation
  • 🧠 Automatic Detection: Kicks in seamlessly for datasets > 100 items
  • 🎲 Accurate Results: Always lands on the correct winning item

⚙️ Configuration

TypeScript

This package is written in TypeScript and provides full type definitions out of the box. No need for @types/* packages!

import type { Prize, SpinRouletteProps } from 'react-spin-roulette';

Performance Tips

  1. Memoize prize arrays: Use useMemo to prevent re-creating the prize array
  2. Large datasets: Works seamlessly with 30k+ items (auto-optimization)
  3. GPU acceleration: Uses translate3d() for hardware-accelerated animations
  4. Avoid inline styles: Use className props for better performance
  5. Consistent spin duration: Animation timing is precise regardless of dataset size
// ✅ Good - memoized
const prizes = useMemo(() => generatePrizes(), []);

// ❌ Bad - recreated every render
const prizes = generatePrizes();

Browser Support

  • ✅ Chrome/Edge 90+
  • ✅ Firefox 88+
  • ✅ Safari 14+
  • ✅ React 17+, 18+
  • ✅ Modern mobile browsers (iOS Safari, Chrome Mobile)

🧪 Testing

# Run tests
npm test

# Run tests in watch mode
npm run test:watch

# Generate coverage report
npm run test:coverage

❓ FAQ

No problem! The component automatically optimizes rendering for large datasets (30k+ items tested). Just pass your array normally:

const prizes = Array.from({ length: 10000 }, (_, i) => ({
  id: `prize-${i}`,
  label: `Prize ${i + 1}`,
}));

<SpinRoulette prizes={prizes} {...otherProps} />;

Absolutely! The component is headless - use any styling solution:

<SpinRoulette
  className="my-custom-roulette"
  prizeClassName="my-custom-prize"
  {...props}
/>

Use the duration and minSpins props:

<SpinRoulette
  duration={2000} // 2 seconds (faster)
  minSpins={3} // 3 rotations instead of 5
  {...otherProps}
/>

Yes! The component is SSR-friendly and works with Next.js, Remix, etc.

Control the isSpinning prop from your state:

const [isSpinning, setIsSpinning] = useState(false);

const triggerSpin = () => {
  setWinningIndex(Math.floor(Math.random() * prizes.length));
  setIsSpinning(true);
};

<SpinRoulette
  isSpinning={isSpinning}
  onComplete={() => setIsSpinning(false)}
  {...otherProps}
/>;

🛠️ Troubleshooting

Prizes not rendering?

  • Ensure each prize has a unique id
  • Check that prizes array is not empty
  • Verify container has width/height

Animation is jumpy?

  • Make sure prizes array is memoized with useMemo
  • Avoid changing winningIndex while spinning

TypeScript errors?

  • Update to React 17+ and TypeScript 4.5+
  • Ensure @types/react is installed

🤝 Contributing

Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.

Development

# Install dependencies
npm install

# Run tests
npm test

# Build library
npm run build

# Run example app
cd example && npm install && npm run dev

📄 License

MIT © [Your Name]

💡 Why Choose React Spin Roulette?

| Feature | React Spin Roulette | Other Libraries | | ------------------- | ------------------- | --------------------- | | 🎨 Headless UI | ✅ Full control | ❌ Fixed styles | | 📊 Large Datasets | ✅ 30k+ items | ❌ Performance issues | | 🛡️ TypeScript | ✅ 100% strict | ⚠️ Partial or none | | 🧪 Tests | ✅ Comprehensive | ⚠️ Limited | | 📦 Bundle Size | ✅ ~5KB gzipped | ❌ 15-50KB+ | | 🎯 Custom Indicator | ✅ Yes | ❌ No | | 🔄 Spin Control | ✅ Configurable | ⚠️ Fixed | | ⚡ Performance | ✅ GPU-accelerated | ⚠️ Varies |

🙏 Acknowledgments

Inspired by react-roulette-pro by @IvanAdmaers.

This project is a complete ground-up rewrite focusing on:

  • Headless UI architecture for complete styling flexibility
  • 🛡️ 100% TypeScript with strict mode and comprehensive type safety
  • Performance-first implementation with smart sampling for huge datasets
  • 🧪 Test-driven development with comprehensive coverage
  • 📦 Zero dependencies (except React peer dependency)
  • 🎯 Clean, maintainable code with extensive English documentation
  • 🚀 Modern React patterns (hooks, memoization, GPU acceleration)

While inspired by the original concept, this is a completely independent implementation with significant architectural improvements.

🔗 Links

📊 Stats

npm downloads GitHub stars GitHub issues


Made with ❤️ and TypeScript

If you find this library helpful, please ⭐ star it on GitHub!