Layout & Sizes
Everything before img() manipulates pixels; img() is the gate into markup. It
resolves presentation intent — alt, layout, sizes, priority, placeholder
— into an image element you can print or inspect:
$el = $factory->image('hero.jpg', new ImageMetadata(4000, 2500))
->cover(800, 500)
->img(alt: 'Sunset over the ridge', layout: 'constrained');
echo $el;
The value after $alt is always passed by name. layout and sizes are the two
knobs that shape a responsive srcset; the rest are covered in
The Image Element and Placeholders.
The layout knob
Enum\Layout has three cases (the Gatsby / unpic / Astro vocabulary). Passing the
string form ('constrained', 'fixed', 'full-width') is equivalent. A layout
derives two things from the resolved width W of the chain: which srcset
set to build, and the inferred sizes string.
| Layout | srcset (when the chain has none) | inferred sizes |
|---|---|---|
constrained |
WidthSet of {W, 2W} plus every screen below 2W |
(min-width: {W}px) {W}px, 100vw |
fixed |
DensitySet at 1x, 2x |
— (density srcsets take no sizes) |
full-width |
WidthSet of every screen and its 2× |
100vw |
// A constrained 800px image against the default screens:
$factory->image('hero.jpg', new ImageMetadata(4000, 2500))
->img(alt: 'Sunset', layout: 'constrained');
// srcset: 1600w, 1536w, 1280w, 1024w, 800w, 768w, 640w
// sizes: (min-width: 800px) 800px, 100vw
fixed is the density mode — the knob picks whether widths() or densities()
is built, so you no longer need to know they are mutually exclusive.
Layout must match an existing set
If the chain already carries a set, the layout only contributes the sizes
string, and its family must agree: constrained / full-width go with a
WidthSet, fixed with a DensitySet. A mismatch throws LogicException
naming both sides (->densities(1, 2) with layout: 'constrained', or
->widths(…) with layout: 'fixed').
Unknown width
constrained and fixed need W. When the chain resolves to an unknown width —
no metadata and no explicit size, or a trim() in the chain — they throw a
LogicException pointing at the two fixes: wire a MetadataResolver, or put an
explicit size in the chain. full-width never needs W; without metadata its
candidates are simply unclamped (on local providers the pixel budget is the
operational backstop).
The sizes declaration
sizes accepts three forms. An explicit sizes always wins over a layout’s
inferred one.
1. A raw string, passed through verbatim:
->img(alt: 'Photo', sizes: '(min-width: 768px) 400px, 100vw')
2. A breakpoint-keyed map (Nuxt syntax), resolved against the
screens:
->img(alt: 'Photo', sizes: ['default' => '100vw', 'md' => '50vw', 'lg' => '600px'])
// sizes: (min-width: 1024px) 600px, (min-width: 768px) 50vw, 100vw
A missing default entry is implicitly 100vw — the resolved string always ends
with an unconditional default (a trailing media condition would be invalid HTML).
3. The Nuxt shorthand string — a string with no parenthesis whose
whitespace-separated tokens include at least one key:value:
->img(alt: 'Photo', sizes: '100vw md:50vw lg:600px') // same as the map above
A map derives srcset candidates too
One sizes map derives both the media-query string and the srcset width
candidates, so layout is not needed alongside it. Derivation is range-based, not
single-point: a px entry contributes its literal width; a vw entry contributes
vw% × every screen width in its range — from its own breakpoint up to (not
including) the next larger key, the largest key extending to the top of the scale,
and default covering every screen below the smallest key. All candidates are
unioned with their 2×, deduped, sorted, and clamped to the source width.
Values that are neither px nor vw (calc(…), em, auto) pass into the
sizes string verbatim and contribute no width candidate.
Precedence
- srcset candidates: an explicit set on the chain (
->widths(…)/
->densities(…)) wins; else asizesmap derives one; else the layout does.
Asizesmap alongside an explicit set contributes only the string. sizesstring: an explicit string wins; else a map; else layout inference;
else aWidthSetwith no policy still emits100vw(browsers assume it anyway
— Chainsaw makes it visible).
A DensitySet takes no sizes: any explicit sizes, or a layout: 'fixed'
alongside a sizes map, throws.
Screens configuration
One breakpoint vocabulary drives sizes map resolution, layout width generation,
and art-direction media queries. ImageFactory accepts a screens map, defaulting
to Tailwind’s scale:
$factory = new ImageFactory(
provider: $provider,
screens: ['sm' => 640, 'md' => 768, 'lg' => 1024, 'xl' => 1280, '2xl' => 1536],
);
Screens change which variants are requested, never the bytes of a given variant,
so the cache is unaffected. A breakpoint name resolves to a min-width query
('md' → (min-width: 768px)); the max- prefix flips it to a max-width query
one pixel below ('max-md' → (max-width: 767.98px)), for art direction.
CLS prerequisites
Width and height attributes are the layout-shift mechanism — modern browsers derive
aspect-ratio from them. They are emitted whenever the resolved dimensions are
known, in every layout. Two things are required for that protection:
- A wired
MetadataResolver(or explicitImageMetadata), so the source
dimensions are known. See Getting Started. - The standard CSS reset —
img { max-width: 100%; height: auto }. Tailwind’s
preflight, modern-normalize, and Bootstrap’s reboot all ship it. Without the
height: autoreset, theheightattribute’s presentational hint wins and
aspect-rationever engages.
If you cannot rely on the site stylesheet (newsletters, syndicated fragments), opt
into a self-contained inline style — see Placeholders.