The default is no photograph. Product UI earns its texture from type, spacing, and material, and every picture you add is an aspect ratio to lock, a placeholder to design, two themes to survive, and bytes on the critical path. A picture belongs only when the picture is the content: something a user uploaded, a product being sold, a chart of their data, or a screenshot of the thing you actually built. Benji Taylor's site is the reference case — 53 device frames showing real product work, and zero stock photographs, no 3D render, no hero image, not even a photo of himself. When a picture does belong, it ships with its ratio locked before a single byte arrives.
The sibling to keep straight is iconography. An icon is a glyph drawn to a stroke grid and sized against the type it sits beside; a picture is a raster or a scene with its own intrinsic ratio and load cost. If the asset has a stroke weight, it is iconography. Two more edges: how long an image may take before its placeholder becomes a loading state is ui-states, and the dimming ratio a photograph takes in the dark theme is dark-mode.
Work inside whatever image pipeline already exists. Look for next/image, Astro's <Image>, @nuxt/image, an image CDN (Cloudinary, imgix, Vercel's optimizer), or a plain <img> convention. Hand-writing srcset beside a framework component that generates it produces two competing sources of truth and one of them will rot. If the project has no pipeline, say so explicitly before adding one — that is a dependency decision, not an image decision.
Quick Reference
You need
Open
Format choice, srcset and sizes, lazy versus priority, SVG hygiene, the placeholder ladder
references/formats-and-loading.md — open it before writing any <img> or <picture> markup.
references/avatars.md — open it the moment a person, team, or account is represented by a picture.
Core Principles
Lock the ratio before the bytes arrive. Set aspect-ratio on the box andwidth/height attributes on the <img>, even when CSS resizes it — the attributes are what let the browser reserve the space during parse. An image without reserved space is a layout shift with a delay fuse. The exception is genuinely unknown intrinsic dimensions at render time, which get a fixed-height container plus object-fit: cover.
One ratio per collection. A grid of 16:9, 4:3, and 3:2 cards reads as broken even though every cell is individually correct, because the eye tracks the gaps between cards, not the cards. Pick one ratio for the surface and crop into it. The exception is masonry, which is explicitly a mixed-ratio format and must therefore be visibly, deliberately masonry.
Choose the placeholder by role, not by duration. Hero and above-the-fold images get a blur-up inlined as a data: URI so it travels in the document itself; grid thumbnails get a single dominant colour; avatars get initials and never a spinner; decorative images get nothing at all. A spinner on an image is always wrong — it promises indeterminate work where the shape is already known. The exception is a batch arriving together, such as a paged-in gallery, which takes one skeleton for the group.
Crop by art direction, not by CSS luck.object-fit: cover on a hero will centre-crop a face out of the frame at narrow widths and nobody will notice until a customer does. Set object-position at minimum; use <picture> with a genuinely different crop when the subject leaves the frame. The exception is user-uploaded content, whose subject you cannot know — square-crop from centre and give the user a way to reposition it.
Every image carries a 1px inset outline: black at 10% in light, white at 10% in dark. Photographs have their own edges, and without a boundary a light-topped image bleeds into a light page. Draw it with outline: 1px solid rgb(0 0 0 / 0.1) and outline-offset: -1px so it costs no layout space. Never a palette neutral (slate, zinc, #111827) and never the accent — a tinted line picks up the surface behind the image and reads as grime along the edge. The exception is a cut-out asset on transparency, where the outline would trace the bounding box instead of the shape.
Screenshots get a second capture, not a filter. Running a UI screenshot through a dark-mode brightness filter produces inverted-looking chrome and grey-on-grey text, and it announces that nobody looked. Capture the app in its dark theme and swap sources with <picture> and prefers-color-scheme. The exception is photographs, which have no dark variant to capture and take the dimming treatment dark-mode defines.
Illustration obeys the icon system's physics. Match illustration stroke weight to the icon set — 2px illustration strokes beside 1px icons read as two different products. Hold one scene to roughly three colours, all from the UI's tokens; recolour any purchased or community pack before it ships. Keep light direction consistent with the UI's shadow direction. Neither icons nor illustrations scale up: a 16px mark blown to 48px keeps its now-spindly stroke and must be redrawn. The exception is a licensed brand asset you are not permitted to alter.
Show product work running, framed, and slowable. A still of an interface proves layout; it proves nothing about behaviour, which is where the work actually is. Put the recording inside a device frame so the viewer knows what they are looking at, and ship a 1x / 0.5x speed toggle — Benji Taylor puts one on every demo on his site, which turns a portfolio piece into something a reader can actually inspect. The exception is when layout genuinely is the subject; then a still is honest.
Smell / Fix
Smell
Fix
Page jumps as images load
No reserved space — add aspect-ratio plus width/height attributes
Card grid feels ragged though each card is fine
Mixed ratios — crop the collection to one
Spinner sitting in an image box
Blur-up, dominant colour, or nothing — never a spinner
Faces cropped out at mobile widths
object-position, or a second crop via <picture>
Image edges look dirty or smudged
Tinted outline — pure black/white at 10% only
Dark theme screenshot looks washed out
It is a filtered light capture; take a real dark capture
Stock photo of people at laptops
Delete it. Nothing replaces it
Illustration looks bolted on
Recolour to tokens, match stroke weight and light direction
Hero image is the LCP and lazy-loaded
Remove loading="lazy", add fetchpriority="high"
Output Format
Every image ships this contract — no element leaves with a field missing:
Decorative images take alt="" — empty, never missing, and never a filename. Below the fold, drop fetchpriority and add loading="lazy".
Checklist
Every image has reserved space before load (aspect-ratio + width/height)
One aspect ratio per collection, or the layout is deliberately masonry
Placeholder chosen by role; no spinners in image boxes
Crops verified at the narrowest supported width, not just at desktop
1px inset outline, black/white at 10%, on every non-cut-out image
Dark theme has real dark captures for screenshots, not filtered light ones
Illustrations match icon stroke weight, token colours, and light direction
LCP image is not lazy-loaded and carries fetchpriority="high"
Every image has an alt attribute, empty if decorative
No stock photography, no generic 3D render, no blob-and-character illustration
1---2name: imagery3description: Use when handling photos and illustration: aspect ratios, cropping, placeholders and blur-up, avatars, device frames, and dark-mode treatment.4---567# Pictures in the Interface89The default is no photograph. Product UI earns its texture from type, spacing, and material, and every picture you add is an aspect ratio to lock, a placeholder to design, two themes to survive, and bytes on the critical path. A picture belongs only when the picture *is* the content: something a user uploaded, a product being sold, a chart of their data, or a screenshot of the thing you actually built. Benji Taylor's site is the reference case — 53 device frames showing real product work, and zero stock photographs, no 3D render, no hero image, not even a photo of himself. When a picture does belong, it ships with its ratio locked before a single byte arrives.1011The sibling to keep straight is `iconography`. An icon is a glyph drawn to a stroke grid and sized against the type it sits beside; a picture is a raster or a scene with its own intrinsic ratio and load cost. If the asset has a stroke weight, it is `iconography`. Two more edges: how long an image may take before its placeholder becomes a loading state is `ui-states`, and the dimming ratio a photograph takes in the dark theme is `dark-mode`.1213**Work inside whatever image pipeline already exists.** Look for `next/image`, Astro's `<Image>`, `@nuxt/image`, an image CDN (Cloudinary, imgix, Vercel's optimizer), or a plain `<img>` convention. Hand-writing `srcset` beside a framework component that generates it produces two competing sources of truth and one of them will rot. If the project has no pipeline, say so explicitly before adding one — that is a dependency decision, not an image decision.1415## Quick Reference1617| You need | Open |18|---|---|19| Format choice, `srcset` and `sizes`, lazy versus priority, SVG hygiene, the placeholder ladder | `references/formats-and-loading.md` — open it before writing any `<img>` or `<picture>` markup. |20| Sizes, shapes, fallback chain, stacked groups, presence dots | `references/avatars.md` — open it the moment a person, team, or account is represented by a picture. |2122## Core Principles23241. **Lock the ratio before the bytes arrive.** Set `aspect-ratio` on the box *and* `width`/`height` attributes on the `<img>`, even when CSS resizes it — the attributes are what let the browser reserve the space during parse. An image without reserved space is a layout shift with a delay fuse. The exception is genuinely unknown intrinsic dimensions at render time, which get a fixed-height container plus `object-fit: cover`.25262. **One ratio per collection.** A grid of `16:9`, `4:3`, and `3:2` cards reads as broken even though every cell is individually correct, because the eye tracks the gaps between cards, not the cards. Pick one ratio for the surface and crop into it. The exception is masonry, which is explicitly a mixed-ratio format and must therefore be visibly, deliberately masonry.27283. **Choose the placeholder by role, not by duration.** Hero and above-the-fold images get a blur-up inlined as a `data:` URI so it travels in the document itself; grid thumbnails get a single dominant colour; avatars get initials and never a spinner; decorative images get nothing at all. A spinner on an image is always wrong — it promises indeterminate work where the shape is already known. The exception is a batch arriving together, such as a paged-in gallery, which takes one skeleton for the group.29304. **Crop by art direction, not by CSS luck.** `object-fit: cover` on a hero will centre-crop a face out of the frame at narrow widths and nobody will notice until a customer does. Set `object-position` at minimum; use `<picture>` with a genuinely different crop when the subject leaves the frame. The exception is user-uploaded content, whose subject you cannot know — square-crop from centre and give the user a way to reposition it.31325. **Every image carries a `1px` inset outline: black at 10% in light, white at 10% in dark.** Photographs have their own edges, and without a boundary a light-topped image bleeds into a light page. Draw it with `outline: 1px solid rgb(0 0 0 / 0.1)` and `outline-offset: -1px` so it costs no layout space. Never a palette neutral (`slate`, `zinc`, `#111827`) and never the accent — a tinted line picks up the surface behind the image and reads as grime along the edge. The exception is a cut-out asset on transparency, where the outline would trace the bounding box instead of the shape.33346. **Screenshots get a second capture, not a filter.** Running a UI screenshot through a dark-mode brightness filter produces inverted-looking chrome and grey-on-grey text, and it announces that nobody looked. Capture the app in its dark theme and swap sources with `<picture>` and `prefers-color-scheme`. The exception is photographs, which have no dark variant to capture and take the dimming treatment `dark-mode` defines.35367. **Illustration obeys the icon system's physics.** Match illustration stroke weight to the icon set — `2px` illustration strokes beside `1px` icons read as two different products. Hold one scene to roughly three colours, all from the UI's tokens; recolour any purchased or community pack before it ships. Keep light direction consistent with the UI's shadow direction. Neither icons nor illustrations scale up: a `16px` mark blown to `48px` keeps its now-spindly stroke and must be redrawn. The exception is a licensed brand asset you are not permitted to alter.37388. **Show product work running, framed, and slowable.** A still of an interface proves layout; it proves nothing about behaviour, which is where the work actually is. Put the recording inside a device frame so the viewer knows what they are looking at, and ship a `1x / 0.5x` speed toggle — Benji Taylor puts one on every demo on his site, which turns a portfolio piece into something a reader can actually inspect. The exception is when layout genuinely is the subject; then a still is honest.3940## Smell / Fix4142| Smell | Fix |43|---|---|44| Page jumps as images load | No reserved space — add `aspect-ratio` plus `width`/`height` attributes |45| Card grid feels ragged though each card is fine | Mixed ratios — crop the collection to one |46| Spinner sitting in an image box | Blur-up, dominant colour, or nothing — never a spinner |47| Faces cropped out at mobile widths | `object-position`, or a second crop via `<picture>` |48| Image edges look dirty or smudged | Tinted outline — pure black/white at 10% only |49| Dark theme screenshot looks washed out | It is a filtered light capture; take a real dark capture |50| Stock photo of people at laptops | Delete it. Nothing replaces it |51| Illustration looks bolted on | Recolour to tokens, match stroke weight and light direction |52| Hero image is the LCP and lazy-loaded | Remove `loading="lazy"`, add `fetchpriority="high"` |5354## Output Format5556Every image ships this contract — no element leaves with a field missing:5758```html59<img60 src="/hero-800.avif"61 srcset="/hero-800.avif 800w, /hero-1600.avif 1600w"62 sizes="(min-width: 768px) 50vw, 100vw"63 width="1600" height="900"64 alt="Order confirmation screen with the receipt expanded"65 decoding="async"66 fetchpriority="high"67 class="-outline-offset-1 outline-1 outline-black/10 dark:outline-white/10"68/>69```7071Decorative images take `alt=""` — empty, never missing, and never a filename. Below the fold, drop `fetchpriority` and add `loading="lazy"`.7273## Checklist7475- [ ] Every image has reserved space before load (`aspect-ratio` + `width`/`height`)76- [ ] One aspect ratio per collection, or the layout is deliberately masonry77- [ ] Placeholder chosen by role; no spinners in image boxes78- [ ] Crops verified at the narrowest supported width, not just at desktop79- [ ] `1px` inset outline, black/white at 10%, on every non-cut-out image80- [ ] Dark theme has real dark captures for screenshots, not filtered light ones81- [ ] Illustrations match icon stroke weight, token colours, and light direction82- [ ] LCP image is not lazy-loaded and carries `fetchpriority="high"`83- [ ] Every image has an `alt` attribute, empty if decorative84- [ ] No stock photography, no generic 3D render, no blob-and-character illustration
Run npx skillmds@latest add agentsorg/imagery in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Use when handling photos and illustration: aspect ratios, cropping, placeholders and blur-up, avatars, device frames, and dark-mode treatment. It is listed under Design & Media on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
agentsorg (@agentsorg) published this skill. Their other Agent Skills are listed on their SkillMD profile.