HTML and Semantics skill 3 of 4

Responsive Images SKILL.md: srcset, sizes, Lazy Loading and Alt Text

The Responsive Images skill makes Claude, Cursor or another AI agent audit and fix image markup. A read-only checker flags lazy-loaded hero images, missing alt text, mixed srcset units and absent dimensions, and a calculator builds sizes from your real CSS widths. The agent edits in small approved batches and never changes an image URL.

responsive-images/SKILL.md, version 1.0.0, 3,732 words, plus scripts/img_check.py and scripts/sizes_calc.py.

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.

RequestUse instead
Headings, landmarks, links and buttons around the imagessemantic-html.md
Image file names, image sitemaps, Google Images rankingimage-seo.md
Diagnosing a slow LCP beyond the image element (server, render-blocking CSS)lcp-optimisation.md
Fixing layout shift from fonts, ads or embedscls-optimisation.md
Compressing, resizing or converting image filesThe build tooling or an image CDN. This skill specifies widths and formats and writes no image files
Video, audio, canvas, inline SVGOut 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.

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

Raw SKILL.md file

.md

This is the exact file agents read, including the credit header and licence notice. Save it as responsive-images/SKILL.md.

---
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.

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.

CodeLevelFix
no-alterrorStep 7. Missing is not the same as empty
junk-altwarnStep 7
empty-link-imageerrorStep 7, linked-image rule
lazy-lcperrorRemove loading="lazy", apply step 3
sizes-without-werrorAdd w descriptors (step 4) or delete sizes
w-without-sizeswarnAdd sizes from the calculator (step 4)
mixed-descriptorserrorOne unit per srcset (step 4)
sizes-auto-needs-lazyerrorGive an explicit sizes (step 4)
too-many-candidateswarnTrim the ladder (step 4)
no-dimensionswarnStep 5
many-highwarnStep 3
picture-no-imgerrorStep 6
source-no-type-or-mediawarnStep 6
bad-loading, bad-fetchprioritywarnUse 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.

Roleloadingfetchpriorityalt
LCP candidatenone (eager by default)highInformative rule
Above the fold, not LCPnonenoneBy content
Below the foldlazynoneBy content
Decorativelazy unless above the foldnonealt=""
Functional (logo link, icon button)nonenoneDestination 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
  1. 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.
  2. Keep the ladder at 6 candidates or fewer (default). Shopify Dev suggests about 4 to 6, because extra widths hurt cache reuse.
  3. 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.
  4. 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.
  5. 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.

  1. 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.
  2. 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.
  3. Alt spot check. A screen reader or the accessibility tree announces three sampled images with the intended text, and decorative images are skipped.
  4. 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.

LineChangeStep
3Logo link alt becomes "Greenfield Farm home"7.3
5Remove lazy, add fetchpriority="high", sizes="100vw", width="1600" height="800", descriptive alt (the user supplied what it shows)3, 4, 5, 7
7One unit only: w descriptors with the card sizes4.1
8Add w descriptors because sizes was present4.1
9Explicit sizes, because this image is eager4.6
10Alt "Packing shed" drafted, flagged for review7.6
11Delete the picture with no img, or add the img fallback if the user wants the format switch6.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

<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

<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">
<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

<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

#FailureHow to spot itWhat to do instead
1Lazy-loaded LCP imagelazy-lcp, or LCP resource load delay above the guideline of about 10%Step 3. Confirm the element in DevTools before and after
2sizes that does not match the CSSThe step 9.2 ratio is far above 1.5 or below 1Re-derive with the calculator from the real CSS. Never paste 100vw as a habit
3picture used for resolution switching, or source with no type or mediasource-no-type-or-media, duplicated srcset logicStep 6.1. Plain img with srcset unless the crop or format changes
4Alt missing, filler or removed to pass a checkno-alt, junk-alt, empty-link-imageStep 7. Missing is a defect, empty is a decision
5fetchpriority or preload used everywheremany-high, several preloads in the headStep 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.

