How to Add Images in React JS
Add local, public, remote, and dynamic images in React with accessible JSX, correct paths, troubleshooting, and production-ready loading tips.
React displays images with the browser’s native <img> element. Give it a src URL and useful alternative text:
export default function Hero() {
return (
<img
src="/images/hero.jpg"
alt="A mountain lake at sunrise"
width={1200}
height={800}
/>
);
}
Use an imported file when the image belongs to your application bundle, a root-absolute URL for files in Vite’s public directory, and a JavaScript expression such as src={user.imageUrl} for data-backed or remote images. React’s image reference covers alt, dimensions, loading, and priority attributes in detail: React <img> reference.
1. Add a local image from src
Put the image beside your source files, import it, and pass the imported URL to src. This is the usual choice for images that are part of the app.
src/
components/
Profile.jsx
assets/
profile-photo.jpg
import profilePhoto from '../assets/profile-photo.jpg';
export default function Profile() {
return (
<figure>
<img
src={profilePhoto}
alt="Portrait of Alex Morgan"
width={240}
height={240}
/>
<figcaption>Alex Morgan</figcaption>
</figure>
);
}
Vite and Create React App resolve the import to a public URL. During a production build, the emitted filename may include a content hash, so the reference remains valid even when the file name changes. Keep the import path static so the bundler can discover the asset. See Vite static asset handling and Create React App’s image documentation.
Importing from the same folder
// src/components/Card.jsx
import cover from './cover.png';
export function Card() {
return <img src={cover} alt="Book cover" />;
}
The number of ../ segments depends on the component’s location. If the editor reports “module not found,” check the relative path, capitalization, and file extension.
2. Serve an image from Vite’s public directory
Use public when a file should be served directly at a stable URL.
public/
images/
logo.png
export default function Logo() {
return (
<img
src="/images/logo.png"
alt="Acme company logo"
width={160}
height={40}
/>
);
}
The public directory itself is not part of the URL. Write /images/logo.png, not /public/images/logo.png. A root-absolute path also avoids failures when the component is rendered on a nested route. Vite documents this behavior in its public directory guide.
Choose this approach for files that must keep a predictable name, are referenced outside JavaScript, or are intentionally served without bundler processing. Imported assets are generally easier to track and fingerprint. In the legacy Create React App workflow, public files are not post-processed or content-hashed; a missing file becomes a runtime 404. Its public-folder documentation describes those trade-offs.
3. Render a remote or dynamic image URL
When the URL comes from props, API data, or state, put the expression in braces. Quoted text is a literal string.
function Avatar({ user }) {
return (
<img
src={user.imageUrl}
alt={`${user.name}'s avatar`}
width={96}
height={96}
/>
);
}
export default function Team({ users }) {
return (
<div>
{users.map((user) => (
<Avatar key={user.id} user={user} />
))}
</div>
);
}
Use src={user.imageUrl}, not src="user.imageUrl". Validate that the final URL is reachable by the browser, uses HTTPS when your page does, and does not require credentials unavailable to an ordinary image request.
Loading data before rendering
import { useEffect, useState } from 'react';
export default function ProductImage({ productId }) {
const [product, setProduct] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/products/${productId}`)
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => {
if (!cancelled) setProduct(data);
})
.catch((err) => {
if (!cancelled) setError(err);
});
return () => {
cancelled = true;
};
}, [productId]);
if (error) return <p role="alert">Image data could not be loaded.</p>;
if (!product) return <p>Loading…</p>;
return (
<img
src={product.imageUrl}
alt={product.name}
width={640}
height={480}
/>
);
}
Fallbacks for broken remote images
import { useState } from 'react';
export function SafeImage({ src, alt, fallback = '/images/fallback.png', ...props }) {
const [currentSrc, setCurrentSrc] = useState(src);
return (
<img
{...props}
src={currentSrc}
alt={alt}
onError={() => {
if (currentSrc !== fallback) setCurrentSrc(fallback);
}}
/>
);
}
Keep the fallback local and avoid an onError loop by checking whether it is already active.
4. Make images accessible and prevent layout shift
- Write concise
alttext that communicates the image’s purpose. - Use
alt=""for purely decorative images so assistive technology skips them. - Add known
widthandheightvalues. The browser can reserve space before the file arrives. - Use
loading="lazy"for noncritical images below the fold. - Keep a critical hero image eager unless your rendering framework specifies another preload strategy.
<img
src={photo}
alt="A developer reviewing code beside a window"
width={1200}
height={800}
loading="lazy"
decoding="async"
/>
Use fetchPriority="low" for noncritical work when appropriate. Do not lazy-load the image that establishes the first visible content unless you have measured that it is safe.
5. Choose between src/assets and public
| Question | Use an import from src |
Use public |
|---|---|---|
| Is the file part of the app bundle? | Yes | Not necessarily |
| Should production files be fingerprinted? | Yes | No |
| Must the URL keep a stable name? | No | Yes |
| Is the image selected at runtime? | Use a known import or an explicit asset map | Use a generated URL |
| Can the bundler discover it? | Static import path required | Served directly |
Dynamic local images
A variable import path such as import(`./assets/${name}.png`) may not work as expected because the bundler must know possible files at build time. Prefer an explicit map:
import sun from './assets/sun.png';
import moon from './assets/moon.png';
const icons = { sun, moon };
export function WeatherIcon({ condition }) {
const src = icons[condition] ?? icons.sun;
return <img src={src} alt="" width={32} height={32} />;
}
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Only the alt text appears | 404, wrong path, or inaccessible remote URL | Open the browser Network panel, inspect the image request, and correct the URL. |
public/... returns 404 |
The public directory was included in the URL | Remove public; use /images/file.png. |
It works at / but fails on another route |
A relative URL resolves against the current route | Use an imported asset or a root-absolute public URL. |
| “Module not found” during build | Incorrect relative path, case mismatch, or missing extension | Check the file name exactly, including capitalization, and verify the file is committed. |
| Remote image is blocked | URL requires authentication, redirects unexpectedly, or the host blocks hotlinking | Use a browser-accessible URL, proxy it through your server when permitted, or store a local copy. |
| Images cause content to jump | No intrinsic dimensions | Set width and height or reserve space with CSS aspect-ratio. |
| Image is stretched | CSS dimensions do not match its aspect ratio | Use object-fit: cover or contain and set the intended aspect ratio. |
| React warns about a missing key | A list of images was rendered without stable keys | Add key={item.id} to the element returned by map. |
7. Performance and reliability checklist
- Choose an appropriate file format and dimensions before shipping; do not send a multi-megapixel source to a small thumbnail.
- Use responsive CSS such as
max-width: 100%so images fit their container. - Set intrinsic dimensions to reduce cumulative layout movement.
- Lazy-load below-the-fold images and keep important above-the-fold images discoverable.
- Use a stable fallback for user-generated or third-party URLs.
- Inspect production URLs after building; imported assets may have hashed names.
- Check cache headers and CDN behavior for remote assets you control.
- For a framework image component, follow that framework’s rules for optimization, preload, and allowed domains. React’s reference notes that framework components can choose different defaults.
.card-image {
display: block;
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
object-fit: cover;
}
8. Test the finished component
- Run the development server and view the component at the route where it will actually appear.
- Open DevTools and confirm each image request returns a successful response.
- Build the application and test the production preview, where asset paths and hashing can differ.
- Disable the network briefly to verify loading and error states.
- Use a screen reader or accessibility checker to confirm informative images have meaningful alternative text and decorative images have empty alternative text.
9. Or skip the browser setup
If you need rendered images of web pages for documentation, previews, tests, or content pipelines, ScreenshotNeo returns a screenshot or PDF from one GET request. Its capture removes cookie and consent banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. It also provides an MCP server for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://react.dev -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://react.dev"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://react.dev' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then((fs) => fs.writeFile('shot.webp', buffer));
Every feature is available on every plan, including full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, click and wait controls, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, usage reporting, and PDF output.
The Free plan includes 1,000 screenshots per month without a card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free. Create a free ScreenshotNeo account.
10. FAQ
Should I put every image in public?
No. Import application assets from src when you want bundling and fingerprinting. Use public for stable, directly served files.
Can I use a URL from an API response?
Yes. Render it as src={data.imageUrl}, validate failures, and provide dimensions and alternative text.
Why does src="{photo}" fail?
Quotes make the value a literal string. JSX expressions use braces without quotes: src={photo}.
Do I need Create React App?
No. Create React App is deprecated for new projects. Use a recommended framework or a supported build setup such as Vite, while applying the same React <img> rules.
What should I do with decorative icons?
Keep alt="" and ensure surrounding text already communicates their meaning.


