Okay, let's refactor this blog post into a more engaging and comprehensive narrative. We'll focus on telling the story behind the component, diving deeper into the technical decisions and challenges, and making it a valuable read for developers interested in creating and publishing their own React Native components.

---
title: "From Scratch to NPM Star: The Making of `rn-vertical-slider` with React Native Gesture Handler & Reanimated"
publishedAt: "2024-01-29" # Or update if republishing
meta_description: "The complete journey of creating `rn-vertical-slider`, a custom React Native component, from identifying a need to publishing on NPM. A deep dive into Gesture Handler, Reanimated 2, and component design."
summary: "Follow the story of `rn-vertical-slider`, a performant vertical slider for React Native. This post details its conception, technical architecture using Gesture Handler & Reanimated 2, implementation challenges, and the process of publishing to NPM."
keywords: "React Native, Custom Component, NPM Package, Vertical Slider, React Native Gesture Handler, Reanimated 2, UI Development, Open Source, TypeScript"
---
 
Every so often in a React Native project, you hit a wall: a specific UI element is needed, but existing solutions just don't cut it. That's exactly how **`rn-vertical-slider`** was born. I needed a performant, customizable vertical slider, and the search for a perfect fit came up short. This wasn't just about filling a gap; it was an opportunity to dive deep into the intricacies of gesture handling and animation with React Native's modern power tools: **Gesture Handler** and **Reanimated 2**. This is the story of that journey, from a simple idea to a published NPM package.
 
## The Genesis: Why Another Slider?
 
The React Native ecosystem is rich, but when I scoured for a vertical slider, I found most existing options were:
*   **Primarily Horizontal:** Vertical orientation often felt like an afterthought.
*   **Performance Bottlenecks:** Many relied on the older `Animated` API or PanResponder, which could lead to jank on the JS thread during fast interactions.
*   **Limited Customization:** Styling options were often restrictive, making it hard to match app-specific designs.
*   **Outdated or Unmaintained:** Some hadn't kept pace with the latest React Native versions or modern animation libraries.
 
I needed something that was **performant by default, highly customizable, and built with the latest best practices** in mind. This was the spark that ignited the development of `rn-vertical-slider`.
 
## The Technical Foundation: Choosing the Right Tools
 
To achieve the desired fluidity and responsiveness, the choice of core dependencies was critical:
 
1.  **`react-native-gesture-handler`:** For handling touch interactions. Its ability to run gesture logic on the native UI thread is paramount for smooth, jank-free performance, especially for continuous gestures like dragging.
2.  **`react-native-reanimated` (v2+):** For animations. Reanimated 2's worklet-based architecture allows animation logic to also run on the UI thread, perfectly complementing Gesture Handler. Shared Values (`useSharedValue`) and animation modifiers (`withSpring`, `withTiming`) provide a powerful and declarative way to create complex animations.
 
