Skip to main content

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

Loading diagram...

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

PropTypeDefaultDescription
optionsIntersectionObserverInit--IntersectionObserver options (rootMargin, threshold)
childrenReact.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

PropTypeDefaultDescription
srcstring(required)URL of the image
placeholderstring--Placeholder URL while loading
altstring(required)Accessible description
widthnumber200Image width
heightnumber200Image height
optionsIntersectionObserverInit{ rootMargin: '200px', threshold: 0.1 }IntersectionObserver options
nextImageboolean--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

NameTypeDescription
isVisiblebooleanWhether the element is visible in the viewport
ComponentRefReact.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

NameTypeDescription
isLoadedbooleanWhether the image has finished loading
imageRefReact.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> element
  • aria-hidden — hides placeholders from assistive technology