Responsive Images skill for Codex
The right bytes for every screen. Codex writes image markup from the real CSS layout: it measures rendered widths, derives a sizes value that matches each breakpoint, builds a sensible width ladder, wraps photos in picture with AVIF and WebP sources, reserves space with width and height, and gives the LCP image high fetch priority instead of lazy loading. It then checks currentSrc at several viewport widths and pixel densities.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- responsive-images Installs to ~/.agents/skills/responsive-images/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install responsive-images The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project responsive-images Installs to .agents/skills/responsive-images/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/responsive-images.zipunzip -o responsive-images.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) responsive-images PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $responsive-images, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes correct responsive image markup - srcset with width descriptors, accurate sizes attributes, picture with AVIF and WebP sources, art direction with media queries, width and height for zero layout shift, fetchpriority for the LCP image and lazy loading below the fold - plus framework components such as next/image, Astro Image and Nuxt Image. Use when images are blurry or oversized on mobile, the LCP element is an image, Lighthouse flags properly size images or next-gen formats, a hero needs a different crop on phones, or someone asks what sizes value to use.
Use it when
- Images download larger than they render, especially on mobile
- Lighthouse flags image sizing, formats or a lazily loaded LCP image
- A banner needs a different crop at small widths
- next/image, Astro Image or Nuxt Image output looks blurry or oversized
Use something else when
- Compression settings, CDN pipelines and bulk conversion: use image-optimization
- Diagnosing LCP, INP and CLS across the whole page: use core-web-vitals
How to check the result
With cache disabled, check the downloaded candidate and currentSrc at 375, 768 and 1440 px for DPR 1 and 2, then confirm Lighthouse reports no image sizing or lazy LCP warnings.
The mistake it prevents
Omitting sizes so the browser assumes 100vw, or lazy-loading the LCP hero image.
Done well means: the browser downloads the smallest file that looks sharp at the rendered size and density, the LCP image starts loading at high priority from the first HTML response, no image shifts layout, and modern formats are served with a safe fallback.
Before changing anything
- Read
AGENTS.md; find how images are produced (static files, build plugin, image CDN like Cloudinary/imgix/Cloudflare, or a framework component). Generate variants with that pipeline, not by hand. - Identify the LCP element for key templates (Lighthouse or DevTools Performance panel).
- Measure actual rendered widths at a few viewport widths (DevTools: hover the
img, note rendered vs intrinsic size).sizesmust come from the CSS layout, not guesses.
Pick the markup
| Situation | Markup |
|---|---|
| Same image, layout width varies with viewport | img + srcset (w descriptors) + sizes |
| Fixed CSS size (avatar, logo, icon) | img + srcset with 1x, 2x density descriptors |
| Modern formats with fallback | picture + source type="image/avif" + type="image/webp" + img |
| Different crop or composition per breakpoint | picture + source media="…" (art direction) |
| Vector artwork, logos | SVG, no srcset |
| Decorative background | CSS image-set(); never for content or LCP if avoidable |
Fluid image
<img
src="/img/team-800.jpg"
srcset="/img/team-400.jpg 400w, /img/team-800.jpg 800w,
/img/team-1200.jpg 1200w, /img/team-1600.jpg 1600w, /img/team-2400.jpg 2400w"
sizes="(min-width: 1280px) 600px, (min-width: 768px) 50vw, calc(100vw - 32px)"
width="1600" height="1067"
alt="Five engineers reviewing a circuit board at a workbench"
loading="lazy" decoding="async">
How to write sizes:
- Describe the image’s rendered CSS width at each breakpoint, in the same order the browser evaluates media conditions: first match wins, so list larger breakpoints first.
- Account for padding and gaps (
calc(100vw - 32px),calc((100vw - 96px) / 3)). - Cap at the container max width (
600pxabove 1280px), or large screens download 2400w. - Without
sizes, the browser assumes100vwand over-downloads for anything narrower. - For lazy images only,
sizes="auto"(with fallback, e.g.sizes="auto, 50vw") lets supporting browsers use the laid-out width. It is ignored for eager images.
Width ladder: roughly every 300–500 px of width from the smallest rendered size to rendered max x 2 (cap at 2x density; 3x adds bytes with little visible gain). Five or six candidates is usually enough.
Formats
<picture>
<source type="image/avif" srcset="/img/hero-800.avif 800w, /img/hero-1600.avif 1600w" sizes="100vw">
<source type="image/webp" srcset="/img/hero-800.webp 800w, /img/hero-1600.webp 1600w" sizes="100vw">
<img src="/img/hero-1600.jpg" srcset="/img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"
sizes="100vw" width="1600" height="900" alt="" fetchpriority="high">
</picture>
| Format | Use for | Notes |
|---|---|---|
| AVIF | Photos, gradients | Smallest at equal quality; slower to encode; start around quality 50–60 |
| WebP | Fallback for AVIF, or default where encoding time matters | Quality ~75 |
| JPEG (progressive) | Final fallback | mozjpeg quality ~75–80 |
| PNG | Only when lossless with transparency is required | Try lossless WebP first |
| SVG | Logos, icons, illustrations | Optimize with SVGO; sanitize user-uploaded SVG |
Order source elements best-first; the browser takes the first type it supports. sizes
must be repeated on every source and the img.
Art direction
<picture>
<source media="(max-width: 767px)" srcset="/img/hero-portrait-600.avif 600w, /img/hero-portrait-1200.avif 1200w" type="image/avif" sizes="100vw" width="1200" height="1500">
<source media="(max-width: 767px)" srcset="/img/hero-portrait-600.jpg 600w, /img/hero-portrait-1200.jpg 1200w" sizes="100vw" width="1200" height="1500">
<img src="/img/hero-wide-1600.jpg" srcset="/img/hero-wide-1600.jpg 1600w, /img/hero-wide-2400.jpg 2400w" sizes="100vw" width="2400" height="1000" alt="Ferry crossing the harbour at dusk">
</picture>
width/height on source lets each crop reserve its own aspect ratio. Use art direction
only when the crop actually changes; for scaling alone, use srcset.
Loading priority
| Image | Attributes |
|---|---|
| LCP image (hero, product shot) | fetchpriority="high", no loading="lazy", in the initial HTML (not injected by JS, not a CSS background) |
| Above the fold, not LCP | default (eager), no fetchpriority |
| Below the fold | loading="lazy" decoding="async" |
| Carousel slides after the first | loading="lazy" |
If the LCP image is discovered late (CSS background, JS-rendered), add
<link rel="preload" as="image" imagesrcset="…" imagesizes="…" fetchpriority="high"> in the
head with the same srcset and sizes; mismatched values download twice.
Framework components
next/image: passsizeswhenever the image is not fixed width (otherwise it generates a 1x/2x set for thewidthprop, or assumes 100vw withfill). Usepriority(orpreload/fetchPriorityin newer versions; check the installed Next.js docs) on the LCP image. Configureimages.remotePatternsfor remote hosts andformats: ['image/avif', 'image/webp'].- Astro
<Image>/<Picture>fromastro:assets: setwidthsandsizes,formats={['avif','webp']}on Picture,loading="eager"+fetchpriority="high"for LCP. - Nuxt Image:
<NuxtImg sizes="sm:100vw md:50vw lg:600px">,<NuxtPicture>for formats. - Image CDNs: generate srcset from URL parameters (
?w=800&fm=avif&q=60), and keep a helper that builds the whole attribute so widths stay consistent.
Gotchas
- Missing
width/height(or CSSaspect-ratio) causes CLS. The attributes set the ratio only; CSSwidth: 100%; height: autostill controls size. loading="lazy"on the LCP image delays it by hundreds of ms; the most common LCP regression.alt=""for decorative images only. Informative images need alt describing the content and purpose (WCAG 1.1.1); do not start with “image of”.- Density descriptors (
2x) and width descriptors (800w) cannot be mixed in one srcset. - Browsers may pick a larger cached candidate; testing with a warm cache misleads. Disable cache.
- Upscaled sources: never generate candidates wider than the original master.
Verification
- DevTools Network (cache disabled), at 375, 768 and 1440 px widths with DPR 1 and 2: the downloaded candidate is the smallest one at least as wide as rendered width x DPR.
- Elements panel:
currentSrcof each image matches expectation. - Lighthouse: no “Properly size images”, “Serve images in next-gen formats”, or “Largest Contentful Paint image was lazily loaded” findings.
- CLS from images is 0 in the Performance panel layout-shift track.
- Format fallback: disable AVIF in a test browser or check in an older Safari; JPEG loads.
Output
For each changed image: markup chosen, candidate widths and formats, the sizes value with the
layout reasoning behind it, loading priority, and before/after transferred bytes at one mobile
and one desktop width.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Responsive Images"
short_description: "The right bytes for every screen"
default_prompt: "$responsive-images Our hero image is the LCP element and loads slowly, fix the markup" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.