React utilities for observing when elements enter or leave the viewport using Intersection Observer API.
npm install @intersection-observer/react> A performant, flexible React wrapper for the Intersection Observer API.
---
- 🪝 Hook — Use useInView for reactive intersection detection
- 🎨 Component — Use LazyRender for declarative lazy loading
- ⚡️ Optimized Performance — Reuses observer instances efficiently
- 🔧 API Match — Based on native Intersection Observer API
- 🔠 TypeScript Support — Fully typed for TS projects
- 🌳 Tree-shakeable — Only the code you use gets bundled
- 🚀 Zero Dependencies — Lightweight with no external deps
---
``bashWith pnpm
pnpm add @intersection-observer/react
---
🚀 Quick Start
$3
`tsx
import { useInView } from '@intersection-observer/react';const MyComponent = () => {
const { ref, inView } = useInView({ threshold: 0.5 });
return (
{inView ? 'Element is visible!' : 'Element is hidden'}
);
};
`$3
`tsx
import { LazyRender } from '@intersection-observer/react';const LazyImage = () => {
return (

);
};
`---
📚 API Reference
$3
A React hook that returns a ref and boolean indicating if the element is in view.
`tsx
function useInView(options?: InViewOptions): {
ref: RefObject
inView: boolean
}
`#### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
|
threshold | number \| number[] | 0 | Percentage of element visibility (0-1) |
| rootMargin | string | '0px' | Margin around the root (e.g., "10px 20px") |
| root | Element \| null | null | Element to use as viewport |#### Example with Options
`tsx
const { ref, inView } = useInView({
threshold: [0, 0.25, 0.5, 0.75, 1], // Multiple thresholds
rootMargin: '50px', // Trigger 50px before element enters viewport
root: document.querySelector('.scroll-container') // Custom root
});
`$3
A component that renders children only when they come into view.
`tsx
interface LazyRenderProps {
children: ReactNode | ((props: RenderProps) => ReactNode)
as?: keyof JSX.IntrinsicElements
onChange?: (inView: boolean) => void
threshold?: number
className?: string
style?: CSSProperties
}interface RenderProps {
inView: boolean
ref: (node: HTMLElement | null) => void
}
`#### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
|
children | ReactNode \| Function | - | Content to render or render function |
| as | string | 'div' | HTML element to render |
| onChange | (inView: boolean) => void | - | Callback when visibility changes |
| threshold | number | 0 | Visibility threshold (0-1) |
| className | string | - | CSS class name |
| style | CSSProperties | - | Inline styles |---
🎯 Use Cases & Examples
$3
`tsx
import { LazyRender } from '@intersection-observer/react';const ImageGallery = ({ images }) => {
return (
{images.map((image) => (
src={image.src}
alt={image.alt}
loading="lazy"
/>
))}
);
};
`$3
`tsx
import { LazyRender } from '@intersection-observer/react';const InfiniteList = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const loadMore = async () => {
if (loading) return;
setLoading(true);
const newItems = await fetchMoreItems();
setItems(prev => [...prev, ...newItems]);
setLoading(false);
};
return (
{items.map(item => (
))}
inView && loadMore()}>
{loading ? 'Loading...' : 'Scroll for more'}
);
};
`$3
`tsx
import { useInView } from '@intersection-observer/react';const AnimatedSection = () => {
const { ref, inView } = useInView({
threshold: 0.3,
rootMargin: '50px'
});
return (
ref={ref}
className={
animate-section ${inView ? 'animate-in' : 'animate-out'}}
>
Animated Content
This section animates when it comes into view
);
};
`$3
`tsx
import { LazyRender } from '@intersection-observer/react';const TrackedSection = ({ sectionId, children }) => {
const handleVisibilityChange = (inView) => {
if (inView) {
analytics.track('section_viewed', { sectionId });
}
};
return (
{children}
);
};
`$3
`tsx
import { LazyRender } from '@intersection-observer/react';const ConditionalContent = () => {
return (
{({ inView, ref }) => (
{inView ? (
Content is now visible!
This content only renders when 50% visible
) : (
)}
)}
);
};
`$3
`tsx
import { useInView } from '@intersection-observer/react';const PerformanceTracker = () => {
const { ref, inView } = useInView({
threshold: 0.5,
rootMargin: '100px'
});
useEffect(() => {
if (inView) {
performance.mark('content-visible');
performance.measure('content-load', 'navigationStart', 'content-visible');
}
}, [inView]);
return (
Performance Tracked Content
This section tracks when it becomes visible
);
};
`---
🔧 Advanced Usage
$3
`tsx
const CustomRootExample = () => {
const containerRef = useRef(null);
const { ref, inView } = useInView({
root: containerRef.current,
threshold: 0.5
}); return (
{inView ? 'In container view' : 'Outside container view'}
);
};
`$3
`tsx
const MultiThresholdExample = () => {
const { ref, inView } = useInView({
threshold: [0, 0.25, 0.5, 0.75, 1]
}); return (
This element tracks multiple visibility levels
Current visibility: {inView ? 'Visible' : 'Hidden'}
);
};
`$3
`tsx
const CleanupExample = () => {
const { ref, inView } = useInView(); // The hook automatically handles cleanup when the component unmounts
// or when the ref changes
return (
{inView ? 'Visible' : 'Hidden'}
);
};
`---
🌐 Browser Support
This package uses the Intersection Observer API, which is supported in all modern browsers:
- ✅ Chrome 51+
- ✅ Firefox 55+
- ✅ Safari 12.1+
- ✅ Edge 15+
For older browsers, you can use a polyfill:
`bash
npm install intersection-observer
``tsx
// Add this to your app entry point
import 'intersection-observer';
`---
📦 Bundle Size
- Minified: ~3KB
- Gzipped: ~1.5KB
- Tree-shakeable: Only imports what you use
---
🛠 Development
This package is part of a monorepo. To link it locally:
`bash
pnpm install
pnpm dev # or use your examples app
``---
MIT — Made with ❤️ by Ahmed Aljohi (https://github.com/AhmedAljhoi)