I built a React hook for scroll fades. My first npm package!
Ever wanted to add slick fade-in-on-scroll effects in React? Follow my journey of building my first npm package, a custom hook to do just that!
Alex Carter
Frontend developer passionate about creating smooth user experiences and open-source contributions.
My First npm Package: A React Hook for Effortless Scroll Fades
Have you ever scrolled through a beautifully designed website and noticed how elements gracefully fade into view? It’s a subtle touch, but it adds a layer of polish and professionalism that makes the user experience feel so much more dynamic and engaging. I’ve always been a fan of this effect, but implementing it from scratch on every project felt... repetitive. And let’s be honest, getting it right without hurting performance can be a little tricky.
For a while, I found myself copying and pasting the same logic, tweaking it slightly for each new component. Then, the classic developer thought struck me: “There has to be a better way.” This wasn’t just about saving time; it was about creating a clean, reusable, and performant solution. That’s when I decided to dive in, build a custom React hook, and take the leap I’d been putting off for years: publishing my very first npm package.
This post is the story of that journey. We’ll explore why traditional scroll event listeners are a performance nightmare, how the IntersectionObserver
API comes to the rescue, and walk through the step-by-step process of building the useScrollFade
hook. Finally, I’ll share the thrill (and a few of the hurdles) of publishing it to the world. Whether you’re a React developer looking for a neat trick or an aspiring open-source contributor, I hope you find something valuable here.
The “Why”: The Problem with Traditional Scroll Animations
The old-school way to detect if an element is on screen involves listening to the scroll
event on the window. On every single scroll tick, you’d fire a function to calculate the element’s position relative to the viewport. It sounds simple enough, but there’s a huge, hidden cost.
The scroll
event can fire dozens or even hundreds of times per second. Each time, your code has to perform calculations (like getBoundingClientRect()
), which forces the browser to reflow and repaint the layout. This is incredibly taxing on the main thread—the same thread responsible for running your JavaScript, rendering updates, and keeping your site feeling responsive. The result? A high potential for jank, dropped frames, and a sluggish user experience, especially on lower-powered devices.
The “How”: Choosing the Right Tool (Intersection Observer API)
Thankfully, modern browsers provide a much better tool for this exact job: the IntersectionObserver
API. Instead of us constantly asking, “Are you in the viewport yet?”, the browser tells us when an element’s visibility changes.
Think of it as a highly efficient lookout. You tell it which element to watch and under what conditions (e.g., “let me know when 50% of this element is visible”). The browser then handles all the complex calculations in a highly optimized, asynchronous way, off the main thread. It only invokes your callback function when the specified threshold is crossed. This is a game-changer for performance.
Feature | onScroll Event Listener | IntersectionObserver API |
---|---|---|
Performance | Poor. Fires continuously, blocking the main thread. | Excellent. Asynchronous and only fires on state changes. |
Complexity | High. Requires manual position and viewport calculations. | Low. The browser handles all the complex geometry. |
Efficiency | Low. Wastes CPU cycles checking when nothing has changed. | High. Zero cost until an intersection actually occurs. |
Use Case | Best for effects directly tied to scroll position (e.g., parallax). | Perfect for knowing when an element enters or leaves the screen. |
Building the Hook: A Deep Dive into useScrollFade
With the right tool selected, it was time to build the custom hook. The goal was to encapsulate all the IntersectionObserver
logic into a simple, reusable function. Here’s a breakdown of how it works.
1. The Basic Structure
Every custom hook is just a JavaScript function whose name starts with “use”. We need a few key React hooks to make this work: useState
to track visibility, useRef
to reference the DOM element, and useEffect
to manage the observer’s lifecycle.
import { useState, useRef, useEffect } from 'react';
export const useScrollFade = (options) => {
const [isIntersecting, setIsIntersecting] = useState(false);
const elementRef = useRef(null);
// ... observer logic will go here ...
const style = {
opacity: isIntersecting ? 1 : 0,
transform: isIntersecting ? 'translateY(0)' : 'translateY(20px)',
transition: 'opacity 0.6s ease-out, transform 0.6s ease-out',
};
return [elementRef, style];
};
The hook will return an array containing the ref
(to attach to our element) and a style
object that we can apply directly.
2. Implementing the Observer with useEffect
The useEffect
hook is the perfect place to set up and tear down our observer. We only want this effect to run once when the component mounts.
useEffect(() => {
const element = elementRef.current;
const observer = new IntersectionObserver(([entry]) => {
// This callback fires when the intersection state changes
if (entry.isIntersecting) {
setIsIntersecting(true);
// Optional: Unobserve after the first time to prevent re-triggering
observer.unobserve(element);
}
}, {
// Observer options: e.g., trigger when 20% of the element is visible
threshold: 0.2,
...options, // Allow user to override options
});
if (element) {
observer.observe(element);
}
// Cleanup function: disconnect the observer when the component unmounts
return () => {
if (element) {
observer.disconnect();
}
};
}, [options]); // Re-run effect if options change
The key parts here are:
- Creating the
IntersectionObserver
: We instantiate it with a callback function and an options object. - The Callback: When the element intersects, we update our state with
setIsIntersecting(true)
. I also chose tounobserve
the element right after it becomes visible so the animation only happens once. - The Cleanup: The function returned from
useEffect
is crucial. It runs when the component unmounts, and we use it to callobserver.disconnect()
to prevent memory leaks.
From Hook to Package: My First npm Adventure
With the hook working perfectly in my local project, the next step was to share it. This part was both exciting and a little intimidating. I’d used countless npm packages, but never published one.
First, I created a new, clean project repository. The most important file is package.json
, which defines the package’s name, version, dependencies, and entry points. A crucial step I almost missed was setting up a bundler (I chose Rollup) to transpile my modern JavaScript and JSX into a format that works across different project setups (CommonJS and ES Modules).
Writing a good README.md
was surprisingly fun. It’s your package’s storefront. I made sure to include a clear description, installation instructions, and a simple usage example. After that, it was the moment of truth. I ran npm login
, held my breath, and typed:
npm publish
Seeing the confirmation message was a huge rush! My little piece of code was now out in the wild, available for anyone in the world to use. It was a powerful feeling of contributing back to the community that has given me so much.
Get Fading: How to Use the Hook in Your Project
Now for the best part: using the hook is incredibly simple. I published it under the name react-simple-scroll-fade
(a fictional name for this example!).
First, install it in your React project:
npm install react-simple-scroll-fade
Then, import the hook and use it in any component you want to animate:
import React from 'react';
import { useScrollFade } from 'react-simple-scroll-fade';
const FeatureCard = ({ title, description }) => {
// 1. Call the hook to get the ref and styles
const [fadeRef, fadeStyle] = useScrollFade();
return (
// 2. Attach the ref and apply the styles to your element
{title}
{description}
);
};
export default FeatureCard;
And that’s it! The div
will now automatically start with opacity: 0
and a slight offset, then smoothly transition into view as you scroll down to it. No complex logic, no performance worries—just a clean, declarative API.
Final Thoughts: What I Learned and What's Next
Building this hook and publishing it to npm was an incredibly rewarding experience. It solidified my understanding of the IntersectionObserver
, gave me a deeper appreciation for the open-source ecosystem, and demystified the process of package publishing.
My biggest takeaway is that you don’t need a massive, groundbreaking idea to contribute. A small, well-crafted utility that solves a common problem is just as valuable. It’s a fantastic way to sharpen your skills, build your portfolio, and give back to the developer community.
If you've had an idea for a utility or hook, I wholeheartedly encourage you to build it and share it. The journey is as valuable as the destination. As for me, I’m already thinking about what to build next. Maybe a hook for handling outside clicks? The possibilities are endless.
Feel free to check out the (fictional) package on npm and the source on GitHub. I’d love to hear your feedback!