ItemTokensStatusRead condition
A. Description (always loaded)108EstimateEvery session with the skill installed
B. SKILL.md body5,439EstimateWhen the skill fires
C. references/decisions.md983EstimateAlt text needs classifying, CSS hides the LCP image, or CSS crops an image
C. references/appendix.md798EstimateOnly to check a claim
Script output: img_check.py (this page's example)349EstimateEvery run. Capped at 60 lines by head -60, about 1,700 tokens
Script output: sizes_calc.pyabout 80EstimateWhen srcset or sizes change
D. Typical run (A + B + decisions + script output)6,959EstimateCommon path
D. Worst case (A + B + both references + 60 lines of output)9,028EstimateLarge page, claim check
E. Cheaper path5,976EstimateRun 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.

What this skill does

Image markup decides two things most teams never test: whether the largest element paints early, and whether a screen reader user hears anything useful. The WebAIM Million 2026 report (data collected February 2026) found missing alt text on 16.2% of images, across 53.1% of one million home pages. web.dev says never to lazy-load the largest contentful paint image, and one stray loading="lazy" on a hero undoes that advice.

This skill runs a short Python checker that reports 15 problem codes with line numbers, gives every image a role and finds the likely LCP image. A second script takes your CSS slot widths and prints a sizes attribute and a srcset width ladder capped at your widest original, so the agent never guesses sizes or upscales. Edits go in batches of at most 2 files or 30 images, and each batch needs your yes.

I read three published image skills before writing this one. One gives width presets without checking your layout, one covers sizes mismatches but states no thresholds or loading rules, and one is led by image SEO. None of the pages I read showed a checker script.

Who it is for

Use it if you maintain a site, theme or component library and want image markup that loads fast, avoids layout shift and reads well in a screen reader. It suits frontend developers, performance engineers, content teams working in a CMS theme and anyone reviewing AI-generated markup. It checks rendered HTML, so it works with any framework or CMS.

It is the wrong skill for headings, links and landmarks (use Semantic HTML), for file names and image search (use Image SEO), for server-side LCP causes (use LCP Optimisation) and for compressing or converting image files.

What is inside

The file is 3,732 words (version 1.0.0) with two scripts and two reference files. It holds:

  • Eight guardrails, including no image URL changes, no upscaling, no images sent to online tools and no invented alt text.
  • Seven inputs the agent must have first, with the exact question for each.
  • A ten-step workflow with a role table, LCP rules, srcset and sizes rules, picture rules and a verification pass.
  • Two standard-library scripts: scripts/img_check.py and scripts/sizes_calc.py.
  • A worked example with real checker output before and after, plus templates for a hero image, card image, logo, format switch, report and starting prompt.
  • Five failure modes, a ten-item quality bar and a cost section.

The token costs below are estimates from the skill’s own count: words times 1.35 for prose and characters divided by 3.5 for code and tables. They are not from a model tokeniser.

ItemTokensStatus
Description (always loaded)108Estimate
SKILL.md body (when the skill fires)5,439Estimate
references/decisions.md, read when alt text needs classifying, CSS hides the LCP image or CSS crops an image983Estimate
references/appendix.md, read only to check a claim798Estimate
Typical run (description, body, one reference, checker output)6,959Estimate
Cheaper path (checker, fix flagged lines, no references)5,976Estimate
Worst case (both references, 60 lines of checker output)9,028Estimate

How to install and use it in Claude, Cursor and other agents

Claude. Keep the folder as responsive-images/ with SKILL.md, scripts/ and references/ inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ or .claude/skills/ in a project.

Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the scripts beside your code. Then send the starting prompt from the Templates section.

Before you install. Read both scripts first. The checker reads files and writes nothing, and the calculator only prints. The skill never uploads your images. Install skills only from sources you trust.

A worked example

An invented farm shop home page has a hero with loading="lazy" and a filename as alt text, a logo link whose image has empty alt, a srcset mixing w and x units, a sizes with no w descriptors, an eager image with sizes="auto" and a picture with no img. The checker reports 7 errors and 4 warnings.

You tell the agent the hero is the LCP element and give your slot widths. The calculator returns sizes="(min-width: 1024px) 400px, (min-width: 640px) 50vw, calc(100vw - 32px)" and a ladder of 400w and 800w. The agent removes lazy from the hero, adds fetchpriority="high", fixes the units, lists the one missing 400w file for you to produce and marks the packing shed alt text as drafted. After one approved batch, the checker reports 0 errors and 0 warnings.

Common mistakes

The skill is built around five ways this goes wrong:

  • Lazy-loaded LCP image. It adds load delay to the element that sets LCP. The skill flags it and asks you to confirm the real element in DevTools.
  • A sizes that ignores the CSS. The browser trusts it and fetches the wrong file. The skill derives it from your real widths.
  • Using picture for everything. Plain srcset handles resolution switching. The skill keeps picture for art direction and format switching.
  • Alt text removed to pass a check. Missing alt is a defect and empty alt is a decision. The skill never deletes it.
  • fetchpriority everywhere. web.dev warns that overuse overrides the browser’s own priorities. The skill allows two by default.

Semantic HTML covers the markup around images and HTML Forms covers form controls. Image SEO, LCP Optimisation and CLS Optimisation cover search and Core Web Vitals. Browse the HTML and Semantics set or the full Frontend Development category.

Frequently asked questions

No. Lazy-load images below the fold and leave the largest above-the-fold image eager. web.dev says never to lazy-load the LCP image, because it adds resource load delay. This skill gives every image a role, flags loading=lazy on a likely LCP image as an error, and asks you to confirm the real LCP element in DevTools.

It should describe the width the image takes in your layout, not the viewport. Write one condition per layout change and finish with a default. This skill’s calculator takes your real CSS widths, such as 400px from 1024px and 50vw from 640px, and prints the sizes string with a matching srcset width ladder capped at your widest original.

Use w descriptors with sizes when the image scales with the layout. Use x descriptors, with no sizes, when it always shows at one CSS size, such as a logo. Do not mix the two in one srcset. The checker flags mixed descriptors, and sizes with no w descriptors, as errors.

Not for resolution switching, where an img with srcset and sizes is enough. Use picture for art direction, which means a different crop per viewport, or for format switching with a type on each source and an img fallback last. The checker flags a picture with no img and sources with neither type nor media.

An empty alt marks an image as decorative so assistive technology skips it. A missing alt gives it no instruction, and web.dev notes the two are not the same. The W3C alt decision tree sets which images get which. This skill never removes alt to pass a check and marks agent-written alt text as drafted and needing review.

No. web.dev says to use it on the likely LCP image, and that setting it on more than one or two images reduces its value. This skill allows at most two by default, labelled as a default, warns when a page has more, and tells you to confirm the LCP element before adding any.

The description is about 108 tokens and loads in every session. The SKILL.md body is about 5,400 tokens when the skill fires. A typical run is about 7,000 tokens including one reference file and the checker output, and the cheapest path is about 6,000. All figures are estimates from the skill’s own count, not a model tokeniser.

Changelog

  • 1.0.0, 24 October 2026: first release.

Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.