gentleduck/lazy
It is a lightweight and accessible React library for lazy-loading images and components. It uses the IntersectionObserver API to trigger loading when content enters the viewport, ensuring smooth performance and accessibility.
Philosophy
Lazy loading should be invisible to the user and effortless for the developer. gentleduck/lazy wraps React's lazy() and Suspense with sensible defaults — loading skeletons, error boundaries, and intersection observer support — so components and images load only when needed, without boilerplate.
Features
- Lazy loading for components and images
- Customizable with IntersectionObserver options
- Accessibility-first: ARIA roles, live regions, focus management
- Placeholder support while content loads
- Composable 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
- Seamless lazy loading
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>elementaria-hidden— hides placeholders from assistive technology