gentleduck/lazy
Lightweight React library for lazy-loading images and components using IntersectionObserver.
Philosophy
gentleduck/lazy wraps React's lazy() and Suspense with loading skeletons, error boundaries, and IntersectionObserver support. Components and images load when they enter the viewport.
Features
- Lazy loading for components and images.
- Configurable IntersectionObserver options.
- ARIA roles, live regions, focus management.
- Placeholder support while content loads.
- Hooks for custom behavior.
Installation
npm install @gentleduck/lazy
npm install @gentleduck/lazy
Usage
1) Lazy Component
The DuckLazyComponent defers rendering until its children enter the viewport.
import { DuckLazyComponent } from '@gentleduck/lazy'
function MyComponent() {
return (
<DuckLazyComponent options={{ rootMargin: '100px', threshold: 0.25 }}>
<div>Content that will be lazily loaded</div>
</DuckLazyComponent>
)
}import { DuckLazyComponent } from '@gentleduck/lazy'
function MyComponent() {
return (
<DuckLazyComponent options={{ rootMargin: '100px', threshold: 0.25 }}>
<div>Content that will be lazily loaded</div>
</DuckLazyComponent>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | IntersectionObserverInit | - | IntersectionObserver options (rootMargin, threshold) |
children | React.ReactNode | - | The lazy-loaded content (required) |
2) Lazy Image
The DuckLazyImage supports placeholders, accessibility attributes, and Next.js next/image.
import { DuckLazyImage } from '@gentleduck/lazy'
function MyImageComponent() {
return (
<DuckLazyImage
src="https://example.com/image.jpg"
placeholder="https://example.com/placeholder.jpg"
alt="A description of the image"
width={400}
height={300}
options={{ rootMargin: '100px', threshold: 0.25 }}
/>
)
}import { DuckLazyImage } from '@gentleduck/lazy'
function MyImageComponent() {
return (
<DuckLazyImage
src="https://example.com/image.jpg"
placeholder="https://example.com/placeholder.jpg"
alt="A description of the image"
width={400}
height={300}
options={{ rootMargin: '100px', threshold: 0.25 }}
/>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | (required) | URL of the image |
placeholder | string | - | Placeholder URL while loading |
alt | string | (required) | Accessible description |
width | number | 200 | Image width |
height | number | 200 | Image height |
options | IntersectionObserverInit | { rootMargin: '200px', threshold: 0.1 } | IntersectionObserver options |
nextImage | boolean | - | Enables Next.js next/image optimization |
3) useLazyLoad Hook
Attach lazy-loading behavior to any element.
import { useLazyLoad } from '@gentleduck/lazy'
function MyComponent() {
const { isVisible, ComponentRef } = useLazyLoad({
rootMargin: '100px',
threshold: 0.25,
})
return (
<div ref={ComponentRef}>
{isVisible ? <div>Visible content</div> : <div>Loading...</div>}
</div>
)
}import { useLazyLoad } from '@gentleduck/lazy'
function MyComponent() {
const { isVisible, ComponentRef } = useLazyLoad({
rootMargin: '100px',
threshold: 0.25,
})
return (
<div ref={ComponentRef}>
{isVisible ? <div>Visible content</div> : <div>Loading...</div>}
</div>
)
}Returns
| Name | Type | Description |
|---|---|---|
isVisible | boolean | Whether the element is visible in the viewport |
ComponentRef | React.RefObject<HTMLDivElement | null> | Ref to attach to the observed element |
4) useLazyImage Hook
Specialized hook for images: manages visibility + load state.
import { useLazyImage } from '@gentleduck/lazy'
function LazyImage({ src, placeholder }) {
const { isLoaded, imageRef } = useLazyImage(src, {
rootMargin: '100px',
threshold: 0.25,
})
return (
<div ref={imageRef}>
{!isLoaded && <img src={placeholder} alt="Placeholder" />}
{isLoaded && <img src={src} alt="Main Image" />}
</div>
)
}import { useLazyImage } from '@gentleduck/lazy'
function LazyImage({ src, placeholder }) {
const { isLoaded, imageRef } = useLazyImage(src, {
rootMargin: '100px',
threshold: 0.25,
})
return (
<div ref={imageRef}>
{!isLoaded && <img src={placeholder} alt="Placeholder" />}
{isLoaded && <img src={src} alt="Main Image" />}
</div>
)
}Returns
| Name | Type | Description |
|---|---|---|
isLoaded | boolean | Whether the image has finished loading |
imageRef | React.RefObject<HTMLImageElement | null> | Ref to attach to the <img> element |
DuckLazyImage Component (Detailed)
Optimized lazy image loader with placeholder + accessibility.
import { DuckLazyImage } from '@gentleduck/lazy'
function MyImageComponent() {
return (
<DuckLazyImage
src="https://example.com/image.jpg"
placeholder="https://example.com/placeholder.jpg"
alt="Mountain view"
width={400}
height={300}
options={{ rootMargin: '100px', threshold: 0.25 }}
/>
)
}import { DuckLazyImage } from '@gentleduck/lazy'
function MyImageComponent() {
return (
<DuckLazyImage
src="https://example.com/image.jpg"
placeholder="https://example.com/placeholder.jpg"
alt="Mountain view"
width={400}
height={300}
options={{ rootMargin: '100px', threshold: 0.25 }}
/>
)
}Integration with Next.js
Enable Next.js image optimization with nextImage.
<DuckLazyImage
nextImage
src="https://example.com/image.jpg"
placeholder="https://example.com/placeholder.jpg"
alt="Next.js optimized image"
width={400}
height={300}
/><DuckLazyImage
nextImage
src="https://example.com/image.jpg"
placeholder="https://example.com/placeholder.jpg"
alt="Next.js optimized image"
width={400}
height={300}
/>Benefits:
- Built-in Next.js optimization
- Lazy loading via
next/image
Integration with React
Works as a drop-in replacement for <img> in plain React apps.
<DuckLazyImage
src="https://example.com/image.jpg"
placeholder="https://example.com/placeholder.jpg"
alt="React lazy image"
width={400}
height={300}
/><DuckLazyImage
src="https://example.com/image.jpg"
placeholder="https://example.com/placeholder.jpg"
alt="React lazy image"
width={400}
height={300}
/>Accessibility Features
aria-live="polite"- announces loading state changes via an<output>element.aria-hidden- hides placeholders from assistive technology.