Use an <img> when the picture is content: a product photo, a chart, a portrait. Give it alt text, width and height. Use a CSS background-image when the picture is decoration behind text. Add srcset and sizes so phones download smaller files, and loading="lazy" for images below the first screen, never for the hero.
| Question | <img> | background-image |
|---|---|---|
| Does the picture carry meaning? | Yes, with alt text | No, decoration only |
| Should search engines index it? | Yes | Rarely picked up |
| Responsive files | srcset and sizes | image-set() or media queries |
| Lazy loading | loading="lazy" | Needs script |
| Crop to fill a box | object-fit: cover | background-size: cover |
| Printed by default | Yes | Usually not |
Updated
A decision table, a srcset builder that shows which file a phone or laptop would download, and editable examples of every pattern: alt text, fixed dimensions, lazy loading, a fast hero image, cropping with object-fit next to background-size, modern formats with <picture>, and responsive backgrounds with image-set().
Enter the widths you export, choose how wide the image is drawn, and see which file a device would download. Example output for 480, 960 and 1600 pixel files in a half-width layout:
<img src="/img/photo-960.jpg"
srcset="/img/photo-480.jpg 480w, /img/photo-960.jpg 960w, /img/photo-1600.jpg 1600w"
sizes="(min-width: 768px) 50vw, 100vw"
alt="Describe the photo" width="1600" height="(your height)" loading="lazy">The attributes give the real size; CSS makes it fluid. Because of width and height the box is reserved before the file loads. The demo images are small inline SVGs.
<style>img { max-width: 100%; height: auto; border-radius: 8px }</style>
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='300'%3E%3Crect width='600' height='300' fill='%230ea5e9'/%3E%3Ccircle cx='432' cy='105' r='54' fill='%23fde68a'/%3E%3Cpath d='M0 300 L210 135 L360 300Z' fill='%23065f46'/%3E%3Ctext x='8' y='20' font-size='14' fill='white' font-family='sans-serif'%3E600 x 300%3C/text%3E%3C/svg%3E"
alt="Pine hills under a low sun at dusk"
width="600" height="300" loading="lazy" decoding="async">Use this on the one large image at the top of the page. Everything below the first screen gets loading="lazy" instead.
<img src="/img/hero-1600.jpg"
srcset="/img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"
sizes="100vw"
alt="Chef plating a dish at the pass"
width="1600" height="900"
fetchpriority="high">| Image | alt |
|---|---|
| Product photo | Describe the item and the detail that matters: Oak side table with two drawers |
| Chart or diagram | State the takeaway: Sales doubled between March and June |
| Image that is a link | Name the destination: Home |
| Decorative pattern or divider | Empty: alt="" |
| Photo with the same caption beside it | Empty, or a short label, so it is not read twice |
| Text inside an image | The same text |
Both crop a picture to fill a box without stretching it. object-position and background-position choose which part stays in view.
Left is an <img> with object-fit; right is a div with a background. They look the same; only the left one has alt text and lazy loading.
<style>
.box { width: 46%; height: 150px; display: inline-block; border-radius: 8px; vertical-align: top }
img.box { object-fit: cover; object-position: 70% 30% }
div.box { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='300'%3E%3Crect width='600' height='300' fill='%230ea5e9'/%3E%3Ccircle cx='432' cy='105' r='54' fill='%23fde68a'/%3E%3Cpath d='M0 300 L210 135 L360 300Z' fill='%23065f46'/%3E%3Ctext x='8' y='20' font-size='14' fill='white' font-family='sans-serif'%3E600 x 300%3C/text%3E%3C/svg%3E") 70% 30% / cover no-repeat }
</style>
<img class="box" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='300'%3E%3Crect width='600' height='300' fill='%230ea5e9'/%3E%3Ccircle cx='432' cy='105' r='54' fill='%23fde68a'/%3E%3Cpath d='M0 300 L210 135 L360 300Z' fill='%23065f46'/%3E%3Ctext x='8' y='20' font-size='14' fill='white' font-family='sans-serif'%3E600 x 300%3C/text%3E%3C/svg%3E" alt="Pine hills under a low sun" width="600" height="300" loading="lazy">
<div class="box" role="presentation"></div>The dark overlay gradient keeps the white text readable on any photo, and the background colour shows if the image fails.
<style>
.banner {
padding: 48px 20px; color: #fff; font: 700 24px system-ui; border-radius: 10px;
background-color: #0c4a6e;
background-image: linear-gradient(rgba(0,0,0,.45), rgba(0,0,0,.45)), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='300'%3E%3Crect width='600' height='300' fill='%230ea5e9'/%3E%3Ccircle cx='432' cy='105' r='54' fill='%23fde68a'/%3E%3Cpath d='M0 300 L210 135 L360 300Z' fill='%23065f46'/%3E%3Ctext x='8' y='20' font-size='14' fill='white' font-family='sans-serif'%3E600 x 300%3C/text%3E%3C/svg%3E");
background-size: cover; background-position: center;
}
</style>
<div class="banner">Spring walks start in April</div>image-set() lets the browser choose by pixel density or by file type. Keep a plain url() line first for older browsers.
.hero {
background-image: url("/img/hero.jpg");
background-image: image-set(
url("/img/hero.avif") type("image/avif"),
url("/img/hero.webp") type("image/webp"),
url("/img/hero.jpg") type("image/jpeg")
);
background-size: cover;
}
@media (max-width: 640px) {
.hero { background-image: url("/img/hero-small.jpg"); }
}The browser uses the first <source> whose type it supports. The <img> inside carries the alt text, the size and the fallback file, and is the element you style.
<picture>
<source type="image/avif" srcset="/img/table-800.avif 800w, /img/table-1600.avif 1600w" sizes="(min-width: 768px) 50vw, 100vw">
<source type="image/webp" srcset="/img/table-800.webp 800w, /img/table-1600.webp 1600w" sizes="(min-width: 768px) 50vw, 100vw">
<img src="/img/table-800.jpg" srcset="/img/table-800.jpg 800w, /img/table-1600.jpg 1600w"
sizes="(min-width: 768px) 50vw, 100vw"
alt="Oak side table with two drawers" width="1600" height="1067" loading="lazy">
</picture>
The question that settles most image decisions is whether a visitor would lose information if the picture failed to load. A team photo on an about page, a screenshot in a tutorial and a product shot in a shop all carry meaning, so they belong in the HTML with alt text.
A blurred texture behind a heading, a pattern in a footer or a photo that only sets the mood can live in CSS. That keeps the HTML clean and lets you swap the picture per screen size without touching markup. If text sits on top, make sure it still reads when the image is missing by setting a background colour too.
With width descriptors the browser first reads sizes to learn how wide the image will be drawn, for example 100vw on phones and 50vw on wider screens. It multiplies that by the device pixel ratio and picks the smallest file that is at least that wide. Browsers may also pick a smaller file on a slow connection or a larger one already in cache.
Getting sizes right matters more than adding more widths. If sizes says 100vw but the image only fills a third of the screen on desktop, the browser downloads a file three times too wide.
When an image without dimensions loads, everything below it jumps down. Width and height attributes let the browser compute the aspect ratio up front, which removes that jump and improves Cumulative Layout Shift.
The hero image is usually the Largest Contentful Paint element. Keep it eager, give it fetchpriority="high", serve it from your own domain or a fast image host, and avoid hiding it behind a script that sets it later.
Search engines index images found in <img> elements far more reliably than ones set in CSS. If you want a picture to appear in image search or to describe your content, put it in an <img> with good alt text. Decorative backgrounds lose nothing by being CSS.
Say what the image shows that matters in this context, in a short sentence. For a product photo that might be the product and its colour; for a chart, the main finding. Skip phrases like image of. If the image is a link, describe where the link goes.
Width descriptors (480w, 960w) list each file's real width and let the browser choose using the sizes attribute and the screen's pixel density. Density descriptors (1x, 2x) suit images that are always shown at one fixed size, such as a logo.
Native lazy loading works in all current major browsers, so a plain attribute is enough for most images. Framework image components often add it for you; check that they do not also lazy-load the hero image.
Not always. Both formats are supported by current major browsers. AVIF is often smaller for photos but slower to encode. A <picture> element with an AVIF source, a WebP source and a JPEG fallback covers everyone, but WebP alone with a JPEG fallback is a reasonable simpler choice.
Related: Image compressor, WebP converter, Alt text helper, Image cropper, HTML links guide.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.