From Zero to NPM: Building the React Component of My Dreams



This content originally appeared on DEV Community and was authored by Ali Shirani


Let’s be real. We’ve all been there. You find a seemingly perfect component on NPM for that one specific feature you need—a date picker, a modal, a swipe button. You install it, you import it, and then the nightmare begins.

You try to change a color, and you have to fight through five layers of CSS specificity. You want to move the icon just a little to the left, but there’s no prop for that. You end up writing hacky CSS, adding !important everywhere, and silently cursing the developer who sealed their component in a black box.

I hit this wall one too many times with swipe buttons. I wanted something that was both beautiful out of the box and completely, utterly customizable. So I decided to build my own.

This is the story of how I built a zero-dependency, fully-themed, and ridiculously flexible swipe button, and how you can build and publish your own professional components, too.

The Problem: The Monolithic Component

My first instinct, like many developers, was to build a single, monolithic component. It looked something like this in theory:

// The "Black Box" approach
<SwipeButton
  onSuccess={...}
  railText="Swipe Me"
  overlayText="Success!"
  icon={<MyIcon />}
  railStyle={{...}}
  sliderStyle={{...}}
  overlayStyle={{...}}
  // ...and a dozen more props
/>

This seems convenient at first, but it’s a trap. What if a user wants to add a subtle glow inside the rail but behind the text? What if they want to use Tailwind CSS classes instead of style objects? Every new requirement adds another prop, and the component’s internal logic becomes a tangled mess.

This approach offers poor Developer Experience (DX). It’s rigid. It’s not composable. It’s not the “React way.”

The “Aha!” Moment: The Compound Component Pattern

The solution came from looking at how great APIs are built, both on the web and in other libraries. Think about the standard HTML <select> element:

<select>
  <option value="1">First</option>
  <option value="2">Second</option>
</select>

You don’t configure options via a giant options prop on the select element. You compose the UI by placing <option> elements inside. This is the Compound Component Pattern.

Libraries like Radix UI and Vaul have mastered this. The core idea is simple:

  1. A parent <Root> component manages all the state and logic.
  2. It uses React Context to pass down that state and logic to its children.
  3. Child components (<Slider>, <Rail>, etc.) consume the context and render the UI, giving the developer full control over each part’s placement and styling.

This was the path forward.

Building the Component

Armed with this new pattern, I refactored the entire component.

1. The Context

First, I defined a React Context to be the “brain” of the operation. It holds all the shared state.

interface SwipeContextType {
  isSwiping: boolean;
  sliderPosition: number;
  // ... and other necessary state
}

const SwipeContext = React.createContext<SwipeContextType | null>(null);

const useSwipeContext = () => { /* ... checks for context ... */ };

2. The <Root> Component

The <SwipeButton.Root> component contains all the useState, useRef, and useEffect hooks. It handles all the complex drag-and-drop logic. Crucially, it wraps its children in the SwipeContext.Provider.

It also does something magical: it injects a <style> tag with a beautiful, complete default theme.

const Root = forwardRef<HTMLDivElement, SwipeButtonRootProps>((props) => {
  // ... all the state and drag logic ...

  const contextValue = { /* ... state and functions ... */ };

  return (
    <SwipeContext.Provider value={contextValue}>
      {/* It injects its own styles! No CSS file needed for the user. */}
      <style>{defaultStyles}</style>
      <div className="swipe-button__root" {...props}>
        {props.children}
      </div>
    </SwipeContext.Provider>
  );
});

3. The Child Components

With the Root doing all the heavy lifting, the child components became beautifully simple. For example, the <Slider> component’s only job is to get its position from the context and render a div.

const Slider = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((props) => {
  // It consumes the context to get what it needs
  const { sliderPosition, handleDragStart, sliderRef } = useSwipeContext();

  return (
    <div
      ref={sliderRef}
      className="swipe-button__slider"
      // Applies the position via a smooth CSS transform
      style={{ transform: `translateX(${sliderPosition}px)` }}
      onMouseDown={handleDragStart}
      onTouchStart={handleDragStart}
      {...props}
    />
  );
});

Finally, I bundled them all into a single export:

export const SwipeButton = { Root, Rail, Overlay, Slider };

The Payoff: Ultimate Flexibility

This new API is a dream to use. It works perfectly out of the box with zero configuration:

import { SwipeButton } from '@my-scope/react-swipe-button';
import { ChevronRight } from 'lucide-react';

function MyComponent() {
  return (
    <SwipeButton.Root onSuccess={() => alert("Success!")}>
      <SwipeButton.Rail>
        <span>Swipe to Unlock</span>
      </SwipeButton.Rail>
      <SwipeButton.Overlay>
        <span>Unlocked!</span>
      </SwipeButton.Overlay>
      <SwipeButton.Slider>
        <ChevronRight color="black" />
      </SwipeButton.Slider>
    </SwipeButton.Root>
  );
}

But the real magic is customization. The entire default theme is built on CSS Variables. Want to create a destructive “delete” button? You don’t need new props. You just override the variables.

Just add this to your CSS:

.destructive-swipe-theme {
  --sw-background: 220 20% 15%; /* A dark background */
  --sw-border: 220 20% 25%;
  --sw-slider: 0 100% 95%; /* A white slider */
  --sw-success: 0 84.2% 60.2%; /* A dangerous red */
}

And apply the class:

<SwipeButton.Root onSuccess={handleDelete} className="destructive-swipe-theme">
  {/* ... child components ... */}
</SwipeButton.Root>

That’s it. You get a completely different look without ever touching the component’s internal logic. This is true separation of concerns.

The Final Step: Publishing to the World

Getting the component onto NPM was the final boss. I configured my package.json with the necessary fields (main, module, types, files, etc.), used a great tool called tsup to bundle my TypeScript into standard JavaScript, and then ran the magic command:

npm publish --access public

Try It Yourself!

This journey taught me so much about component architecture, developer experience, and the power of open source. Building something out of personal frustration is one of the most rewarding things you can do as a developer.

I invite you to check out the final product.

What component have you always wished was better? Maybe it’s time you built it yourself.

Thanks for reading


This content originally appeared on DEV Community and was authored by Ali Shirani