Improve the front-end DX of <img srcset>

Created on 27 June 2025, 8 days ago

Overview

Currently, the image prop type consists of 4 properties: src, alt, width, and height.

This means a (Twig) SDC can render an image prop like this:

<img src="{{ image.src }}" alt="{{ image.alt }}" width="{{ image.width }}" height="{{ image.height }}" />

And a (JS) Code Component can render it like this:

export default function({ image }) {
  return (
    <img src={ image.src } alt={ image.alt } width={ image.width } height={ image.height } />
  )
}

Or, taking advantage of JSX prop spreading, like this:

export default function({ image }) {
  const { src, alt, width, height } = image;
  return (
    <img { ...{src, alt, width, height} } />
  )
}

In ✨ Add automated image optimization to image component Active , we're working on adding a 5th property: srcsetCandidateTemplate. That name is still a work in progress. For the purpose of this issue, let's simplify it to scaledSrc. The idea is that src would be the original image, or possibly one with an image style applied for visual effect (color shifting, watermarking, cropping, etc.), but not for size optimization. Meanwhile, scaledSrc would be the URL template for scaling to one of several widths. For example:

src = '/sites/default/files/foo.jpg'
scaledSrc = '/sites/default/files/styles/xb_parameterized_width--{width}/public/foo.jpg.webp?itok=1Rl59WAb'

Assuming we add a toSrcSet filter, this would allow a Twig SDC to do this:

<img src="{{ image.src }}" alt="{{ image.alt }}" width="{{ image.width }}" height="{{ image.height }}" srcset="{{ image.scaledSrc|toSrcSet }}" sizes="auto 100vw" />

It would allow a JS code component to do something similar, but where JS components really shine is in the ability to use the JS ecosystem, such as next/image (or a version of it that can be used on its own without Next.js: next-image-standalone). So, for example:

import Image from "next-image-standalone";

export default function({ image }) {
  const { src, alt, width, height, scaledSrc } = image;
  const loader = ({ width }) => scaledSrc.replace('{width}', width);

  return (
    <Image { ...{src, alt, width, height, loader} } />
  )
}

The above is nice and enables the code component to use various other nice features from next/image by passing the appropriate props. But one thing that's annoying about the above is the need to pass in an explicit loader prop. Although it's possible to configure a centralized loader, so long as it's relying on scaledSrc, the code component has to pass along scaledSrc somehow, just like the Twig version earlier.

I discussed this with @lauriii and he's been pushing back on this, asking: is it possible to let component authors just deal with standard <img> concepts like src, alt, width, and height, and not introduce any Drupalisms like a scaledSrc property?

Proposed resolution

What if instead of separate src and scaledSrc properties we combined both into src like this:

src = '/sites/default/files/foo.jpg?alternateWidths=%2Fsites%2Fdefault%2Ffiles%2Fstyles%2Fxb_parameterized_width--%7Bwidth%7D%2Fpublic%2Ffoo.jpg.webp%3Fitok%3D1Rl59WAb'

In other words, instead of a scaledSrc property we add it as a query parameter to src. This query parameter wouldn't affect the response if the browser requests this src (especially if foo.jpg is already on disk in which case query parameters do nothing), but it would allow the Twig SDC to do this:

<img src="{{ image.src }}" alt="{{ image.alt }}" width="{{ image.width }}" height="{{ image.height }}" srcset="{{ image.src|toSrcSet }}" sizes="auto 100vw" />

And more importantly, it would allow a JS code component to do this:

import Image from "next-image-standalone";

export default function({ image }) {
  const { src, alt, width, height } = image;

  return (
    <Image { ...{src, alt, width, height} } />
  )
}

Where a default loader function could be configured completely invisibly and from the perspective of the component author, they're using next/image completely idiomatically.

šŸ“Œ Task
Status

Active

Version

0.0

Component

Shape matching

Created by

šŸ‡ŗšŸ‡øUnited States effulgentsia

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @effulgentsia
  • šŸ‡«šŸ‡®Finland lauriii Finland

    šŸ’Æ 🤩 šŸ‘

  • šŸ‡³šŸ‡±Netherlands balintbrews Amsterdam, NL

    Wow. What an amazing idea! šŸ™‡

  • šŸ‡ŗšŸ‡øUnited States effulgentsia

    Tagging this as beta target, because it would be nice to not have to either break BC after beta1 or support BC related to this. But not tagging it as a beta blocker, because it's not worth delaying a beta release on it.

  • šŸ‡®šŸ‡³India libbna New Delhi, India

    I have given this issue a try: I checked the MR of #3515646 ✨ Image component for code components Active & then I have updated the Image component to implement the proposed solution that eliminates the need for Drupal-specific props while maintaining full responsive image functionality.

    import NextImage from 'next-image-standalone';
    import type { ImageLoaderParams, ImageProps } from 'next-image-standalone';
    
    const parseAlternateWidths = (src: string): string | null => {
      try {
        const url = new URL(src, window.location.origin);
        const alternateWidths = url.searchParams.get('alternateWidths');
        return alternateWidths ? decodeURIComponent(alternateWidths) : null;
      } catch {
        return null;
      }
    };
    
    export default function Image({
      src,
      alt,
      width,
      height,
      ...props
    }: Omit<ImageProps, 'loader'>) {
      const loader = ({ width }: ImageLoaderParams) => {
        const alternateWidths = parseAlternateWidths(src);
        if (alternateWidths) {
          return alternateWidths.replace('{width}', width.toString());
        }
    
        return src;
      };
    
      return (
        <NextImage
          src={src}
          alt={alt}
          width={width}
          height={height}
          loader={loader}
          {...props}
        />
      );
    }
    
    1. Removed srcSetCandidateTemplate prop
    2. Added URL parameter parsing via parseAlternateWidths
    3. Combined image source and responsive width info into single src prop
    4. Simplified component API for better DX (Developer Experience)

    Before pushing the changes, I’d appreciate it if someone could review the approach and let me know if any improvements or adjustments are needed. Thanks!

Production build 0.71.5 2024