---
name: responsive-images
description: "Audits and fixes HTML image markup. Runs a read-only checker for missing or filler alt text, lazy-loaded LCP images, wrong or missing srcset and sizes, mixed w and x descriptors, absent width and height and broken picture elements, then derives sizes from your CSS widths with a calculator. Use for responsive images, srcset, sizes, picture, loading=lazy, fetchpriority, alt text, image layout shift or image pull requests. Do NOT use for compressing images, image SEO, server-side LCP causes, video or SVG."
license: CC-BY-4.0
metadata:
  author: Afeez Adebayo
  author_url: https://afeezadebayo.com
  author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
  author_x: https://x.com/za_icon
  source: https://afeezadebayo.com/skills-md/frontend-development/html-semantics/responsive-images/
  category: frontend-development
  subcategory: html-semantics
  version: "1.0.0"
  created: "2026-10-24"
  updated: "2026-10-24"
---

# Responsive Images: srcset, sizes, Loading and Alt Text

An agent audits and fixes `img` and `picture` markup in this order: run a read-only checker, classify each image by role, derive `sizes` from the real CSS layout with a calculator script, then edit in small approved batches. It reads the checker's output, not every file.

It works on plain HTML and on rendered framework output. Browser and source facts sit in `references/appendix.md`.

## When to use this skill, and when not to

Use it to audit or fix image markup: wrong or missing `srcset` and `sizes`, lazy-loaded hero images, layout shift from images, `picture` elements, missing or poor alt text, or a pull request that adds images.

| Request | Use instead |
|---|---|
| Headings, landmarks, links and buttons around the images | `semantic-html.md` |
| Image file names, image sitemaps, Google Images ranking | `image-seo.md` |
| Diagnosing a slow LCP beyond the image element (server, render-blocking CSS) | `lcp-optimisation.md` |
| Fixing layout shift from fonts, ads or embeds | `cls-optimisation.md` |
| Compressing, resizing or converting image files | The build tooling or an image CDN. This skill specifies widths and formats and writes no image files |
| Video, audio, canvas, inline SVG | Out of scope |

## Guardrails

1. **Propose, then change.** Report and plan first. Edit files only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Never change an image URL or file name on its own.** CMS records, caches and redirects depend on them. A new derivative file the plan needs is listed for the user to create.
3. **Batches stay small.** At most 2 files or 30 `img` changes per batch (defaults), each shown as a diff.
4. **No image leaves the project.** The agent does not upload images to online tools or paste image data into prompts. Photos can show faces, number plates and children, and EXIF data can hold GPS location. If the agent must look at an image to write alt text, it views it locally and never copies EXIF fields into a report.
5. **Never invent alt text for an image it cannot see.** Ask the user (step 7).
6. **Nothing is invented.** Thresholds are labelled defaults. Source facts carry a date. Version-dependent facts carry **verify before relying on this**.
7. **Not legal advice.** Alt text affects accessibility law compliance. The agent marks up images correctly and routes any conformance statement to a person.
8. **Refuses to:** upscale an image, add `loading="lazy"` to a likely LCP image, put `fetchpriority="high"` on more than two images (default), or remove alt text to make the checker pass.

## Inputs the agent must have before starting

Ask for what is missing in one message and wait. Do not proceed on assumptions.

| # | Input | What to ask if missing |
|---|---|---|
| 1 | Target | "Which pages or templates? File paths, or rendered HTML saved from the page." |
| 2 | Rendered output | "Is the markup written by hand or generated (CMS, React, Next, a theme)? If generated, can you save the rendered HTML?" |
| 3 | Layout widths | "How wide is each image slot in CSS at each breakpoint? Paste the CSS or tell me the widths." |
| 4 | Source files | "What is the widest original for each image, in pixels, and which widths already exist?" |
| 5 | LCP evidence | "Which element does Lighthouse or Chrome DevTools report as the LCP element on mobile?" |
| 6 | Image meaning | "Where an image carries information, what does it show or say?" |
| 7 | Dependants | "Do tests, analytics or a CMS read image attributes or class names?" |

If input 5 is unknown, the agent treats the largest above-the-fold image as the candidate, writes "LCP assumed" in the report and asks the user to confirm in DevTools. If input 3 is unknown, it does not write `sizes`. A guessed `sizes` is worse than none, because the browser trusts it.

## Workflow

### Step 0: Confirm scope

