Responsive Images With Hugo’s Image Processing
Responsive images in Hugo need two decisions: generate useful source sizes during the build, then describe the rendered image slot accurately in HTML. Hugo prepares the files; the browser chooses a candidate from srcset.
A reusable partial keeps those decisions consistent across templates and Markdown. It also provides one place to avoid enlarging small originals and to distinguish a prominent image from one that can load later.
Start responsive images in Hugo with a page bundle
Keep the original beside the article that owns it:
content/
└── blog/
└── example/
├── index.md
└── photo.jpg
In a page template, capture that page resource :
{{ $image := .Resources.GetMatch "photo.jpg" }}
Files in static/ are copied as static files; this lookup does not turn them into page resources. The original needs to be available through Hugo’s resource system before processing methods can be called.
Check the match before using it. A missing filename should produce a useful diagnostic or a deliberate fallback, rather than an obscure method error.
Choose the operation before choosing the widths
The processing method determines what happens to the composition:
{{ $resized := $image.Resize "600x webp" }}
{{ $thumbnail := $image.Fill "600x400 webp" }}
{{ $contained := $image.Fit "1200x800 webp" }}
Resize
with one dimension preserves proportions. Supplying both dimensions resizes to that exact shape, which can distort the image.
Fill
preserves proportions and crops to the requested box. Fit
keeps the whole image within its bounding box without enlarging a smaller source.
Use cropping for a thumbnail whose shape is intentional. A screenshot or diagram usually benefits from keeping its complete contents. Format conversion can accompany the operation, as the webp option does here.
Generate candidates without enlarging the original
Create layouts/_partials/responsive-image.html:
{{ $image := .image }}
{{ if reflect.IsImageResourceProcessable $image }}
{{ $width := int (math.Min 1200 $image.Width) }}
{{ $fallback := $image.Resize (printf "%dx webp" $width) }}
{{ $sources := slice }}
{{ range slice 400 800 }}
{{ if lt . $width }}
{{ $candidate := $image.Resize (printf "%dx webp" .) }}
{{ $sources = $sources | append (printf "%s %dw" $candidate.RelPermalink $candidate.Width) }}
{{ end }}
{{ end }}
{{ $sources = $sources | append (printf "%s %dw" $fallback.RelPermalink $fallback.Width) }}
<img
src="{{ $fallback.RelPermalink }}"
srcset="{{ delimit $sources ", " }}"
sizes="{{ .sizes | default "100vw" }}"
width="{{ $fallback.Width }}"
height="{{ $fallback.Height }}"
alt="{{ .alt }}"
{{ with .title }}title="{{ . }}"{{ end }}
loading="{{ if .lazy }}lazy{{ else }}eager{{ end }}"
{{ if .priority }}fetchpriority="high"{{ end }}
>
{{ else }}
{{ errorf "responsive-image.html needs a processable image resource" }}
{{ end }}
The largest candidate is capped at the smaller of 1200 pixels and the original width. Smaller candidates are generated only below that cap, so a 300-pixel source stays a single 300-pixel candidate.
The width descriptors use the generated files’ actual dimensions. HTML’s width and height attributes also provide an aspect ratio before loading, as explained in the image element reference .
The reflect.IsImageResourceProcessable check
handles the resource type before the template tries resizing it. The partial expects a processable image; callers decide how to handle anything else.
The partial is for static images. If retaining animation or a particular source format matters, route that asset through a separate policy.
Describe the slot, not the source file
Call the partial from a page template:
{{ with .Resources.GetMatch "photo.jpg" }}
{{ partial "responsive-image.html" (dict
"image" .
"alt" "A tram passing wooden houses in Riga"
"sizes" "(max-width: 700px) 100vw, 700px"
"lazy" false
"priority" true
) }}
{{ end }}
This slot description assumes a full-width image below 700 pixels and a 700-pixel-wide slot above it. Match it to the actual layout, including gutters; it is not a universal value for every article template.
srcset describes the files available. sizes describes their expected display width, and the browser uses that information during selection. Choosing between srcset and picture
covers composition changes and format alternatives separately.
The call above treats the image as a prominent candidate for early loading. For a below-the-fold image, use lazy: true and omit priority instead. A shared partial should not silently lazy-load the page’s main image.
Connect ordinary Markdown to the same partial
Create layouts/_markup/render-image.html:
{{ $image := .Page.Resources.GetMatch .Destination }}
{{ if reflect.IsImageResourceProcessable $image }}
{{ partial "responsive-image.html" (dict
"image" $image
"alt" .PlainText
"title" .Title
"sizes" "(max-width: 700px) 100vw, 700px"
) }}
{{ else }}
<img src="{{ .Destination | safeURL }}" alt="{{ .PlainText }}"
{{ with .Title }}title="{{ . }}"{{ end }}>
{{ end }}
Normal Markdown can now use the bundle image:

The image render hook receives the destination, description, and optional title. Processable bundle images use the partial; SVG, remote destinations, and unmatched paths retain a basic image fallback.
That fallback does not repair a missing file. It preserves the destination and alternative text rather than trying an unsupported resize operation.
This hook intentionally leaves loading eager by default because it does not know the image’s position. A site can pass explicit image metadata for lazy loading later. Keep that decision separate from the resizing algorithm.
Keep the build work proportional to the page
Hugo caches processed images , but every additional transformation still introduces an output to manage. Three useful widths are easier to justify than fifteen nearly identical candidates.
The width cap in this example suits a modest article layout. A large photography page may need wider candidates for high-density displays; a small icon may need none of this processing.
The useful boundary is between preparing assets and selecting one for a visitor. Keep preparation in Hugo, give the browser an honest slot description, and avoid a runtime script whose only job is choosing among those same static files.
Filed under Hugo and Performance on
Last updated on
Enjoyed this article?
If you found it useful, share it with a friend or someone who might find it helpful too. Thanks for reading!
Related articles
- Cache Repeated Hugo Template Work With partialCached
Use Hugo partialCached for expensive repeated template work without accidentally reusing page-specific output.
- Why I Keep Fonts, CSS, and JavaScript With My Site
Self-hosting CSS, JavaScript, and fonts keeps critical dependencies versioned, predictable, and close to the page that needs them.
- How I Organize My Hugo Site
A predictable Hugo structure for content, page resources, layouts, assets, static files, and configuration.