```jsx
// Core dependencies that make the magic happen
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; // Gesture Handler v2+
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  useAnimatedGestureHandler, // Older API, but Gesture.Pan() with .onUpdate etc. is preferred for v2
  withSpring,
  withTiming,
  runOnJS, // To call JS functions from UI thread worklets
} from 'react-native-reanimated';
import { View, StyleSheet, ViewStyle } from 'react-native'; // Basic RN components

Note: While useAnimatedGestureHandler was prominent in earlier Reanimated 2 versions, the Gesture.Pan().onStart().onUpdate().onEnd() API from Gesture Handler v2 is now the more common and often preferred approach for declarative gesture definitions that integrate seamlessly with Reanimated shared values.

The Nuts and Bolts: Implementation Deep Dive

Building a slider involves more than just moving a thumb; it's about precise value calculation, respecting boundaries, and providing clear feedback.

1. Core Slider Logic: Responding to Touch

The heart of the slider is how it translates touch gestures into position and value changes.

// Simplified core logic within the VerticalSlider component
// (Assumes SLIDER_HEIGHT is the visible height of the slider track)
 
export interface VerticalSliderProps {
  value?: number;
  minimumValue?: number;
  maximumValue?: number;
  onValueChange?: (value: number) => void;
  onSlidingComplete?: (value: number) => void;
  step?: number;
  disabled?: boolean;
  height?: number; // Allow custom height for the track
  // Style props
  style?: ViewStyle; 
  trackStyle?: ViewStyle;
  thumbStyle?: ViewStyle;
  minimumTrackStyle?: ViewStyle; // Style for the filled part of the track
  // Color props for easier theming
  minimumTrackTintColor?: string;
  maximumTrackTintColor?: string;
  thumbTintColor?: string;
  // Accessibility
  accessibilityLabel?: string;
}
 
export const VerticalSlider: React.FC<VerticalSliderProps> = ({
  value: initialValue = 0,
  minimumValue = 0,
  maximumValue = 100,
  onValueChange,
  onSlidingComplete,
  step = 1,
  disabled = false,
  height: sliderHeight = 200, // Default slider height
  // ... other props
}) => {
  const THUMB_HEIGHT = 30; // Example thumb height
 
  // Convert initial value to initial translation
  const valueToTranslation = (val: number) => {
    'worklet';
    const clampedVal = Math.min(Math.max(val, minimumValue), maximumValue);
    const percentage = (clampedVal - minimumValue) / (maximumValue - minimumValue);
    // Invert for vertical: 0% value = top (max translation), 100% value = bottom (0 translation)
    return (1 - percentage) * (sliderHeight - THUMB_HEIGHT); 
  };
 
  const translationY = useSharedValue(valueToTranslation(initialValue));
  const startY = useSharedValue(0); // To store initial Y on gesture start
  const currentValue = useSharedValue(initialValue); // To store the current calculated value
 
  // Update translation when initialValue prop changes externally
  useEffect(() => {
    translationY.value = withTiming(valueToTranslation(initialValue), { duration: 100 });
    currentValue.value = initialValue;
  }, [initialValue, minimumValue, maximumValue, sliderHeight]);
 
  const panGesture = Gesture.Pan()
    .enabled(!disabled)
    .onBegin(() => {
      startY.value = translationY.value; // Capture current translation
    })
    .onUpdate((event) => {
      let newTranslation = startY.value + event.translationY;
      // Clamp translation within bounds (0 to sliderHeight - THUMB_HEIGHT)
      newTranslation = Math.max(0, Math.min(newTranslation, sliderHeight - THUMB_HEIGHT));
      translationY.value = newTranslation;
 
      // Calculate new value based on translation
      const rawValue = calculateValue(
        newTranslation,
        minimumValue,
        maximumValue,
        sliderHeight - THUMB_HEIGHT,
        THUMB_HEIGHT // Pass thumb height for precise calc
      );
      const steppedValue = snapToStep(rawValue, step, minimumValue);
      currentValue.value = steppedValue; // Update shared value for potential use in worklets
 
      // Notify JS thread if onValueChange is provided
      if (onValueChange) {
        runOnJS(onValueChange)(steppedValue);
      }
    })
    .onEnd(() => {
      // Optional: Add spring effect on release or other end-gesture logic
      // translationY.value = withSpring(translationY.value, { damping: 15 });
      if (onSlidingComplete) {
        runOnJS(onSlidingComplete)(currentValue.value);
      }
    });
 
  const animatedThumbStyle = useAnimatedStyle(() => {
    return {
      transform: [{ translateY: translationY.value }],
    };
  });
  
  // Animated style for the minimum track (filled portion)
  const animatedMinimumTrackStyle = useAnimatedStyle(() => {
    const trackHeight = sliderHeight - translationY.value - THUMB_HEIGHT / 2;
    return {
      height: Math.max(0, trackHeight), // Ensure height is not negative
      // Position it from the bottom up to the thumb's center
      bottom: 0, 
    };
  });
 
 
  // The GestureDetector wraps the interactive area
  return (
    <GestureHandlerRootView style={{ flex: 1 }}> {/* Needed for Gesture Handler v2+ */}
      <View style={[styles.container, { height: sliderHeight }, style]}>
        {/* Maximum Track (the unfilled part) */}
        <View style={[styles.track, { backgroundColor: maximumTrackTintColor }]} /> 
        {/* Minimum Track (the filled part) */}
        <Animated.View style={[styles.minimumTrack, { backgroundColor: minimumTrackTintColor }, animatedMinimumTrackStyle]} />
        
        <GestureDetector gesture={panGesture}>
          <Animated.View style={[styles.thumb, { height: THUMB_HEIGHT, width: THUMB_HEIGHT, borderRadius: THUMB_HEIGHT / 2, backgroundColor: thumbTintColor }, thumbStyle, animatedThumbStyle]} testID="slider-thumb" />
        </GestureDetector>
      </View>
    </GestureHandlerRootView>
  );
};

2. Precise Value Calculations (Worklets)

Translating pixel movements to meaningful slider values needs to be precise and also run on the UI thread.

// Helper functions defined as worklets (must have 'worklet'; pragma)
 
function calculateValue(
  currentTranslationY: number,
  min: number,
  max: number,
  trackHeight: number, // Effective track height for calculation
  thumbHeight: number // Consider thumb height if positioning from its top
) {
  'worklet';
  // Invert the ratio for vertical slider: 0 translation (top) = max value, max translation (bottom) = min value
  const ratio = currentTranslationY / trackHeight; 
  // Value = max - (ratio * range)
  return max - ratio * (max - min);
}
 
function snapToStep(value: number, step: number, minimumValue: number) {
  'worklet';
  if (step <= 0) return value; // No snapping if step is invalid
  // Adjust value relative to minimumValue before snapping, then add minimumValue back
  // This ensures snapping aligns correctly if minimumValue is not a multiple of step
  const relativeValue = value - minimumValue;
  const snappedRelative = Math.round(relativeValue / step) * step;
  let finalValue = minimumValue + snappedRelative;
 
  // Ensure the final value is within min/max bounds (especially after snapping)
  // This step should ideally be done after snapping in the calling function
  // finalValue = Math.max(minimumValue, Math.min(finalValue, maximumValue)); // Clamp to min/max
  return finalValue;
}

3. Customizable Styling

A good component needs to be stylable.

// Default styles (can be overridden by props)
const styles = StyleSheet.create({
  container: {
    width: 40, // Default width
    justifyContent: 'center',
    alignItems: 'center',
    // backgroundColor: '#E0E0E0', // Default max track color
    borderRadius: 20,
  },
  track: { // This is the maximum track (background)
    position: 'absolute',
    width: 6, // Default track width
    height: '100%',
    borderRadius: 3,
    // backgroundColor is set by maximumTrackTintColor prop
  },
  minimumTrack: { // This is the filled portion of the track
    position: 'absolute',
    width: 6, // Default track width
    borderRadius: 3,
    // backgroundColor is set by minimumTrackTintColor prop
    // height and bottom are animated
  },
  thumb: {
    position: 'absolute', // Thumb is positioned absolutely based on translationY
    // width, height, borderRadius, backgroundColor are set by props or defaults
    // It's centered horizontally by the container's alignItems: 'center'
  },
});

The Road to NPM: Publishing the Package

Creating the component is one thing; making it easily consumable by others is another.

1. Essential package.json Configuration

This file is the heart of your NPM package.

{
  "name": "rn-vertical-slider",
  "version": "1.2.0", // Increment with each release
  "description": "A performant, customizable vertical slider component for React Native, built with Reanimated 2 and Gesture Handler 2.",
  "main": "lib/commonjs/index.js", // Entry point for CommonJS (Node)
  "module": "lib/module/index.js", // Entry point for ES Modules
  "types": "lib/typescript/index.d.ts", // TypeScript definitions
  "react-native": "src/index.ts", // Source entry point for React Native bundler
  "source": "src/index.ts", // Original source
  "files": [ // Files to include in the NPM package
    "src",
    "lib",
    "android", // If you have native code
    "ios",     // If you have native code
    "cpp",     // If you have C++ code
    "*.podspec", // For iOS native modules
    "README.md",
    "LICENSE"
  ],
  "scripts": {
    "typescript": "tsc --noEmit",
    "lint": "eslint \"**/*.{js,ts,tsx}\"",
    "test": "jest",
    "build": "bob build", // Using react-native-builder-bob for building
    "release": "release-it", // For automating releases
    "example": "yarn --cwd example",
    "pods": "cd example && pod-install --quiet",
    "bootstrap": "yarn example && yarn && yarn pods"
  },
  "keywords": [
    "react-native",
    "slider",
    "vertical-slider",
    "reanimated",
    "gesture-handler",
    "ui-component"
  ],
  "repository": {
    "type": "git",
    "url": "git+https://github.com/your-username/rn-vertical-slider.git"
  },
  "author": "Your Name <your.email@example.com> (https://your-website.com)",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/your-username/rn-vertical-slider/issues"
  },
  "homepage": "https://github.com/your-username/rn-vertical-slider#readme",
  "publishConfig": {
    "registry": "https://registry.npmjs.org/"
  },
  "peerDependencies": { // Crucial: specify what your component relies on
    "react": ">=17.0.0",
    "react-native": ">=0.64.0",
    "react-native-gesture-handler": ">=2.0.0",
    "react-native-reanimated": ">=2.0.0"
  },
  "devDependencies": { // Tools for development and building
    "@commitlint/config-conventional": "^17.0.2",
    "@release-it/conventional-changelog": "^5.0.0",
    "commitlint": "^17.0.2",
    "eslint": "^8.4.1",
    // ... other dev dependencies like TypeScript, Jest, @testing-library/react-native
    "react-native-builder-bob": "^0.20.0",
    "release-it": "^15.0.0"
  },
  "react-native-builder-bob": { // Configuration for react-native-builder-bob
    "source": "src",
    "output": "lib",
    "targets": [
      "commonjs",
      "module",
      ["typescript", { "project": "tsconfig.build.json" }]
    ]
  }
}

2. Robust TypeScript Support

Providing type definitions is a must for modern React Native libraries.

// src/index.ts or a dedicated types.ts file
import type { ViewStyle } from 'react-native';
 
export interface VerticalSliderProps {
  /** Initial value of the slider. Defaults to `minimumValue`. */
  value?: number;
  /** Minimum value of the slider. Defaults to 0. */
  minimumValue?: number;
  /** Maximum value of the slider. Defaults to 100. */
  maximumValue?: number;
  /** Callback continuously called while the value changes. */
  onValueChange?: (value: number) => void;
  /** Callback called when the user finishes dragging the slider. */
  onSlidingComplete?: (value: number) => void;
  /** Step value of the slider. Changes will snap to the closest step. Defaults to 1. Set to 0 to disable stepping. */
  step?: number;
  /** If true, the slider will be disabled and unresponsive to gestures. Defaults to false. */
  disabled?: boolean;
  /** The height of the slider track. Defaults to 200. */
  height?: number;
  /** Style for the main container view. */
  style?: ViewStyle;
  /** Style for the track view (the background bar). */
  trackStyle?: ViewStyle;
  /** Style for the filled portion of the track (below/after the thumb). */
  minimumTrackStyle?: ViewStyle;
  /** Style for the thumb (the draggable part). */
  thumbStyle?: ViewStyle;
  /** Color of the filled portion of the track. */
  minimumTrackTintColor?: string;
  /** Color of the unfilled portion of the track. */
  maximumTrackTintColor?: string;
  /** Color of the thumb. */
  thumbTintColor?: string;
  /** Accessibility label for the slider. */
  accessibilityLabel?: string;
  /** Test ID for automation. */
  testID?: string;
}

Ensuring Quality: Testing and Validation

A reliable component needs thorough testing.

// __tests__/VerticalSlider.test.tsx
import React from 'react';
import { render, fireEvent // Import from @testing-library/react-native
} from '@testing-library/react-native';
import { VerticalSlider, VerticalSliderProps } from '../src/index'; // Adjust path
import { GestureHandlerRootView } from 'react-native-gesture-handler';
 
// Mock Reanimated and Gesture Handler for Jest environment
// This is often complex and requires careful setup.
// See official docs for Jest setup with these libraries.
// jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
// jest.mock('react-native-gesture-handler', () => {
//   const View = require('react-native/Libraries/Components/View/View');
//   return {
//     GestureHandlerRootView: View,
//     GestureDetector: View, // Mocking GestureDetector
//     Gesture: { Pan: () => ({ onBegin: jest.fn(), onUpdate: jest.fn(), onEnd: jest.fn() }) }, // Mocking Pan gesture methods
//   };
// });
 
 
const renderSlider = (props?: Partial<VerticalSliderProps>) => {
  // GestureHandlerRootView is required to wrap components using Gesture Handler
  return render(
    <GestureHandlerRootView style={{ flex: 1 }}>
      <VerticalSlider
        testID="vertical-slider"
        minimumValue={0}
        maximumValue={100}
        height={200}
        {...props}
      />
    </GestureHandlerRootView>
  );
};
 
describe('VerticalSlider', () => {
  it('renders correctly with default values', () => {
    const { getByTestId } = renderSlider();
    expect(getByTestId('slider-thumb')).toBeTruthy(); // Check if thumb is rendered
  });
 
  it('calls onValueChange when dragged (conceptual test)', () => {
    const onValueChangeMock = jest.fn();
    renderSlider({ onValueChange: onValueChangeMock, value: 50 });
 
    // Simulating gestures in Jest with Reanimated/Gesture Handler is non-trivial.
    // This is a conceptual placeholder.
    // You'd typically mock the gesture events or use a higher-level testing approach.
    // For unit tests, you might test the `calculateValue` and `snapToStep` worklets directly.
    
    // Example: Test initial value setting through onValueChange
    // This test needs to be adapted based on how you can trigger or mock gesture events
    // in your testing setup for Reanimated/Gesture Handler.
    
    // For now, let's test if the component initializes with the correct value
    // and if onValueChange is called if the `value` prop changes.
    // (This specific test logic needs to be adapted to a real testing scenario for gestures)
    
    // A better unit test might be to directly test the worklet functions:
    // expect(calculateValueWorklet(0, 0, 100, 200, 30)).toBe(100); // Top position means max value
    // expect(calculateValueWorklet(200 - 30, 0, 100, 200 - 30, 30)).toBe(0); // Bottom position means min value
    
    // For now, we'll just check if the mock function is present.
    expect(onValueChangeMock).not.toHaveBeenCalled(); // Initially not called
  });
  
  it('respects minimum and maximum value boundaries', () => {
    // This would also require more involved gesture simulation or direct worklet testing
  });
 
  it('snaps to step values correctly', () => {
    // Test the snapToStep worklet directly
    // expect(snapToStepWorklet(53, 10, 0)).toBe(50);
    // expect(snapToStepWorklet(57, 10, 0)).toBe(60);
  });
});

Key Lessons From the Trenches

This project was a significant learning experience:

  1. Plan Your API Thoughtfully: Define props and callbacks clearly from the outset. Think about how developers will use your component. Good JSDoc/TSDoc comments are invaluable.
  2. Comprehensive Documentation is King: A clear README with installation instructions, usage examples, and prop descriptions is essential for adoption. An example app within the repository is even better.
  3. Prioritize TypeScript Support: In 2024, it's almost a must-have for any serious library.
  4. Write Meaningful Tests: Cover core logic and common use cases.
  5. Automate Your Build & Release Process: Tools like react-native-builder-bob and release-it (with conventional changelogs) save a ton of time and reduce errors.
  6. Understand Peer Dependencies: Get this right to avoid a world of pain for your users.
  7. Embrace the UI Thread: For smooth gestures and animations, keep logic on the UI thread with Reanimated worklets and Gesture Handler. runOnJS should be used sparingly for callbacks to the JS thread.

Seeing It in Action: Usage Example

// App.js
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet } from 'react-native';
import { VerticalSlider } from 'rn-vertical-slider'; // Assuming you've published or linked it
import { GestureHandlerRootView } from 'react-native-gesture-handler';
 
 
const App = () => {
  const [sliderValue, setSliderValue] = useState(25);
 
  return (
    // GestureHandlerRootView should be at the root of your app or screen
    <GestureHandlerRootView style={{ flex: 1 }}> 
      <SafeAreaView style={styles.safeArea}>
        <View style={styles.container}>
          <Text style={styles.valueText}>Value: {sliderValue.toFixed(0)}</Text>
          <VerticalSlider
            value={sliderValue}
            onValueChange={setSliderValue}
            minimumValue={0}
            maximumValue={100}
            step={1}
            height={300} // Custom height
            thumbTintColor="#FF6347"
            minimumTrackTintColor="#FFD700"
            maximumTrackTintColor="#D3D3D3"
            style={styles.sliderStyle} // Custom container style
            // thumbStyle={{ width: 40, height: 40, borderRadius: 20 }} // Custom thumb style
          />
        </View>
      </SafeAreaView>
    </GestureHandlerRootView>
  );
};
 
const styles = StyleSheet.create({
  safeArea: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 20,
  },
  valueText: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 30,
  },
  sliderStyle: {
    width: 60, // Make the slider touch area wider
  },
});
 
export default App;

The Reward: A Community Resource

rn-vertical-slider is now out in the wild, being used in various apps, and has garnered positive feedback from the React Native community. The journey from identifying a personal need to publishing a shareable, open-source solution was incredibly rewarding. It underscored the power of modern React Native animation and gesture libraries and reinforced the value of contributing back to the ecosystem.

This experience has taught me invaluable lessons in component API design, performance optimization for interactive elements, the intricacies of the NPM publishing process, and the importance of clear documentation.


Have you built and published your own React Native components? What were your biggest challenges and learnings? Share your story in the comments below!