Responsive Images

Responsive images, transformed by your CDN

Write one tag, get a srcset, sizes and modern formats from Cloudflare, Cloudinary, imgix or KeyCDN.

composer require symfony/ux-image

What Image gives you

  • srcset and sizes

    Every width the browser may need, from a single tag.

  • Modern formats

    AVIF or WebP when the browser accepts them, JPEG otherwise.

  • No layout shift

    Width, height and aspect ratio reserve the space before the image loads.

  • Any provider

    Change the DSN, keep your templates.

  • Presets and operations

    Name a set of transformations, or pass provider-specific effects.

Responsive Images, Delivered by your CDN

Add Image to your app.

Responsive img tags, transformed by your CDN

config/packages/ux_image.yaml
ux_image:
    provider: '%env(resolve:UX_IMAGE_DSN)%'

    presets:
        thumbnail:
            width: 200
            height: 200
            quality: 60
<twig:ux:image
    src="/uploads/col-de-saint-bonnet.jpg"
    alt="View from the Col de Saint-Bonnet"
    preset="thumbnail"
/>
Symfony logo

UX Image

Landscape, cropped to 16:9
View from the Col de Saint-Bonnet

2.3 MB JPEG measured in your browser

templates/ux_packages/image/_landscape.html.twig
<twig:ux:image
    src="/uploads/col-de-saint-bonnet.jpg"
    alt="View from the Col de Saint-Bonnet"
    width="480"
    height="270"
/>
Rendered HTML
<img
    src="https://res.cloudinary.com/fnmfvsxv/image/fetch/s--pYfkWjrh--/w_480,h_270,c_fill,f_auto/https://ux.symfony.com/uploads/col-de-saint-bonnet.jpg"
    alt="View from the Col de Saint-Bonnet"
    srcset="https://res.cloudinary.com/fnmfvsxv/image/fetch/s--pYfkWjrh--/w_480,h_270,c_fill,f_auto/https://ux.symfony.com/uploads/col-de-saint-bonnet.jpg 480w,
        https://res.cloudinary.com/fnmfvsxv/image/fetch/s--IP7ocXoV--/w_640,h_360,c_fill,f_auto/https://ux.symfony.com/uploads/col-de-saint-bonnet.jpg 640w,
        https://res.cloudinary.com/fnmfvsxv/image/fetch/s--gCP1dXee--/w_750,h_422,c_fill,f_auto/https://ux.symfony.com/uploads/col-de-saint-bonnet.jpg 750w,
        https://res.cloudinary.com/fnmfvsxv/image/fetch/s--An-4f96H--/w_828,h_466,c_fill,f_auto/https://ux.symfony.com/uploads/col-de-saint-bonnet.jpg 828w,
        https://res.cloudinary.com/fnmfvsxv/image/fetch/s--fWWMvhjt--/w_960,h_540,c_fill,f_auto/https://ux.symfony.com/uploads/col-de-saint-bonnet.jpg 960w"
    sizes="(min-width: 480px) 480px, 100vw"
    loading="lazy"
    width="480"
    height="270"
    style="object-fit: cover; max-width: 480px; max-height: 270px; aspect-ratio: 480 / 270; width: 100%; height: auto;" />
Portrait, cropped to a square
The Gros Caillou boulder

4.7 MB JPEG measured in your browser

templates/ux_packages/image/_portrait.html.twig
<twig:ux:image
    src="/uploads/gros-caillou.jpg"
    alt="The Gros Caillou boulder"
    width="360"
    height="360"
/>
Rendered HTML
<img
    src="https://res.cloudinary.com/fnmfvsxv/image/fetch/s--rMSVwNcX--/w_360,h_360,c_fill,f_auto/https://ux.symfony.com/uploads/gros-caillou.jpg"
    alt="The Gros Caillou boulder"
    srcset="https://res.cloudinary.com/fnmfvsxv/image/fetch/s--rMSVwNcX--/w_360,h_360,c_fill,f_auto/https://ux.symfony.com/uploads/gros-caillou.jpg 360w,
        https://res.cloudinary.com/fnmfvsxv/image/fetch/s--S57poYUb--/w_640,h_640,c_fill,f_auto/https://ux.symfony.com/uploads/gros-caillou.jpg 640w,
        https://res.cloudinary.com/fnmfvsxv/image/fetch/s--lu2pqIyD--/w_720,h_720,c_fill,f_auto/https://ux.symfony.com/uploads/gros-caillou.jpg 720w"
    sizes="(min-width: 360px) 360px, 100vw"
    loading="lazy"
    width="360"
    height="360"
    style="object-fit: cover; max-width: 360px; max-height: 360px; aspect-ratio: 360 / 360; width: 100%; height: auto;" />
