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

@eshan.rajapakshe/react-waypoint

v0.0.2

Published

Modern, fully TypeScript implementation of react-waypoint with React 19 compatibility

Readme

@eshan.rajapakshe/react-waypoint

Modern, fully TypeScript implementation of react-waypoint with React 19 compatibility

A performant and reliable React component for executing callbacks when scrolling to an element. Built with TypeScript, powered by IntersectionObserver API, and optimized for React 19.

🎮 Live Demo

Try it on CodeSandbox →

See interactive examples of lazy loading, scroll animations, infinite scroll, and more!

✨ Features

  • 🚀 Modern & Performant - Uses IntersectionObserver API for efficient detection
  • 💪 TypeScript Native - Full type safety with comprehensive type definitions
  • 🧠 Full IntelliSense - Complete autocomplete and type checking in your IDE
  • ⚛️ React 19 Compatible - Built with latest React patterns and hooks
  • 📦 Tree Shakeable - ESM modules for optimal bundle size (<3KB gzipped)
  • 🎯 Reliable - Comprehensive test coverage and battle-tested logic
  • 🔄 API Compatible - Drop-in replacement for original react-waypoint
  • 🎨 Flexible - Support for vertical/horizontal scrolling and custom offsets
  • 🔍 Debug Mode - Built-in debugging for development

📦 Installation

npm install @eshan.rajapakshe/react-waypoint
yarn add @eshan.rajapakshe/react-waypoint
pnpm add @eshan.rajapakshe/react-waypoint

🚀 Quick Start

import { Waypoint } from '@eshan.rajapakshe/react-waypoint';

function App() {
  return (
    <div>
      <div style={{ height: '200vh' }}>Scroll down...</div>

      <Waypoint
        onEnter={() => console.log('Entered viewport!')}
        onLeave={() => console.log('Left viewport!')}
      >
        <div>I trigger callbacks when scrolled into view!</div>
      </Waypoint>

      <div style={{ height: '200vh' }}>More content...</div>
    </div>
  );
}

📖 API Documentation

Props

onEnter?: (props: WaypointCallbackProps) => void

Callback fired when the waypoint enters the viewport.

<Waypoint onEnter={({ currentPosition, previousPosition }) => {
  console.log('Entered!', currentPosition);
}}>
  <div>Content</div>
</Waypoint>

onLeave?: (props: WaypointCallbackProps) => void

Callback fired when the waypoint leaves the viewport.

<Waypoint onLeave={({ currentPosition, previousPosition }) => {
  console.log('Left!', currentPosition);
}}>
  <div>Content</div>
</Waypoint>

onPositionChange?: (props: WaypointCallbackProps) => void

Callback fired whenever the waypoint position changes.

<Waypoint onPositionChange={({ currentPosition }) => {
  console.log('Position:', currentPosition); // 'above' | 'inside' | 'below' | 'invisible'
}}>
  <div>Content</div>
</Waypoint>

topOffset?: string | number

Offset from the top of the viewport. Accepts pixels (100, '100px') or percentages ('50%').

Positive values move the boundary down, negative values move it up.

// Trigger 100px before reaching viewport top
<Waypoint topOffset="-100px">
  <div>Content</div>
</Waypoint>

// Trigger at 20% from top
<Waypoint topOffset="20%">
  <div>Content</div>
</Waypoint>

bottomOffset?: string | number

Offset from the bottom of the viewport. Accepts pixels or percentages.

Positive values move the boundary up, negative values move it down.

// Trigger 50px before reaching viewport bottom
<Waypoint bottomOffset="50px">
  <div>Content</div>
</Waypoint>

horizontal?: boolean

Enable horizontal scrolling detection instead of vertical.

<Waypoint horizontal>
  <div>Horizontal content</div>
</Waypoint>

scrollableAncestor?: HTMLElement | Window | 'window'

The scrollable container to monitor. If not provided, automatically finds the scrollable ancestor.

const containerRef = useRef<HTMLDivElement>(null);

<div ref={containerRef} style={{ overflow: 'auto', height: '400px' }}>
  <Waypoint scrollableAncestor={containerRef.current || undefined}>
    <div>Content</div>
  </Waypoint>
