How to Build a React Image Component
Build a reusable React image component with accessible alt text, responsive sources, reserved layout space, selective lazy loading, and an optional safe fallback.
A React image component is a small wrapper around the browser’s native <img> element. React already supports image attributes such as alt, width, height, srcSet, sizes, loading, fetchPriority, and onError; you do not need a custom abstraction unless it makes your application’s conventions easier to use. React’s image reference documents the supported props.
This tutorial builds a typed, reusable component, then adds responsive loading and an optional fallback. The same principles apply to JavaScript React; the component below uses TypeScript.
1. Start with accessible image semantics
Give informative images alternative text that conveys their purpose in context. For a decorative image that adds no information, use an empty alternative, alt="", so assistive technology can skip it. Do not generate alt text from a filename: filenames rarely provide a useful description.
<Image src="/team-at-work.webp" alt="The design team reviewing a page mockup" width={1200} height={800} />
<Image src="/blue-divider.svg" alt="" width={1200} height={8} />
Choose text based on what the image communicates on that page, not on every detail visible in it. The WAI Images Tutorial covers informative, decorative, and other image patterns.
2. Build a reusable component that forwards native props
Forwarding native image props keeps the wrapper useful without recreating browser behavior. This TypeScript component requires src and alt, accepts the normal image attributes, and lets callers pass responsive and loading options.
import { useState, type ComponentProps } from "react";
type ImageProps = Omit<ComponentProps<"img">, "src" | "alt"> & {
src: string;
alt: string;
fallbackSrc?: string;
};
export function Image({
src,
alt,
fallbackSrc,
onError,
...imgProps
}: ImageProps) {
const [failed, setFailed] = useState(false);
const displayedSrc = failed && fallbackSrc ? fallbackSrc : src;
return (
<img
{...imgProps}
src={displayedSrc}
alt={alt}
onError={(event) => {
onError?.(event);
if (fallbackSrc && !failed) setFailed(true);
}}
/>
);
}
Because the props extend React’s native image props, callers can use width, height, srcSet, sizes, loading, fetchPriority, className, style, and other supported attributes. The fallback is opt-in. If the fallback itself fails, failed is already true, so the component does not switch again or loop.
The component keeps its failure state for the mounted instance. If the parent changes src without remounting, reset the state when the source changes or key the component by source. For example, use <Image key={src} src={src} ... /> when source changes represent a new image. Keep the original alt meaningful for the fallback too; if it is a generic placeholder, consider whether that placeholder should instead be decorative.
3. Reserve layout space with intrinsic dimensions
Provide the image’s intrinsic width and height whenever you know them. The browser can reserve the right aspect ratio before downloading the file, reducing unexpected layout movement. This is particularly useful for lazy-loaded images. The values are the source image’s dimensions, not necessarily its rendered CSS size.
<Image
src="/mountain-1200x800.webp"
alt="Snow-covered mountain above a lake"
width={1200}
height={800}
className="article-image"
/>
.article-image {
display: block;
max-width: 100%;
height: auto;
}
CSS controls the rendered size; the intrinsic dimensions communicate the aspect ratio. If the image is cropped into a fixed frame, set an intentional aspect ratio and use object-fit:
.avatar {
width: 10rem;
aspect-ratio: 1;
object-fit: cover;
border-radius: 50%;
}
See MDN’s img reference for the browser behavior of image dimensions and attributes.
4. Make images responsive with srcSet and sizes
For the same image available at several widths, srcSet lists candidate files and their pixel widths. sizes tells the browser how wide the image slot will be at different viewport sizes. The browser uses these hints, along with device conditions, to select a suitable candidate.
<Image
src="/landscape-800.webp"
srcSet="/landscape-400.webp 400w, /landscape-800.webp 800w, /landscape-1600.webp 1600w"
sizes="(max-width: 600px) 100vw, (max-width: 1100px) 80vw, 900px"
alt="A coastline at sunset"
width={1600}
height={900}
/>
Use width descriptors such as 400w with an accurate sizes value. The fallback src remains useful for browsers or contexts that do not select a candidate. If the displayed slot has a known fixed width, describe that width in sizes; if it varies with your layout, keep the media conditions aligned with the CSS breakpoints.
Use <picture> when the browser should choose a different source for a format, crop, or art direction. For example:
<picture>
<source
media="(max-width: 600px)"
srcSet="/portrait-crop.webp"
/>
<source type="image/avif" srcSet="/landscape.avif" />
<Image
src="/landscape.jpg"
alt="A hiker looking across a valley"
width={1600}
height={900}
/>
</picture>
Use srcSet and sizes for resolution variants of the same image; choose <picture> when conditions should select a different source or composition. More candidates are not automatically better: generate candidates that match your actual image sizes and layout. Read MDN’s responsive images guide for the browser’s source selection model.
5. Choose loading priority deliberately
Set loading="lazy" on images that are below the fold and can wait until the browser approaches them. Do not apply it automatically to an image needed immediately in the initial viewport; delaying a prominent image can make it appear later.
<Image
src="/related-article.webp"
alt="A preview of the related article"
width={640}
height={360}
loading="lazy"
/>
<Image
src="/page-hero.webp"
alt="A wide view of the city skyline"
width={1600}
height={900}
fetchPriority="high"
/>
Use high fetch priority sparingly for a genuinely important image; do not assign it to every image. React documents that it may emit an image preload hint during rendering, while loading="lazy" and fetchPriority="low" prevent that automatic hint. Server-rendering frameworks may wrap or change image behavior, so consult the framework’s current image documentation when using its image component.
6. Add an optional fallback without creating a loop
An onError handler can replace a failed image with a known local placeholder. The component above switches at most once. A minimal usage example is:
<Image
src={profile.photoUrl}
fallbackSrc="/avatar-placeholder.svg"
alt={`Profile photo of ${profile.name}`}
width={96}
height={96}
/>
Do not pass an empty src. React notes that an empty source can make the browser request the current page. If no image URL is available, conditionally render another element or do not render the image. Also decide what a failed image should communicate: a neutral placeholder may need empty alt text, while a meaningful replacement may need descriptive alt text.
7. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Screen reader announces a filename or useless description | Alt text was copied from a filename or omitted | Write concise contextual alt text, or use alt="" for a decorative image. |
| Content jumps when an image loads | Intrinsic dimensions or an aspect ratio were not reserved | Set the correct width and height, or reserve space with CSS. |
| Browser downloads an unexpectedly large image | sizes does not describe the actual rendered slot, or candidates are missing |
Align sizes with layout breakpoints and provide suitable width candidates. |
| Image is slow to appear at the top of the page | The initial viewport image was marked lazy or given low priority | Remove lazy loading for that image and review its fetch priority. |
| Fallback causes repeated requests or errors | The error handler replaces the source with another failing URL repeatedly | Switch only once to a known fallback and retain a failed state, as in the component above. |
| Image appears broken after changing its URL | Fallback state from the previous source remains set | Remount with a source-based key or reset local state when the source changes. |
| Image URL returns an error despite working locally | Incorrect deployed path, case mismatch, inaccessible host, or an invalid asset URL | Inspect the request URL and response in browser developer tools; verify deployment paths and access rules. |
| TypeScript rejects an image prop | The prop name or value does not match React’s native image attribute types | Use ComponentProps<"img"> to inherit supported attributes and check React’s image reference. |
8. Performance, reliability, and cost
Start with browser-native image selection, correct dimensions, and selective lazy loading. Measure the actual page and image set before claiming one approach is faster; network, image encoding, slot width, and device all affect the result. Responsive candidates can avoid downloading a needlessly large resource, while accurate dimensions help prevent layout movement. Lazy loading can reduce early offscreen requests, but can delay content if applied to an image users need right away.
For reliability, use stable asset URLs, verify deployed paths, and make fallback behavior finite. Keep the fallback available at a dependable URL and provide dimensions for it as well. Native image loading does not require a third-party service. If your workflow needs screenshots to inspect how images render on real pages, ScreenshotNeo is a separate website screenshot API and MCP server from Yorker Media; it is not required to build this component.
9. Frequently asked questions
Does every React app need an image component?
No. A native <img> is valid in React. Add a wrapper when shared defaults, typing, or fallback conventions make image usage clearer.
Should alt text always describe everything in the picture?
No. Describe the image’s relevant meaning in context. Decorative images should use an empty alt attribute.
Should I use a framework image component?
Use one if your framework provides behavior your application needs, and check its current documentation for its props and rendering details. This tutorial covers the browser-native element and a lightweight React wrapper.
Or skip the browser setup
If you need a screenshot of a page to inspect the result, ScreenshotNeo returns an image or PDF from one API request. See the ScreenshotNeo API documentation for parameters and formats.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed before capture, along with known newsletter popups and chat widgets; each step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status.
- An MCP server gives AI agents tools for screenshots, page information, and PDF capture.
- The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo and get 1,000 screenshots a month free, with no card.


