SDC Image Handling
When to Use
Use this when your SDC component has an image prop (using
$ref: 'json-schema-definitions://canvas.module/image') and you need to render it in the Twig template correctly — with responsive images, lazy loading, performance attributes, and proper alt text.
Decision
| Situation | Choose | Why |
|---|---|---|
| Above-the-fold / hero image (LCP candidate) | loading: 'eager' + attributes: create_attribute({fetchpriority: 'high'}) |
Prioritizes the largest contentful paint |
| Below-the-fold images | loading: 'lazy' (the default) |
Defers network requests until needed |
| Multiple images on a page | Only ONE with fetchpriority: high |
Multiple high-priority images defeat the purpose |
Pattern
Always use Canvas's built-in canvas:image component to render image props. Do not build your own <img> tag from the image prop object.
The critical detail: canvas:image takes src at the top level, not an image object. Its first line is {% set width = width ?? src|getWidth %}, and getWidth() is declared public function getWidth(string $src) in a declare(strict_types=1) file. Pass {image: image} and src is undefined, so getWidth(null) raises a TypeError — the component blows up at render and the editor gets a broken-component box.
So spread your image object into the include context:
{# The pattern Canvas's own components use.
`image` is your $ref'd image prop; `sizes` and `loading` here are props of
the *outer* component, so the editor can tune them per placement. #}
{% if image %}
{% include 'canvas:image' with image|merge({
loading,
sizes,
class: 'card__image',
attributes: create_attribute({'data-testid': 'card-image'}),
}|filter((value) => value is not null)) only %}
{% endif %}
{# For above-the-fold / hero images: eager. #}
{% if hero_image %}
{% include 'canvas:image' with hero_image|merge({
loading: 'eager',
class: 'hero__image',
}|filter((value) => value is not null)) only %}
{% endif %}
The |filter((value) => value is not null) is not decoration: it drops unset keys so canvas:image's own ?? fallbacks can fire instead of receiving an explicit null.
canvas:image parameters (from components/image/image.component.yml and image.twig):
| Parameter | Type | Effective default | Purpose |
|---|---|---|---|
src |
string (format: uri-reference, contentMediaType: image/*) |
— | Required. Relative or absolute URL, or a stream-wrapper URI (public://…) — file_url() resolves it |
alt |
string | none (attribute omitted) | Alt text |
width |
integer | derived from src via \|getWidth |
Intrinsic width |
height |
integer | derived from src via \|getHeight |
Intrinsic height |
sizes |
string | auto 100vw |
sizes attribute, emitted only when a srcset was produced |
loading |
string, enum lazy/eager |
lazy |
lazy below-fold; eager above-fold |
class |
string | image |
Class attribute. Undeclared in the YAML but read by the Twig — it works |
attributes |
Attribute | none | Extra attributes, rendered at the end of the tag. Also undeclared in the YAML but read by the Twig |
Notes on what is not there:
- No
fetchpriorityparameter.image.twignever reads it; passing it is inert. If you needfetchpriority="high"on an LCP image, put it inattributes:attributes: create_attribute({fetchpriority: 'high'}) - No
image_attributesparameter. The key isattributes srcsetis computed, not passed.image.twigderives it fromsrc|toSrcSet(width). There is nosrcsetprop and noimage.srcsetvalue
canvas:image is itself a worked example of the defaults rule: its YAML says default: lazy on loading, Canvas ignores that, and the lazy you actually get comes from loading ?? 'lazy' on the last line of image.twig.
Performance guidance:
- Use loading: eager for the first visible image on a page (LCP candidate); add fetchpriority: 'high' through attributes
- Use loading: lazy (the default) for all below-the-fold images
- Only one image on a page should carry fetchpriority="high" — using it on several defeats the purpose
Common Mistakes
{% include 'canvas:image' with {image: image} only %}— the killer.canvas:imagereadssrc, so this throws aTypeErrorunder strict types. Spread withimage|merge({...})<img src="{{ image }}">— the image prop is an object, not a URL string; this outputs nothing or breaks<img src="{{ image.url }}">—image.urldoes not exist. The key isimage.src, and building your own tag still bypasses srcset and the stream-wrapper URL resolution; usecanvas:image- Passing
fetchpriority:orimage_attributes:— neither is acanvas:imageparameter. Useattributes: create_attribute({...}) - Using
{% embed 'canvas:image' %}— Canvas:image must be included with{% include ... only %}, not embedded - Setting
fetchpriority="high"on every image — only the LCP image should have this - Missing
{% if image %}guard — the prop can be empty if the editor hasn't uploaded an image yet
See Also
- SDC Props Reference for image prop definition
- Canvas SDC Images docs: https://project.pages.drupalcode.org/canvas/sdc-components/image/
- Canvas Code Component responsive images: https://project.pages.drupalcode.org/canvas/code-components/responsive-images/
- Dripyard article on handling images across Drupal and Canvas: https://dripyard.com/blog/handling-images-drupal-and-canvas-same-component