</div>

fireOnRapidScroll?: boolean

Fire callbacks during rapid scrolling. Default: true.

When false, uses debouncing to skip intermediate positions during fast scrolling.

<Waypoint fireOnRapidScroll={false}>
  <div>Content</div>
</Waypoint>

debug?: boolean

Enable debug logging to console. Useful for development.

<Waypoint debug>
  <div>Content</div>
</Waypoint>

TypeScript Types

import type {
  WaypointPosition,
  WaypointCallbackProps,
  WaypointProps
} from '@eshan.rajapakshe/react-waypoint';

// Position type
type WaypointPosition = 'above' | 'inside' | 'below' | 'invisible';

// Callback props
interface WaypointCallbackProps {
  currentPosition: WaypointPosition;
  previousPosition: WaypointPosition;
  event?: Event;
  waypointTop?: number;
  viewportTop?: number;
  viewportBottom?: number;
}

Position Constants

import { POSITIONS } from '@eshan.rajapakshe/react-waypoint';

console.log(POSITIONS.above);     // 'above'
console.log(POSITIONS.inside);    // 'inside'
console.log(POSITIONS.below);     // 'below'
console.log(POSITIONS.invisible); // 'invisible'

🎯 Common Use Cases

Lazy Loading Images

import { Waypoint } from '@eshan.rajapakshe/react-waypoint';
import { useState } from 'react';

function LazyImage({ src, alt }: { src: string; alt: string }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <Waypoint onEnter={() => setLoaded(true)}>
      <div>
        {loaded ? (
          <img src={src} alt={alt} />
        ) : (
          <div style={{ height: '300px', background: '#eee' }}>Loading...</div>
        )}
      </div>
    </Waypoint>
  );
}

Infinite Scroll

import { Waypoint } from '@eshan.rajapakshe/react-waypoint';
import { useState } from 'react';

function InfiniteList() {
  const [items, setItems] = useState(Array.from({ length: 20 }, (_, i) => i));
  const [loading, setLoading] = useState(false);

  const loadMore = async () => {
    if (loading) return;

    setLoading(true);
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000));
    setItems(prev => [...prev, ...Array.from({ length: 20 }, (_, i) => prev.length + i)]);
    setLoading(false);
  };

  return (
    <div>
      {items.map(item => (
        <div key={item} style={{ padding: '20px', border: '1px solid #ddd' }}>
          Item {item}
        </div>
      ))}

      <Waypoint onEnter={loadMore} bottomOffset="-200px">
        <div style={{ padding: '20px', textAlign: 'center' }}>
          {loading ? 'Loading...' : 'Load More'}
        </div>
      </Waypoint>
    </div>
  );
}

Scroll Spy / Active Navigation

import { Waypoint } from '@eshan.rajapakshe/react-waypoint';
import { useState } from 'react';

function ScrollSpy() {
  const [activeSection, setActiveSection] = useState('');

  return (
    <div>
      <nav style={{ position: 'fixed', top: 0 }}>
        <a href="#section1" style={{ fontWeight: activeSection === 'section1' ? 'bold' : 'normal' }}>
          Section 1
        </a>
        <a href="#section2" style={{ fontWeight: activeSection === 'section2' ? 'bold' : 'normal' }}>
          Section 2
        </a>
      </nav>

      <Waypoint onEnter={() => setActiveSection('section1')} topOffset="60px">
        <section id="section1" style={{ height: '100vh' }}>
          <h2>Section 1</h2>
        </section>
      </Waypoint>

      <Waypoint onEnter={() => setActiveSection('section2')} topOffset="60px">
        <section id="section2" style={{ height: '100vh' }}>
          <h2>Section 2</h2>
        </section>
      </Waypoint>
    </div>
  );
}

Analytics Tracking

import { Waypoint } from '@eshan.rajapakshe/react-waypoint';

function TrackedContent({ id, children }: { id: string; children: React.ReactNode }) {
  const trackView = () => {
    // Send analytics event
    analytics.track('content_viewed', { contentId: id });
  };

  return (
    <Waypoint onEnter={trackView}>
      <div>{children}</div>
    </Waypoint>
  );
}