Confirm inputs 1 to 7 and the branch name. Tell the user the plan: checker, roles, LCP, srcset and sizes, dimensions, picture, alt, batches, verification. Gate 1: wait for a yes.

### Step 1: Run the checker

Run `python3 scripts/img_check.py page.html` on rendered HTML. It reads files, writes nothing, exits 1 on any error and prints `file:line level code message`. Pipe it through `head -60` and read the output only. Do not read the script. Keep the output as the baseline.

| Code | Level | Fix |
|---|---|---|
| `no-alt` | error | Step 7. Missing is not the same as empty |
| `junk-alt` | warn | Step 7 |
| `empty-link-image` | error | Step 7, linked-image rule |
| `lazy-lcp` | error | Remove `loading="lazy"`, apply step 3 |
| `sizes-without-w` | error | Add `w` descriptors (step 4) or delete `sizes` |
| `w-without-sizes` | warn | Add `sizes` from the calculator (step 4) |
| `mixed-descriptors` | error | One unit per `srcset` (step 4) |
| `sizes-auto-needs-lazy` | error | Give an explicit `sizes` (step 4) |
| `too-many-candidates` | warn | Trim the ladder (step 4) |
| `no-dimensions` | warn | Step 5 |
| `many-high` | warn | Step 3 |
| `picture-no-img` | error | Step 6 |
| `source-no-type-or-media` | warn | Step 6 |
| `bad-loading`, `bad-fetchpriority` | warn | Use the listed values only |

The checker cannot see CSS, the real LCP element or what an image shows. `lazy-lcp` uses a guess: `fetchpriority="high"`, a class or id containing hero, banner, masthead, cover or featured, or the first image in the file. Treat each hit as a question for step 3, not a verdict.

### Step 2: Classify each image

Assign every `img` one role. The role fixes its loading and alt behaviour.

| Role | `loading` | `fetchpriority` | alt |
|---|---|---|---|
| LCP candidate | none (eager by default) | `high` | Informative rule |
| Above the fold, not LCP | none | none | By content |
| Below the fold | `lazy` | none | By content |
| Decorative | `lazy` unless above the fold | none | `alt=""` |
| Functional (logo link, icon button) | none | none | Destination or action |

Decide the fold at the smallest viewport the user cares about, 360 CSS pixels wide by default. An image that is below the fold on desktop but above it on mobile counts as above.

### Step 3: Decide the LCP image