The thumbnail preset: 200 × 200, quality 60
View from the Col de Saint-Bonnet

2.3 MB JPEG measured in your browser

templates/ux_packages/image/_thumbnail.html.twig
<twig:ux:image
    src="/uploads/col-de-saint-bonnet.jpg"
    alt="View from the Col de Saint-Bonnet"
    preset="thumbnail"
/>
Rendered HTML
<img
    src="https://res.cloudinary.com/fnmfvsxv/image/fetch/s--pgUCG2hH--/w_200,h_200,c_fill,f_auto,q_60/https://ux.symfony.com/uploads/col-de-saint-bonnet.jpg"
    alt="View from the Col de Saint-Bonnet"
    srcset="https://res.cloudinary.com/fnmfvsxv/image/fetch/s--pgUCG2hH--/w_200,h_200,c_fill,f_auto,q_60/https://ux.symfony.com/uploads/col-de-saint-bonnet.jpg 200w,
        https://res.cloudinary.com/fnmfvsxv/image/fetch/s--ITlYm-dR--/w_400,h_400,c_fill,f_auto,q_60/https://ux.symfony.com/uploads/col-de-saint-bonnet.jpg 400w"
    sizes="(min-width: 200px) 200px, 100vw"
    loading="lazy"
    width="200"
    height="200"
    style="object-fit: cover; max-width: 200px; max-height: 200px; aspect-ratio: 200 / 200; width: 100%; height: auto;" />

Rendered on the server

Your CDN does the heavy lifting.

UX Image never resizes an image itself. It writes the URLs your provider understands, and the provider transforms, encodes and caches every variant.

One DSN, any provider

Pick the provider with a DSN, and sign its URLs when it supports it. Switching providers never touches your templates.

# Cloudflare, on a proxied zone
UX_IMAGE_DSN=cloudflare://cdn.example.com

# Cloudinary, fetching from your origin, signed URLs
UX_IMAGE_DSN=cloudinary://my-cloud?origin=https://example.com&api_secret=${CLOUDINARY_SECRET}

# imgix, from a Web Folder source, signed URLs
UX_IMAGE_DSN=imgix://my-source.imgix.net?sign_key=${IMGIX_TOKEN}

# KeyCDN, through a pull zone, signed URLs
UX_IMAGE_DSN=keycdn://myzone.kxcdn.com?secure_token=${KEYCDN_TOKEN}

Three layouts

Constrained shrinks with its container, fixed keeps its size, full-width spans the viewport.

<twig:ux:image src="/uploads/hero.jpg" alt="Hero"
    layout="full-width" width="1600" height="600" priority />

<twig:ux:image src="/uploads/avatar.jpg" alt="Avatar"
    layout="fixed" width="64" height="64" />

Image or picture

<twig:ux:image> renders one img and lets the provider pick the format. <twig:ux:picture> adds one source per format, for providers that cannot, like KeyCDN.

<twig:ux:image src="/uploads/hero.jpg" alt="Hero"
    width="800" height="450" />

<twig:ux:picture src="/uploads/hero.jpg" alt="Hero"
    width="800" height="450" />

Operations per provider

Each provider has its own effects. Only the active one is applied, and a misspelled provider name fails at render time.

<twig:ux:image src="/uploads/hero.jpg" alt="Hero" width="800"
    :operations="{
        cloudflare: {sharpen: 1},
        cloudinary: {e: 'sharpen:100'},
        imgix: {sharp: 20},
        keycdn: {sharpen: 10},
    }" />

A single URL

For an og:image, an email or a CSS background, ux_image_url() returns one transformed URL.

{% set og_image = ux_image_url('/uploads/hero.jpg', {
    width: 1200,
    height: 630,
}) %}

<meta property="og:image" content="{{ absolute_url(og_image) }}">

Install Image.

$ composer require symfony/ux-image

More from Symfony UX.

Image is one package among many. They share the same Stimulus foundation and install the same way.

Image for the Toolkit UX package

Toolkit

Collection of components and templates that you can use to build your pages.

Image for the Native UX package

Native

Build native mobile apps that wrap your Symfony web application

Image for the Pagination UX package

Pagination New

Paginate any data with numbered pages, lookahead, or cursors.

Browse every package

Keep Image at hand.

Documentation, the people behind it, and where to ask when something does not behave.