Five Things Every CMS Image Field Needs

· 6 min read

Most CMS image fields do exactly one job.

They take a file, then hand you a single box to type an alt attribute into. That’s fine for one photo on one page, but it falls apart the moment the same photo gets reused as a hero here and a thumbnail there, because a single field can’t hold two different jobs at once.

Three real media libraries make the pattern easy to spot.

WordPress's Attachment Details panel, showing separate Title, Caption, and Alt Text fields that are all saved once per file. WordPress: three separate text fields, one saved value each, no per-placement override.

A Contentful-style asset editor, showing a single Description field used as both caption and alt text for every entry that links the file. Contentful: one Description field doing alt text’s job, same everywhere the asset appears.

A Sanity Studio-style image field, showing a hotspot circle on the thumbnail next to a single, still-empty Alt Text box below it. Sanity: a hotspot for cropping, but still just one alt text box underneath it.

The model might look like this,

{
  "asset": "photo-042.jpg",
  "alt": "Team standup, April 2026",
  "description": "Company X development team. Sign below says 'April 2026'"
}

There is nothing wrong with the JSON model but most usages these are not enough.

Here’s what I’d want added into that field every time an image is uploaded.

Additional metadata that makes an image an “asset”

  1. Alt text that travels with the placement
  2. A field that asks what the image is for, before what to call it
  3. Focal points, not fixed crops
  4. Responsive variants and modern formats, generated once
  5. Lazy loading and reserved space, by default

Alt text that travels with the placement

Most media libraries store exactly one alt text per asset, saved once when the file goes in.

That is wrong, because the same image plays a different role every time it gets reused. A product photo can be informative on a listing page and purely decorative as a background texture on a landing page three templates over.

The W3C’s guidance on text alternatives asks for an alternative that serves an equivalent purpose, and purpose is a property of the placement, not the file. The practical fix is simple: store a sensible default on the asset, then let each placement override it.

flowchart TD
    A[Image asset] -->|default alt| B["Team standup, April 2026"]
    A --> C[Used as: blog post hero]
    A --> D[Used as: about-page thumbnail]
    C -->|override: decorative| E["alt is empty"]
    D -->|override: informative| F["Five of us at the whiteboard"]

One asset, one default, two placements that override it — because the image’s job changes, not the file.

That turns alt text from a property of a file into a something that describes the relationship between the image and everything around it.

A field that asks what the image is for, before what to call it

Mentioned earlier most apps, shows an input for alt text and nothing else. Nothing tells the editor what to describe, so most either skip it or type “image of a person smiling,” which helps no one.

Lets change it: ask the role first. Decorative? Functional? or Informative? The W3C decision tree walks through exactly that triage. In most CMS, you won’t be asKed for “Functional”. In those cases, the UI is as simple as adding a checkbox for “decorative?” default to unchecked.

flowchart TD
    A[Upload image] --> B{What is this image for?}
    B -->|Decorative| C[Lock alt to empty, show why]
    B -->|Functional| D[Ask: what action does it trigger?]
    B -->|Informative| E[Ask: what one fact does it add?]

The role picked up top decides what the field asks next. No more blank box, no more guessing.

  • Pick decorative and the field locks to an empty alt with a short note explaining why.
  • Pick functional and it asks for the action the image triggers, not its appearance.
  • Pick informative and it asks for the one fact the image adds that the surrounding text doesn’t already say.

The upload form becomes a two-second decision instead of a blank input that quietly punishes whoever fills it in last.

Focal points, not fixed crops

A photo gets uploaded, and can be used everywhere in the application - a square avatar, a wide hero, a tall card on mobile.

A single manual crop only helps the ratio it was cut for, so the next template either squeezes the subject out of frame or drags a designer back in with scissors.

A “focal point” fixes this at the source. Sanity’s image pipeline stores a hotspot on the image once, then its URL builder crops every requested ratio around that point automatically, with no per-template manual work required (Storyblok ships the same pattern natively; Contentful gets there via a marketplace app, though you still write the crop logic yourself).

flowchart TD
    A["Photo + hotspot (x, y)"] --> B[Square avatar]
    A --> C[Wide hero, 16:9]
    A --> D[Tall mobile card]

Same file, same hotspot, three crops that all keep the subject in frame.

