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 #3515646: Add automated <img srcset> generation, 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.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

effulgentsia created an issue. See original summary.

lauriii’s picture

💯 🤩 👏

balintbrews’s picture

Wow. What an amazing idea! 🙇

effulgentsia’s picture

Issue tags: +beta target

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.

libbna’s picture

I have given this issue a try: I checked out to the MR of #3515646 & 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!

isholgueras made their first commit to this issue’s fork.

wim leers’s picture

Assigned: Unassigned » isholgueras
Status: Active » Needs work

Initial review posted. Nice start!

isholgueras’s picture

@effulgentsia,

I'm missing something in your approach that is making me going back and forth in the ticket. I've pushed some code to help me explain that.

What you're proposing is to modify what is coming in the img.src with the url to the image itself, without any style, and as a query parameters alternateWidths send only the template (with the {width} and the itok) so the ui can react and modify.

The decoded url you've written is

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

Is that exactly that we want to send in the src=""

And for srcset what we want to generate is a full list of allowed widths? Something like:

<img class="image" src="/sites/default/files/2025-07/2025-07-02_17-30.png" srcset="/sites/default/files/styles/xb_parameterized_width--640/public/2025-07/3534165-6_2.png 640w, /sites/default/files/styles/xb_parameterized_width--750/public/2025-07/3534165-6_2.png 750w, /sites/default/files/styles/xb_parameterized_width--828/public/2025-07/3534165-6_2.png 828w, /sites/default/files/styles/xb_parameterized_width--1080/public/2025-07/3534165-6_2.png 1080w, /sites/default/files/styles/xb_parameterized_width--1200/public/2025-07/3534165-6_2.png 1200w, /sites/default/files/styles/xb_parameterized_width--1920/public/2025-07/3534165-6_2.png 1920w, /sites/default/files/styles/xb_parameterized_width--2048/public/2025-07/3534165-6_2.png 2048w, /sites/default/files/styles/xb_parameterized_width--3840/public/2025-07/3534165-6_2.png 3840w" alt="Phandalin" width="1487" height="1003" sizes="auto 100vw" loading="lazy">

I still don't get what is the HTML we want to return for this, sorry :(

wim leers’s picture

Is that exactly that we want to send in the src=""

No, we'd want to strip ?alternateWidths=…

And for srcset what we want to generate is a full list of allowed widths?

Yes. Render the sdc.experience_builder.image SDC in HEAD and copy/paste the resulting markup. That is the end result you must achieve using these different mechanics 😊

effulgentsia’s picture

No, we'd want to strip ?alternateWidths=…

Although we can choose to strip this from what the Twig renders into the src attribute if we want to, I don't think we have to. I think it's okay for the Twig to just do src="{{ image.src }}" as the MR already does and for that to mean the query parameter is retained. The query parameter is harmless other than adding some extra bytes to the HTML output, and those extra bytes are minor compared to what we end up needing in srcset anyway.

This MR still needs, within ShapeMatchingHooks.php, where we set the expression for srcSetCandidateTemplate to instead change the expression of src to be such that it evaluates to a URL that includes the alternateWidths query parameter. Eventually stuff like this could be done with AdaptedPropSource but I don't know if we have adapters working sufficiently well yet, so it might make sense to instead add another computed property to ImageItemOverride. XB's shape matching and prop expressions are kind of a tricky part of XB, so perhaps @wim leers could help out with this part?

isholgueras’s picture

Thanks both for your feedback! I know exactly what needs to be done.

wim leers’s picture

Assigned: isholgueras » wim leers

I think it's okay for the Twig to just do src="{{ image.src }}" as the MR already does and for that to mean the query parameter is retained.

That's technically fine indeed 👍

This MR still needs, within ShapeMatchingHooks.php, where we set the expression for srcSetCandidateTemplate to instead change the expression of src to be such that it evaluates to a URL that includes the alternateWidths query parameter.

Close! Making src be different will require changing:

  • \Drupal\experience_builder\Plugin\Field\FieldTypeOverride\ImageItemOverride::propertyDefinitions()
  • \Drupal\experience_builder\TypedData\ImageDerivativeWithParametrizedWidth::getValue()
  • \Drupal\experience_builder\Entity\ParametrizedImageStyle::buildUrlTemplate()

Eventually stuff like this could be done with AdaptedPropSource but I don't know if we have adapters working sufficiently well yet

They do on the back end at a low level; test coverage proves they work fine.

But … neither the front-end (XB UI) nor the back-end's \Drupal\experience_builder\Form\ComponentInputsForm that powers the Settings tab support it today. Because it's blocked on design.

, so it might make sense to instead add another computed property to ImageItemOverride. XB's shape matching and prop expressions are kind of a tricky part of XB, so perhaps @wim leers could help out with this part?

Yep, my thoughts exactly!

On it 👍

wim leers’s picture

Extra complexity here is that we only know at the ImageItem level what the special sauce is, but the src (image URL) is currently coming from

            'src' => new ReferenceFieldTypePropExpression(
              new FieldTypePropExpression('image', 'entity'),
              new FieldPropExpression(BetterEntityDataDefinition::create('file'), 'uri', NULL, 'url'),
            ),

👆That follows the image field type's entity property, and on the File entity that that points to, it instructs XB to retrieve the uri field's url property. String representation: src↝entity␜␜entity:file␝uri␞␟url.

We'd now need to change that quite a bit, and crucially, in a way that the dependency information remains present. 😅

wim leers’s picture

