How to Add Images to HTML Tables
Put an <img> inside the target <td>, then use valid paths, useful alt text, and responsive sizing for accessible tables.
Put the <img> element inside the <td> cell where the image belongs. Give it a retrievable src and context-appropriate alt text. The image remains table-cell content; it does not change the table’s row and column semantics.
<table>
<caption>Available bicycle colors</caption>
<thead>
<tr>
<th scope="col">Color</th>
<th scope="col">Preview</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Red</th>
<td>
<img src="images/red-bike.jpg" alt="Red bicycle" width="320" height="200">
</td>
</tr>
</tbody>
</table>
The MDN <img> reference describes the element as embedded content. Keep the table structure described by <table>, <tr>, <th>, and <td>; the WAI tables tutorial covers the associated header semantics.
1. Build a complete image table
Use a caption when the purpose of the table is not obvious. Use column headers for each field and row headers when the first cell identifies the row. Put the image in the data cell that represents it.
<table class="catalog">
<caption>Trail shoes by model</caption>
<thead>
<tr>
<th scope="col">Model</th>
<th scope="col">Image</th>
<th scope="col">Weight</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Summit 2</th>
<td>
<img src="/images/summit-2.webp" alt="Summit 2 trail shoe, blue upper" width="240" height="180" loading="lazy">
</td>
<td>280 g</td>
</tr>
<tr>
<th scope="row">Ridge Pro</th>
<td>
<img src="https://cdn.example.com/ridge-pro.jpg" alt="Ridge Pro trail shoe, black and orange" width="240" height="180" loading="lazy">
</td>
<td>295 g</td>
</tr>
</tbody>
</table>
2. Choose the correct src
src can be a relative path or an absolute URL. Relative paths are resolved from the document URL, so spelling, capitalization, directory location, and deployment output all matter.
| Source | Example | Use when |
|---|---|---|
| Relative | images/red-bike.jpg |
The file is deployed with your site. |
| Root-relative | /assets/red-bike.webp |
The asset lives under the site root. |
| Absolute | https://cdn.example.com/red-bike.jpg |
An accessible CDN or other host serves the file. |
Open the final image URL directly in a browser or inspect the network response. A 404, blocked request, authentication requirement, or mixed-content restriction prevents display. Publish only files you have permission to use; the MDN images tutorial discusses image assets and licensing.
3. Write useful alternative text
alt supplies an equivalent description when the image cannot be processed and gives screen-reader users the image’s purpose. Describe information that is relevant to the row, not every visual detail.
- Informative image:
alt="Red bicycle". - Image with a meaningful state:
alt="Out of stock: blue Summit 2 shoe". - Decorative or redundant preview:
alt="". - Image inside a link: describe the destination or action, such as
alt="View Summit 2 details".
Do not omit alt on a content image. An empty value is intentional for decoration or information already provided in adjacent text.
4. Control dimensions and prevent overflow
Declare intrinsic width and height when you know them. They let the browser reserve the aspect ratio while the file loads. Constrain the rendered image with CSS so it stays inside the cell.
.catalog {
border-collapse: collapse;
width: 100%;
}
.catalog th,
.catalog td {
border: 1px solid #d0d7de;
padding: 0.75rem;
text-align: left;
vertical-align: middle;
}
.catalog img {
display: block;
max-width: 100%;
height: auto;
}
If the source dimensions are unknown, do not force both CSS dimensions independently; that can distort the image. Use max-width: 100% and height: auto, or use object-fit: cover with a deliberately fixed box when cropping is acceptable.
5. Use responsive image sources when needed
For multiple sizes, provide srcset and sizes so the browser can select an appropriate file. The table markup stays the same.
<td>
<img
src="/images/shoe-640.jpg"
srcset="/images/shoe-320.jpg 320w,
/images/shoe-640.jpg 640w,
/images/shoe-1280.jpg 1280w"
sizes="(max-width: 700px) 40vw, 240px"
width="240"
height="180"
alt="Blue Summit 2 trail shoe"
>
</td>
Use loading="lazy" for images below the initial viewport when delaying them is acceptable. Keep the first visible images eager enough for the table’s purpose. Responsive candidates do not replace a valid fallback src.
6. Keep table semantics intact
- Use a table only for data with row-and-column relationships, not for page layout.
- Use
<th scope="col">for column headers and<th scope="row">for row labels. - Use a concise
<caption>when it helps identify the table. - Do not replace headers with background images or text embedded in images.
- Keep the image inside the cell whose data it represents.
7. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Image appears outside the intended cell | The <img> is outside <td>, or tags are incorrectly nested. |
Place it between the opening and closing <td>; validate the row and table closing tags. |
| Broken-image icon | Wrong path, filename case, deployment location, or inaccessible host. | Copy the final URL, open it directly, and inspect the network status. |
| Image is wider than the table | The intrinsic width exceeds the cell. | Apply max-width: 100%; height: auto; and check available cell width. |
| Image is stretched | CSS width and height use a different aspect ratio. | Set one dimension to auto, preserve intrinsic dimensions, or use intentional cropping with object-fit. |
| Screen reader announces a filename or nothing useful | Missing or unsuitable alt. |
Write concise, contextual alternative text, or use alt="" for decorative or redundant imagery. |
| Remote image is blocked | Authentication, hotlink protection, CORS policy, or mixed content. | Serve the asset from an accessible origin, use HTTPS, and check response headers and browser console errors. |
8. Performance, reliability, and cost considerations
- Resize files to the largest display size you actually need; oversized originals increase transfer time.
- Choose an efficient format supported by your delivery pipeline, while keeping visual quality appropriate.
- Set intrinsic dimensions to reduce layout movement while images load.
- Use a cacheable URL and a CDN for repeated catalog images.
- Lazy-load long tables, but avoid delaying images that are immediately visible or central to the task.
- Provide a useful text value in the same row so the table remains understandable when an image fails.
9. Or skip the browser setup
If you need rendered table screenshots for documentation, regression checks, or sharing, ScreenshotNeo captures a URL with one request. It accepts cookie and consent banners before capture, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets each step be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; the response reports the page verdict and billing status in X-Page-Verdict and X-Billed headers.
See the ScreenshotNeo API docs for all options. The basic call returns PNG, JPEG, WebP, or PDF output depending on parameters.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com/table.html \
-o table.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/table.html"},
timeout=90,
)
r.raise_for_status()
open("table.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/table.html'
});
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('table.webp', buffer));
You can also capture one element by CSS selector, wait for a selector, delay, or network idle, load lazy images for full-page captures, set a viewport or device preset, use dark mode and retina scale, inject CSS or JavaScript, hide selectors, click before capture, provide headers, cookies, user agents, authorization, timezone, or geolocation, block ads, trackers, requests, or resource types, resize output, cache with a chosen TTL, create signed links, run asynchronous jobs with signed webhooks, capture up to 100 URLs per bulk call, and use the usage API or OpenAPI specification. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 shots per month without a card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.
10. FAQ
Can an image be placed directly inside <tr>?
No. A row must contain cells. Put the image inside a <td> or <th>.
Does adding an image change table accessibility?
No. Accessibility still depends on correct headers, captioning where useful, and appropriate alternative text.
Should every image have non-empty alt text?
No. Decorative or redundant images should use alt=""; informative images need a concise description.
Why does my relative path work locally but fail after deployment?
The deployed document may be in a different directory, the filename case may differ, or the asset may not have been copied to the production output. Inspect the deployed URL directly.
Can I make the table responsive?
Yes. Keep the image fluid with max-width: 100%; height: auto; and choose a table-specific small-screen layout that preserves the relationships between headers and cells.