Animation Triggers

import { Waypoint } from '@eshan.rajapakshe/react-waypoint';
import { useState } from 'react';

function AnimatedSection() {
  const [visible, setVisible] = useState(false);

  return (
    <Waypoint onEnter={() => setVisible(true)}>
      <div
        style={{
          opacity: visible ? 1 : 0,
          transform: visible ? 'translateY(0)' : 'translateY(50px)',
          transition: 'all 0.6s ease-out',
        }}
      >
        Animated content!
      </div>
    </Waypoint>
  );
}

🔄 Migration from Original react-waypoint

This library is designed as a drop-in replacement for the original react-waypoint:

- import Waypoint from 'react-waypoint';
+ import { Waypoint } from '@eshan.rajapakshe/react-waypoint';

// All props work exactly the same!
<Waypoint
  onEnter={handleEnter}
  onLeave={handleLeave}
  topOffset="100px"
>
  <div>Content</div>
</Waypoint>

Key Improvements

  • ✅ Full TypeScript support (no @types package needed)
  • ✅ Uses IntersectionObserver for better performance
  • ✅ React 19 compatible
  • ✅ Better tree-shaking and smaller bundle size
  • ✅ Improved SSR support
  • ✅ Better debugging with debug prop

🎨 Advanced Usage

Without Children (Invisible Marker)

<Waypoint
  onEnter={() => console.log('Scrolled to this point')}
/>

Multiple Callbacks

<Waypoint
  onEnter={() => console.log('Entered')}
  onLeave={() => console.log('Left')}
  onPositionChange={({ currentPosition }) => {
    console.log('Position:', currentPosition);
  }}
>
  <div>Content</div>
</Waypoint>

Custom Scrollable Container

function ScrollableContainer() {
  const containerRef = useRef<HTMLDivElement>(null);

  return (
    <div ref={containerRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: '200vh' }}>
        <Waypoint
          scrollableAncestor={containerRef.current || undefined}
          onEnter={() => console.log('Entered in custom container')}
        >
          <div>Content</div>
        </Waypoint>
      </div>
    </div>
  );
}

🐛 Troubleshooting

Callbacks not firing

  1. Check if IntersectionObserver is supported - The library uses IntersectionObserver, which is supported in all modern browsers. For older browsers, consider using a polyfill.

  2. Verify scrollable ancestor - Make sure the scrollable container is properly detected. You can manually specify it with the scrollableAncestor prop.

  3. Enable debug mode - Use the debug prop to see detailed logs:

<Waypoint debug onEnter={() => console.log('entered')}>
  <div>Content</div>
</Waypoint>

Callbacks firing multiple times

  • This is expected when the waypoint crosses viewport boundaries multiple times
  • Use onEnter and onLeave instead of onPositionChange if you only want specific transitions

Performance issues

  • Set fireOnRapidScroll={false} for debounced callbacks
  • Avoid heavy computations in callbacks
  • Use useCallback to memoize callback functions

🏗️ How It Works

The library uses a dual-detection strategy:

  1. IntersectionObserver (Primary) - Efficiently detects when elements enter/leave the viewport
  2. Scroll Listeners (Fallback) - Catches edge cases and provides additional precision

This approach provides the best of both worlds: performance from IntersectionObserver and reliability from scroll listeners.

🌐 Browser Support

  • Chrome/Edge 58+
  • Firefox 55+
  • Safari 12.1+
  • All modern mobile browsers

For older browsers, use an IntersectionObserver polyfill.

📊 Bundle Size

  • Minified: ~8KB
  • Gzipped: <5KB
  • Zero runtime dependencies (except React)

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

MIT

🙏 Acknowledgments

This library is inspired by the original react-waypoint by Brigade. We're grateful to the original creators for their pioneering work in this space.

Our implementation modernizes the approach with:

  • Full TypeScript support
  • IntersectionObserver API
  • React 19 compatibility
  • Improved performance and reliability

Made with ❤️ for the React community