Drupal\Tests\experience_builder\Kernel\DataType\ComponentInputsDependenciesTest::testCalculateDependencies
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
         5 => 'file',
         6 => 'node',
         7 => 'file',
-        8 => 'node',
-        9 => 'file',
     ],
     'config' => Array &2 [
         0 => 'node.type.alpha',
@@ @@
         9 => 'node.type.alpha',
         10 => 'field.field.node.alpha.field_hero',
         11 => 'image.style.xb_parametrized_width',
-        12 => 'node.type.alpha',
-        13 => 'field.field.node.alpha.field_hero',
-        14 => 'image.style.xb_parametrized_width',
-    ],
-    'content' => Array &3 [
-        0 => 'file:file:d500fabe-6e85-4877-b250-a6d719fdb53e',
     ],
 ]

— results for ComponentInputsDependenciesTest

This is what I predicted in #14 about dependency information going missing: it's because we're no longer having XB itself follow the image field → entity reference → file entity → uri field → url property chain, hence XB doesn't know about this dependency: it's all abstracted away by this new computed field property.

So that computed field property must somehow provide the correct dependency information… tricky!

wim leers’s picture

Assigned: wim leers » isholgueras

Got an initial solution for the dependency challenge above partially working. It needs more attention.

But due to the reliance on this computed property, we introduced a new challenge: the automatic (typed data/constraint-based) shape for finding candidate DynamicPropSources now won't work anymore: it continues to find what's in HEAD (follow the entity reference down to the file). So that logic will now also need to be updated for all tests to pass. If @isholgueras can get it to the point where that is the cause for the last remaining failures, that'd be great!

wim leers’s picture

Priority: Normal » Major
Issue tags: +DX (Developer Experience)
wim leers’s picture

Assigned: isholgueras » wim leers
balintbrews’s picture

I added all frontend pieces to make image props work in code components (1ec1c952).

You can test with the following code component:

import Image from "next-image-standalone";

// Make sure you have an image prop named "Photo".
const Cover = ({ photo }) => {
  return <Image {...photo} />;
};

export default Cover;

justafish made their first commit to this issue’s fork.

effulgentsia’s picture

Issue tags: -beta target +beta blocker

@lauriii and I discussed that getting the image component DX right should actually block the beta, so I tagged #3535453: Create an Image SDC that can be included by other SDCs as such and promoting this one as well.

wim leers’s picture

Assigned: wim leers » isholgueras
Status: Needs work » Reviewed & tested by the community
Related issues: +#3536115: Allow use of same-shape-adapters ahead of general adapter support in #3464003

Got the shape matching pieces done.

With 0.6.0-alpha1 out and working on this, my mind had the space to spot a whole range of concerns. Created #3536115: Allow use of same-shape-adapters ahead of general adapter support in #3464003 for those. This issue doesn't make things worse (it follows an already established pattern), so we shouldn't block this on that.

2 concerns about the public API of the new Twig function remain, @isholgueras is solving those,:

  1. https://git.drupalcode.org/project/experience_builder/-/merge_requests/1...
  2. https://git.drupalcode.org/project/experience_builder/-/merge_requests/1...

Once those are solved, I think this is ready to land! 🚀

balintbrews’s picture

I made a small change in the starter code so we don't set a bad example with prop spreading. Thanks to @effulgentsia for pointing that out. I re-tested the code component functionality, still works great with the changes by this MR. The UI code probably could use a quick review as I wrote all of it, I wouldn't want to sign off on my own code. 🙂

wim leers’s picture

Title: Improve the front-end DX of <img srcset> » Improve the front-end DX of `<img srcset>`
Assigned: isholgueras » Unassigned

Pushed commits adding @todo comments pointing to #3464003: [PP-1] [later phase] [needs design] Introduce "adapters" UX, #3530351: Decouple image+video (URI) shape matching from specific image+video file types/extensions and #3536115: Allow use of same-shape-adapters ahead of general adapter support in #3464003.

Addressed the last remaining concern.

Did a final clean-up pass to and in doing so, found an edge case bug: if the image itself was exactly the largest allowed parametrized image style width, we'd generate a srcset width for it, which makes no sense: it'd mean generating a derivative of identical dimensions as the original — costing unnecessary CPU + storage resources to the server, and unnecessary network transfer for both client and server.

Thanks @hooroomoo for the front-end approval!

Let's ship this 😊 — and I'll see some of you in #3536115: Allow use of same-shape-adapters ahead of general adapter support in #3464003 tomorrow 🤓

wim leers’s picture

Status: Reviewed & tested by the community » Fixed

The only failure: playwright, but that's a known pseudo-random fail: #3536108: The `playwright` CI job is also checking code style, but a failure is easily missed.

neha_bawankar’s picture

Tested changes on branch 0.x , for following scenarios :

Scenario Result Status
Create image component, set img width in global css as 500
const Image = ({ image }) => {
  return (
    &lt;img {...image} /&gt;
  );
};
export default Image
        
In page view, image width is 500
&lt;img src="/sites/default/files/2025-07/download.jpeg" alt="normal" width="187" height="148"&gt;
PASS
Drag and drop existing test image component, add media, image width is set in global css as 500px
&lt;img class="image" src="/sites/default/files/2025-07/download.jpeg" ... width="187" height="148" loading="lazy"&gt;
PASS
Upload component from CLI, add component to page, add media, publish changes
&lt;img src="/sites/default/files/2025-07/download.jpeg" alt="normal" width="187" height="148" loading="lazy"&gt;
PASS
wim leers’s picture

Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.