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.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 24 October 2026. Updated 24 October 2026. Free to use under CC BY 4.0.
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
- 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.
- 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.
- Batches stay small. At most 2 files or 30
imgchanges per batch (defaults), each shown as a diff. - 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.
- Never invent alt text for an image it cannot see. Ask the user (step 7).
- Nothing is invented. Thresholds are labelled defaults. Source facts carry a date. Version-dependent facts carry verify before relying on this.
- Not legal advice. Alt text affects accessibility law compliance. The agent marks up images correctly and routes any conformance statement to a person.
- Refuses to: upscale an image, add
loading="lazy"to a likely LCP image, putfetchpriority="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.
Raw SKILL.md file
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.
| 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
- Use the element the user named in input 5. Otherwise use the largest above-the-fold image and mark it assumed.
- It gets no
loading="lazy". web.dev: "Never lazy-load your LCP image." - It gets
fetchpriority="high". Across the page, at most 2 images carry it (default, from web.dev's "one or two" guidance). - It must be findable in the initial HTML through
srcorsrcset. If the HTML has noimgfor it because CSS or script inserts it, either move it into the HTML or add the preload inreferences/decisions.md(read it only for this case). - 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
- Pick the unit. A picture that scales with the layout gets
wdescriptors plussizes. A picture shown at one fixed CSS size, such as a logo, getsxdescriptors and nosizes. Never mix the two. - 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- Copy its
sizesline andsrcset widthsline.--maxis the widest original from input 4. The calculator never plans a width above it, because upscaling adds bytes and no detail. - Keep the ladder at 6 candidates or fewer (default). Shopify Dev suggests about 4 to 6, because extra widths hurt cache reuse.
- If a needed width has no file, list it in the plan as a file for the user to produce. Do not point
srcsetat a file that does not exist. sizes="auto"is allowed only on images withloading="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 explicitsizes.- A
sizesthat 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
pictureis for two jobs only: art direction (a different crop per viewport, withsource media) and format switching (source type, best format first). Resolution switching alone usesimgwithsrcset.- Each
sourcehastypeormedia. Without either it always matches and hides the sources below it. - The
pictureends with animgthat carriessrc,alt,width,heightandloading. Putfetchpriorityon thatimg. - Do not preload one image in several formats (web.dev warns of extra downloads).
Step 7: Alt text
- Classify each image with the W3C table in
references/decisions.md(read it only if the checker flaggedno-alt,junk-altorempty-link-image, or the user wants alt text written). alt=""marks decoration. A missingaltis a defect. Never remove alt to silence the checker.- An image that is the only content of a link needs alt that names the destination.
- 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.
- Do not start alt with "image of" or "picture of", and do not repeat the file name.
- 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
- Turn the checker output and the decisions into a change list: file and line, current markup, new markup, rule (step number).
- Search for dependants (commands below). Any class or attribute a test or script reads is left alone.
- 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
- Re-run
img_check.py. Pass: 0 errors on the changed files and no new warnings. - 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.
- 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.
- 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.
- Alt spot check. A screen reader or the accessibility tree announces three sampled images with the intended text, and decorative images are skipped.
- 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 warningsSteps 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, 800wThe 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">Fixed-size logo
<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
| # | 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.
- Gate 1 and gate 2 approvals are recorded and the work is on a named branch.
- The baseline and final checker output are in the report, and the final run shows 0 errors on changed files.
- Every
imghas a role from step 2. - The LCP element is named and marked confirmed or assumed (step 3).
- No
sizeswas written without real CSS widths, and each came from the calculator (step 4). - Every file named in a
srcsetexists, or is listed as a file for the user to produce. - No image URL, file name or class read by a dependant changed without the user's yes.
- Step 9.2 was run on the LCP image and at least two other images, with the numbers in the report.
- Alt text written by the agent is marked "drafted, needs review", and unknown images are listed with a question.
- 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
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,
srcsetandsizesrules,picturerules and a verification pass. - Two standard-library scripts:
scripts/img_check.pyandscripts/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.
| Item | Tokens | Status |
|---|---|---|
| Description (always loaded) | 108 | Estimate |
| SKILL.md body (when the skill fires) | 5,439 | Estimate |
references/decisions.md, read when alt text needs classifying, CSS hides the LCP image or CSS crops an image | 983 | Estimate |
references/appendix.md, read only to check a claim | 798 | Estimate |
| Typical run (description, body, one reference, checker output) | 6,959 | Estimate |
| Cheaper path (checker, fix flagged lines, no references) | 5,976 | Estimate |
| Worst case (both references, 60 lines of checker output) | 9,028 | Estimate |
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
sizesthat ignores the CSS. The browser trusts it and fetches the wrong file. The skill derives it from your real widths. - Using
picturefor everything. Plainsrcsethandles resolution switching. The skill keepspicturefor 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.
fetchpriorityeverywhere. web.dev warns that overuse overrides the browser’s own priorities. The skill allows two by default.
Related skills
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
Should I lazy-load every image?
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.
What should the sizes attribute be?
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.
Should I use w or x descriptors in srcset?
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.
Do I need the picture element for responsive images?
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.
What is the difference between alt empty and alt missing?
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.
Should I put fetchpriority high on every hero image?
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.
How many tokens does the Responsive Images skill use?
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.
All HTML and Semantics skills
