The Image Element

img() returns an Output\ImgElement: the resolved <img> as a value object. It
carries the visual source (Image or a Srcset set) plus the resolved
presentation data (alt, sizes, loading policy, placeholder, extra attributes).
Construction is cheap — URLs materialize lazily, only when you print or read an
accessor, so a variant is never generated until it is actually needed.

$el = $factory->image('hero.jpg', new ImageMetadata(4000, 2500))
    ->cover(800, 500)
    ->widths(400, 800, 1200)
    ->img(alt: 'Sunset', sizes: ['md' => '50vw']);

Elements implement MarkupInterface (so they stringify to HTML) and
JsonSerializable.

Printing

echo $el;                                  // <img …>
echo $el->render(['class' => 'hero']);     // last-minute attributes merged on top

render() merges its argument over the element’s own attributes with the same
rules described below, then dispatches through the
renderer carried on the image.

alt is required

img() has no default alt — you pass one at every call (Astro-style
enforcement). The tristate:

  • alt: 'A field at dusk'alt="A field at dusk".
  • alt: '' → explicitly decorative: alt="" and role="presentation".
  • The zero-config Image::render() / Srcset::render() shortcut passes no alt
    and emits none — absence is honest and visible to linters, rather than a silent
    empty alt.

The escape hatch

The element exposes the same resolved data the renderer sees, so you can build
markup by hand, hydrate a JS front end, or emit a preload Link header. This is
the getImageProps() equivalent.

$el->src();       // string — the fallback URL
$el->srcset();    // ?string — the candidate list, or null for a plain image
$el->sizes();     // ?string — null for a density srcset
$el->width();     // ?int    — the reserved box width, null when unknown
$el->height();    // ?int
$el->style();     // ?string — the inline-style recipe, if any (no placeholder)

$el->attributes(); // array<string, string> — the final, ordered, materialized map
                   // (the placeholder delta is resolved and merged here)

attributes() is the whole element as an ordered, null-dropped map: src,
srcset, sizes, width, height, alt, loading, decoding,
fetchpriority, style, then your extra attributes.

JSON hydration

echo json_encode($el);
// {"src":"…","srcset":"…","sizes":"…","width":800,"height":500,"alt":"Sunset",…}

jsonSerialize() returns attributes(), ready for a JS image component,
image-set() CSS, or a headless API.

Extra attributes

The attrs argument is the entry to the element’s extra-attributes bag — class,
id, data-*, anything:

$image->img(alt: 'Hero', attrs: ['class' => 'rounded', 'data-gallery' => 'trip']);

Attribute names are validated (/^[a-zA-Z][a-zA-Z0-9-]*$/) and values are escaped
with htmlspecialchars(ENT_QUOTES | ENT_SUBSTITUTE). A Stringable value (an
Image → its URL, a Srcset → its candidate list) is cast, which is what enables
the data-src* lazy-load patterns below.

null drops an attribute

A null value in attrs (or in render()'s argument) drops the computed
attribute — the lever for JS lazy-loading:

// LQIP in src, real srcset deferred to a JS hydrator:
$image->widths(400, 800)->img(
    alt: 'Hero',
    attrs: [
        'srcset'      => null,                        // drop the computed srcset
        'data-srcset' => $image->widths(400, 800),    // Stringable → candidate list
        'loading'     => null,                        // suppress the lazy default
    ],
);

When you need different markup

For a structural change — a <figure> wrapper, a web component, a JSON payload, a
different lazy-load convention — supply a custom renderer to the factory instead of
reaching for attributes. The renderer receives the resolved element, so it never
recomputes policy. See Rendering.