It’s a small piece of metadata, but it’s the difference between an image library that scales with new templates and one that generates a support ticket every time somebody adds a layout.

Responsive variants and modern formats, generated once

Every camera exports huge files by default, and most editors upload exactly what the camera gave them.

(An aside I’ll allow myself: half of every media library I’ve ever opened is forty-megapixel originals nobody resized. That’s a rant about photographer habits, not a CMS problem, so I’ll leave it alone.)

The 2024 Web Almanac’s media chapter found that half of pages ship their largest image at a size the Almanac’s own analysis calls likely bigger than a mobile screen can display.

The CMS’s job is to not care. One upload should produce a set of width variants and hand the browser a srcset and sizes pair so it can pick the right one, which is what MDN’s responsive images guide describes.

Format selection can work the same way. An image CDN can inspect the browser’s Accept header and serve AVIF or WebP instead of the original JPEG, using whatever the browser will actually take.

flowchart TD
    A[One upload, full resolution] --> B["Generate width variants (400w / 800w / 1200w / 1600w)"]
    B --> C{Browser Accept header}
    C -->|AVIF supported| D[Serve AVIF]
    C -->|WebP supported| E[Serve WebP]
    C -->|Fallback| F[Serve JPEG]

One upload, one pipeline. The browser negotiates its own format from there.

None of that is a UI we need to build. It’s plumbing, and it belongs in the backend and infrastructure of the upload button, not something a user needs to select.

Lazy loading and reserved space, by default

An <img> tag with no loading attribute and no dimensions does two things wrong at once.

  • It competes with everything else on the page for bandwidth the instant the page loads, and;
  • It leaves no space reserved for itself, so the layout jumps the moment it finally arrives.

Native lazy loading fixes the first problem. Setting loading="lazy" tells the browser to defer offscreen images until they’re actually needed, a single attribute and no library required.

The standard setup has one exception. Whatever loads first (above the fold), often the hero, should stay eager, sometimes with fetchpriority="high", because lazy-loading the Largest Contentful Paint candidate only delays the page’s most important image.

Declaring width and height, or a CSS aspect-ratio, fixes the second problem, because the browser can reserve the right box before a single byte of the image arrives.


<img src="chilren-playing.png" width="500" height="350" loading="lazy" alt="Kids playing tag on the street" />
flowchart TD
    A[img rendered by template] --> B{Above the fold?}
    B -->|Yes, LCP candidate| C["loading=eager, fetchpriority=high"]
    B -->|No| D[loading=lazy]
    C --> E[width/height or aspect-ratio reserved]
    D --> E

One branch for the hero, one for everything else. Both still reserve their space.

Both belong in whatever template renders the image, generated automatically from data the CMS already has, rather than left for an editor to remember on image two hundred.

What a boring image field buys you

None of these five properties take much code once a platform already has the underlying primitive, a hotspot API, an image CDN, a role-aware form. The harder version is building that primitive yourself, from a blank content model.

The checklist

  • A default alt with a per-placement override
  • a role-first prompt
  • a focal point
  • generated variants in modern formats
  • lazy loading with reserved space.

The image metadata we want

{
  "asset": "photo-042.jpg",
  "altDefault": "Team standup, April 2026",
  "hotspot": { "x": 0.52, "y": 0.38 },
  "variants": ["400w", "800w", "1200w", "1600w"],
  "formats": ["avif", "webp", "jpeg"],
  "loading": "lazy",
  "placements": {
    "blog-hero": { "role": "decorative", "alt": "" },
    "about-page-thumbnail": { "role": "informative", "alt": "Five of us at the whiteboard" }
  }
}

That’s the whole field: five ideas from this post, one small schema. Nothing in it is exotic — it’s just more than a filename and a text box.

What they buy you is an image pipeline that doesn’t depend on every editor remembering every rule on every upload, because the rules live in the field instead of in someone’s memory.

Build it once, and every image after that inherits it for free.

Happy building!

By @codespud

DISCLAIMER This is my personal weblog and learning tool. The content within it is exactly that – personal. The views and opinions expressed on the posts and the comments I make on this Blog represent my own and not those of people, institutions or organisations I am affiliated with unless stated explicitly. My Blog is not affiliated with, neither does it represent the views, position or attitudes of my employer, their clients, or any of their affiliated companies.

© 2006 - 2026, Copyright - codespud.com · RSS