1. Use the element the user named in input 5. Otherwise use the largest above-the-fold image and mark it assumed.
2. It gets no `loading="lazy"`. web.dev: "Never lazy-load your LCP image."
3. It gets `fetchpriority="high"`. Across the page, at most 2 images carry it (default, from web.dev's "one or two" guidance).
4. It must be findable in the initial HTML through `src` or `srcset`. If the HTML has no `img` for it because CSS or script inserts it, either move it into the HTML or add the preload in `references/decisions.md` (read it only for this case).
5. Compressing a hero image does not always improve LCP, because the saved time can move into render delay (web.dev). If the user's goal is LCP, hand the next diagnosis to `lcp-optimisation.md`.

### Step 4: srcset and sizes

1. Pick the unit. A picture that scales with the layout gets `w` descriptors plus `sizes`. A picture shown at one fixed CSS size, such as a logo, gets `x` descriptors and no `sizes`. Never mix the two.
2. Run the calculator with the real CSS widths from input 3:

```
python3 scripts/sizes_calc.py --rule "(min-width: 1024px) 400px" --rule "(min-width: 640px) 50vw" --default "calc(100vw - 32px)" --max 1600
```

3. Copy its `sizes` line and `srcset widths` line. `--max` is the widest original from input 4. The calculator never plans a width above it, because upscaling adds bytes and no detail.
4. Keep the ladder at 6 candidates or fewer (default). Shopify Dev suggests about 4 to 6, because extra widths hurt cache reuse.
5. If a needed width has no file, list it in the plan as a file for the user to produce. Do not point `srcset` at a file that does not exist.
6. `sizes="auto"` is allowed only on images with `loading="lazy"` (Shopify Dev). Use it only if the user's framework or team already uses it, and **verify before relying on this** in the target browsers. The LCP image always needs an explicit `sizes`.
7. A `sizes` that disagrees with the CSS makes the browser fetch the wrong file. After any layout change, re-run the calculator.

### Step 5: Dimensions

Every `img` gets `width` and `height` equal to the intrinsic pixels of its `src` file, plus CSS `max-width: 100%; height: auto`, so the browser reserves space before the file loads. Use the table in `references/decisions.md` only when CSS crops the image. Layout shift beyond images goes to `cls-optimisation.md`.

### Step 6: picture

1. `picture` is for two jobs only: art direction (a different crop per viewport, with `source media`) and format switching (`source type`, best format first). Resolution switching alone uses `img` with `srcset`.
2. Each `source` has `type` or `media`. Without either it always matches and hides the sources below it.
3. The `picture` ends with an `img` that carries `src`, `alt`, `width`, `height` and `loading`. Put `fetchpriority` on that `img`.
4. Do not preload one image in several formats (web.dev warns of extra downloads).

### Step 7: Alt text

1. Classify each image with the W3C table in `references/decisions.md` (read it only if the checker flagged `no-alt`, `junk-alt` or `empty-link-image`, or the user wants alt text written).
2. `alt=""` marks decoration. A missing `alt` is a defect. Never remove alt to silence the checker.
3. An image that is the only content of a link needs alt that names the destination.
4. If the agent cannot view the image and nearby text does not say what it shows, list it under "needs a human" with the exact question. Do not write a guess into the file.
5. Do not start alt with "image of" or "picture of", and do not repeat the file name.
6. Report alt text written by the agent as "drafted, needs review". A person confirms any image whose meaning matters (products, people, charts).

### Step 8: Plan and apply in batches

1. Turn the checker output and the decisions into a change list: file and line, current markup, new markup, rule (step number).
2. Search for dependants (commands below). Any class or attribute a test or script reads is left alone.
3. Group into batches under guardrail 3. Gate 2: show each batch's diff and wait for a yes. Edit in place. Do not echo unchanged markup back.

### Step 9: Verify

1. Re-run `img_check.py`. Pass: 0 errors on the changed files and no new warnings.
2. Right-size check. Load the page in a throttled mobile viewport and a desktop one, then run in the DevTools console:

```
[...document.images].map(i => [i.currentSrc.split('/').pop(), i.naturalWidth, i.clientWidth, devicePixelRatio])
```

Pass (default tolerance chosen by this skill, not a standard): `naturalWidth` is at least `clientWidth × devicePixelRatio` and no more than 1.5 times it. Under means blur. Over means wasted bytes, so recheck `sizes`.
3. Priority check. In the Network panel the LCP image request shows priority High and starts early, and no other image shows High unless step 3 allows it.
4. Lighthouse or CrUX. If the user's goal was LCP, compare LCP and its subparts before and after. web.dev's guideline is under about 10% of LCP time in each delay subpart. Report the numbers. Do not claim an improvement the numbers do not show.
5. Alt spot check. A screen reader or the accessibility tree announces three sampled images with the intended text, and decorative images are skipped.
6. Layout. No visible jump as images load, at 360 and 1280 CSS pixels wide.

### Step 10: Report

Use the report template. State what was changed, what was assumed (LCP, widths), files the user must produce, alt text needing review and what was not checked.

## Worked example

The page is invented. The checker output is real: it came from running the scripts in this skill on these files.

**Input.** A farm shop home page. The user says: hero is the LCP element in Lighthouse; cards sit in a 3-column grid on desktop (400px slots at 1024px and above), 50% width from 640px, full width less 32px padding below; widest original is 1600px.

**Step 1, checker (before):**

```
page_before.html:3   error  empty-link-image  link contains only an image with alt="", so the link has no name
page_before.html:5   error  lazy-lcp  loading=lazy on a likely LCP image (hero class); confirm with a real LCP element
page_before.html:5   warn   junk-alt  alt looks like a filename or filler: 'hero.jpg'
page_before.html:5   warn   no-dimensions  no width and height (or inline aspect-ratio): layout shift risk
page_before.html:5   warn   w-without-sizes  w descriptors without sizes: browser assumes 100vw
page_before.html:7   error  mixed-descriptors  srcset mixes w and x descriptors
page_before.html:8   error  sizes-without-w  sizes has no effect without w descriptors in srcset
page_before.html:9   error  sizes-auto-needs-lazy  sizes=auto is valid only with loading=lazy; eager images need explicit sizes
page_before.html:10  error  no-alt  no alt attribute (missing is not the same as empty)
page_before.html:10  warn   source-no-type-or-media  source has neither type nor media, so it always matches
page_before.html:11  error  picture-no-img  picture has no img fallback, so nothing renders
page_before.html: 7 errors, 4 warnings
```

**Steps 2 to 7, decisions.**

| Line | Change | Step |
|---|---|---|
| 3 | Logo link alt becomes "Greenfield Farm home" | 7.3 |
| 5 | Remove `lazy`, add `fetchpriority="high"`, `sizes="100vw"`, `width="1600" height="800"`, descriptive alt (the user supplied what it shows) | 3, 4, 5, 7 |
| 7 | One unit only: `w` descriptors with the card `sizes` | 4.1 |
| 8 | Add `w` descriptors because `sizes` was present | 4.1 |
| 9 | Explicit `sizes`, because this image is eager | 4.6 |
| 10 | Alt "Packing shed" drafted, flagged for review | 7.6 |
| 11 | Delete the `picture` with no `img`, or add the `img` fallback if the user wants the format switch | 6.3 |

The calculator output for the card slots, which the plan uses:

```
sizes="(min-width: 1024px) 400px, (min-width: 640px) 50vw, calc(100vw - 32px)"
srcset widths: 400w, 800w
```

The 800w file already exists and the 400w file does not, so the plan lists it as one file for the user to produce. The user approves one batch.

**Step 9.** `page_after.html: 0 errors, 0 warnings`. The user runs the DevTools check on the hero: `naturalWidth` 1600, `clientWidth` 800, `devicePixelRatio` 2, ratio 1.0, pass. Network shows the hero at priority High. The report lists one open item: the alt text on the packing shed photo needs the owner's confirmation.

## Templates

### Hero (LCP) image

```html
<img src="/img/hero-1600.jpg"
     srcset="/img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"
     sizes="100vw" width="1600" height="800"
     alt="Rows of seedlings in a greenhouse at sunrise" fetchpriority="high">
```

### Card image below the fold

```html
<img src="/img/card-600.jpg"
     srcset="/img/card-400.jpg 400w, /img/card-800.jpg 800w"
     sizes="(min-width: 1024px) 400px, (min-width: 640px) 50vw, calc(100vw - 32px)"
     width="800" height="533" alt="Harvest crates" loading="lazy">
```

### Fixed-size logo

```html
<img src="/logo-120.png" srcset="/logo-120.png 1x, /logo-240.png 2x" width="120" height="32" alt="Greenfield Farm home">
```

### Format switch with fallback

```html
<picture>
  <source type="image/avif" srcset="/img/p-800.avif 800w, /img/p-1600.avif 1600w" sizes="100vw">
  <img src="/img/p-800.jpg" srcset="/img/p-800.jpg 800w, /img/p-1600.jpg 1600w" sizes="100vw"
       width="1600" height="800" alt="Packing shed" loading="lazy">
</picture>
```

### Finding dependants

```
grep -rnE "(img|picture)[^>]*(class|id)=" --include=*.test.* --include=*.spec.* .
grep -rn "currentSrc\|naturalWidth\|\.srcset" --include=*.js --include=*.ts .
```

### Report

```
RESPONSIVE IMAGES REPORT: [pages], [date], branch [name]
Baseline: [n] errors, [n] warnings        After: [n] errors, [n] warnings
LCP element: [selector] ([confirmed in DevTools / assumed])
Changed: lazy-lcp [n], sizes [n], dimensions [n], picture [n], alt [n]
Files the user must produce: [path, width]
Alt text drafted, needs review: [list]
Right-size check: [pass/fail per sampled image]. LCP before/after: [ms or "not measured"]
Not checked: [browsers, CMS output, pages not saved]
```

### Starting prompt

```
Use the responsive-images skill on [files]. The hero is the LCP element.
Slot widths: [widths]. Widest originals: [px]. Run the checker first, write
nothing until I approve the plan, and do not change any image URL.
```

## Failure modes and anti-patterns

| # | Failure | How to spot it | What to do instead |
|---|---|---|---|
| 1 | Lazy-loaded LCP image | `lazy-lcp`, or LCP resource load delay above the guideline of about 10% | Step 3. Confirm the element in DevTools before and after |
| 2 | `sizes` that does not match the CSS | The step 9.2 ratio is far above 1.5 or below 1 | Re-derive with the calculator from the real CSS. Never paste `100vw` as a habit |
| 3 | `picture` used for resolution switching, or `source` with no `type` or `media` | `source-no-type-or-media`, duplicated `srcset` logic | Step 6.1. Plain `img` with `srcset` unless the crop or format changes |
| 4 | Alt missing, filler or removed to pass a check | `no-alt`, `junk-alt`, `empty-link-image` | Step 7. Missing is a defect, empty is a decision |
| 5 | `fetchpriority` or preload used everywhere | `many-high`, several preloads in the head | Step 3.3. web.dev warns that overuse overrides the browser's own priorities |

## Quality bar

Check every item before handing over. Rules are in the step named, not repeated here.

1. Gate 1 and gate 2 approvals are recorded and the work is on a named branch.
2. The baseline and final checker output are in the report, and the final run shows 0 errors on changed files.
3. Every `img` has a role from step 2.
4. The LCP element is named and marked confirmed or assumed (step 3).
5. No `sizes` was written without real CSS widths, and each came from the calculator (step 4).
6. Every file named in a `srcset` exists, or is listed as a file for the user to produce.
7. No image URL, file name or class read by a dependant changed without the user's yes.
8. Step 9.2 was run on the LCP image and at least two other images, with the numbers in the report.
9. Alt text written by the agent is marked "drafted, needs review", and unknown images are listed with a question.
10. The report and any examples contain no EXIF data, personal data or image content copied from private files.

## Cost

Figures are estimates from this skill's own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. Rival figures are estimates from the sizes shown on the pages I read.

| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 108 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 5,439 | Estimate | When the skill fires |
| C. `references/decisions.md` | 983 | Estimate | Alt text needs classifying, CSS hides the LCP image, or CSS crops an image |
| C. `references/appendix.md` | 798 | Estimate | Only to check a claim |
| Script output: `img_check.py` (this page's example) | 349 | Estimate | Every run. Capped at 60 lines by `head -60`, about 1,700 tokens |
| Script output: `sizes_calc.py` | about 80 | Estimate | When `srcset` or `sizes` change |
| D. Typical run (A + B + decisions + script output) | 6,959 | Estimate | Common path |
| D. Worst case (A + B + both references + 60 lines of output) | 9,028 | Estimate | Large page, claim check |
| E. Cheaper path | 5,976 | Estimate | Run the checker, fix only the flagged lines, skip the references |

Rival sizes, estimated from the pages I read: jezweb responsive-images about 9,100 characters, so roughly 2,600 tokens; thedaviddias responsive-images about 1,300 characters in the body plus a `references/rule.md` I could not size, so roughly 370 tokens before that file; kostja94 image-optimization could not be sized from the page. This skill costs more to load than the first two. What the scripts save is the agent reading and reasoning over every `img` tag, and I have not measured that saving.

## Handoffs

`semantic-html.md` covers markup around images. `image-seo.md` covers file names, image sitemaps and search. `lcp-optimisation.md` and `core-web-vitals.md` cover the rest of LCP. `cls-optimisation.md` covers layout shift. `html-forms.md` covers form controls.

## Appendix

Tool-specific facts, thresholds and the rival comparison sit in `references/appendix.md`. Read it only to check a claim.

## Sources

Last verified 24 October 2026 for every source, except where a date says otherwise.

- web.dev, Responsive images (updated 9 December 2021): https://web.dev/learn/design/responsive-images
- web.dev, Optimize Largest Contentful Paint (updated 31 March 2025): https://web.dev/articles/optimize-lcp
- W3C WAI, An alt decision tree (updated 13 May 2024): https://www.w3.org/WAI/tutorials/images/decision-tree/
- WebAIM, The WebAIM Million 2026 (data February 2026): https://webaim.org/projects/million/
- Shopify Dev, Serve correctly sized images with srcset and sizes (no date shown): https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-responsive-images
- jezweb, claude-skills, responsive-images (via playbooks.com). Rival implementation: https://playbooks.com/skills/jezweb/claude-skills/responsive-images
- thedaviddias, Front-End-Checklist, responsive-images (via tessl.io). Rival implementation: https://tessl.io/registry/skills/github/thedaviddias/Front-End-Checklist/responsive-images
- kostja94, marketing-skills, image-optimization (via claudemarketplaces.com). Rival implementation: https://claudemarketplaces.com/skills/kostja94/marketing-skills/image-optimization
- Anthropic, Agent Skills best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices

---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/html-semantics/responsive-images/

Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.
