roadmap icon indicating copy to clipboard operation
roadmap copied to clipboard

Responsive images

Open ascorbic opened this issue 1 year ago • 7 comments

Summary

Implements opinionated best practices in Astro Image, generating srcset, sizes and styles automatically.

---
import { Image } from "astro:assets"
import rocket from "./rocket.jpg"
---
<Image src={rocket} width={800} height={600} layout="responsive" />

Links

ascorbic avatar Nov 04 '24 13:11 ascorbic

I've updated the implementation details part of the RFC based on things I've discovered while prototyping.

ascorbic avatar Nov 07 '24 10:11 ascorbic

Hey! I‘m excited to see work being picked up again on the image components. I read the rfc and don’t really understand how values for the sizes attribute are created when „responsive“ layout is used. Is there JS being injected or how does the component know how large the image is show at different breakpoints?

carlcs avatar Nov 20 '24 07:11 carlcs

@carlcs It generates a sizes attribute based on the assumption that it's the full width of the screen when downsized. You'd need to pass your own if this is incorrect.

ascorbic avatar Nov 20 '24 09:11 ascorbic

@ascorbic that‘s perfect if we can still set sizes manually! You might want to change docs a bit because this part is a bit misleading.

		 * The following `<Image />` component properties should not be used with responsive images as these are automatically generated:
		 * 
		 * - `densities`
		 * - `widths`
		 * - `sizes` 		 

carlcs avatar Nov 20 '24 10:11 carlcs

@carlcs https://github.com/withastro/astro/pull/12482

ascorbic avatar Nov 20 '24 12:11 ascorbic

Hi @ascorbic , I really love this feature! Is there a timeline for it to go from experimental to stable?

flavianh avatar Feb 10 '25 10:02 flavianh

It would be great to enable support for:

img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}

I've got a background image that I'm using as a wallpaper, I just want it to act like layout="full-width", except I want to override the height as 100%. I'd like to avoid needing to just use important! to override the astro-provided styles, and still be able to opt into the autogeneration of sizes.

My current working solution is:


<Picture
  class:list={["NebulaHero", className]}
  src={bgHero}
  decoding="async"
  alt=""
  loading="eager"
  fetchpriority="high"
  layout="full-width"
/>

<style>
  .NebulaHero {
    position: absolute;
    inset: 0;
    height: 100% !important;
  }
</style>

jasikpark avatar Mar 07 '25 20:03 jasikpark

@jasikpark you should be able to do that now. You need to use a class rather than img, because the specificity is too low. I just tried this and it works:

---
import { Image } from "astro:assets";
import penguin from "../assets/penguin.jpg";
---
<Image src={penguin} alt="a penguin" layout="full-width" class="cover"/>
<style>
  .cover {
    height: 100%;
    position: absolute;
    inset: 0;
  }
</style>

ascorbic avatar Mar 28 '25 09:03 ascorbic

https://github.com/withastro/astro/issues/13666

I encountered a specificity issue when using Tailwind with responsive images. Changing the selector doesn’t resolve the problem because Tailwind’s @layer lowers its specificity.

I believe the styles applied by responsive images shouldn't exist. The sizes, width, and height attributes in HTML already provide a solid fallback for CSS and shouldn’t interfere with styling. The way responsive images impose styles contradicts the original intent of these attributes.

I previously saw Astro as a framework that stays close to native behavior and maintains predictability, which is why I chose it. However, its responsive image implementation feels overly complex, and I don’t find it appealing.

yinhx3 avatar Apr 22 '25 12:04 yinhx3

@yinhx3 thanks for your feedback. One of the main reasons we have the experimental features and RFC process is to get feedback on APIs and implementations. I was planning to simplify these styles and lower the specificity. I was originally going to use :where(), but your comment prompted me to look into using @layer which looks like it will work here, and makes it play better with Tailwind 4.

ascorbic avatar Apr 22 '25 14:04 ascorbic

Cascade layers aren't supported in many of the browser versions that Astro currently supports. Is there a way to use them in a backward-compatible way?

connor-baer avatar Apr 22 '25 14:04 connor-baer

@ascorbic Can you clarify the necessity of adding styles? They seem to serve the same function as attributes.

yinhx3 avatar Apr 22 '25 14:04 yinhx3

@yinhx3 the object-fit/position styles are to ensure consistent behaviour, whether or not the image service supports cropping. There is an argument to be made that they're not so important now that most of the built-in image services support cropping (Vercel is the main exception). max-width: 100% or width: 100% ensure that the resizing behaviour matches the expected layout. Your demos are using Tailwind which has base styles that handle it, so it doesn't affect you, but the idea of this feature is to make it easy to have images that automatically work as expected, with the correct sizes and srcset. By setting these automatically we avoid footguns that mean that images have values for these that may work, but will cause poor performance etc.

The next version will be removing the height, and the options that used the specific width and height of the image in the styles

ascorbic avatar Apr 22 '25 15:04 ascorbic

@ascorbic I’m sure you’ve considered this more thoroughly than I have, especially in terms of performance and image service integration. My perspective is that if certain styles for <img> can be managed through global CSS or a CSS reset, then adding them to astro.config.js may not be necessary. Doing so would increase configuration complexity, whereas using CSS allows users to easily control specificity and layering. We need to carefully balance functionality and complexity.

yinhx3 avatar Apr 22 '25 15:04 yinhx3

@yinhx3 the astro.config.mjs options are less about the styling than about controlling the auto-generated srcsets and sizes. We may end up removing the styles, but what I don't want is to end up requiring everyone to have to add styles in order to avoid setting some defaults.

ascorbic avatar Apr 22 '25 15:04 ascorbic

@ascorbic I agree with your perspective. Generating images of different sizes and populating the srcset, sizes, width, and height attributes is precisely what an SSG/SSR framework should handle. Global defaults for object-fit, object-position, and width can be set via CSS. Astro can have an opinionated CSS reset or leave it unset, allowing users to choose their own CSS reset—both approaches make sense.

yinhx3 avatar Apr 22 '25 16:04 yinhx3

I am considering changing the name of the layout option responsive. Currently the feature is called "responsive images", with three options for layout: fixed, full-width and responsive. These are used to define which srcset and sizes to generate, as well as the default styles. It's become clear that having one of the layouts called "responsive" is confusing, and people are choosing it when they need full-width. I'm thinking that it should be renamed to constrained, which better reflects what it actually is for: images that are displayed at a maximum of their original size, but will downsize if the display is too narrow.

ascorbic avatar Apr 23 '25 10:04 ascorbic

I'm thinking that it should be renamed to constrained

I think this fits the definition we currently have for the experimental docs:

"The image will scale to fit the container, maintaining its aspect ratio, but will not exceed the specified dimensions."

"Constrained" certainly implies a limit. (And, I believe this matches Gatsby's image plugin naming?) Seems like a good choice!

In any event, not naming one of the options the name of the feature seems like a smart move in general!

sarah11918 avatar Apr 23 '25 11:04 sarah11918

@sarah11918 I'd forgotten it was the same as Gatsby image! Their docs have a nice video showing the difference. I used the same names for Unpic too.

ascorbic avatar Apr 24 '25 08:04 ascorbic

This has now been updated with the changes to the layout name and simplified styles, included in https://github.com/withastro/astro/pull/13677

ascorbic avatar Apr 24 '25 08:04 ascorbic

I did some digging into responsive images, it works really well in the current version!

I opened a bug report related to duplicate / identical files being generated with different hash https://github.com/withastro/astro/issues/13819

And I'd like to propose to please consider the use case of Retina screenshots in .md files. The use case I believe is very common: some images in .md files are 1x resolution, some are 2x. How do you handle these correctly? (Without breaking into MDX).

I mean some kind of pattern needs to be used in the .md file, either filenames like image_2x.png, or image.png?r=2x or image.png#2x. I prefer _2x.png like file names.

I'm migrating from Ghost, where I made a client side JS to recognise this pattern and set the size correctly.

But with Astro's responsive images, I cannot do this, it needs to be signaled to the responsive image pipeline before rendering. Can you recommend a way to do this? My only idea is to make a Remark plugin, but it's not clear how to do this. Is there any simpler way?

My proposal is basically to auto-recognise _2x.ext like file names and automatically handle them as Retina.

hyperknot avatar May 18 '25 16:05 hyperknot

Also, from the linked Gatsby page, they only use screen size based brake-points for full-width images, which makes a lot of sense. For non-full-width images, why are we doing any kind of resizing based on common screen sizes? Our "constrained" image size has nothing to do with the screen-size.

[0.25, 0.5, 1, 2] densities make a lot more sense.

image

hyperknot avatar May 18 '25 16:05 hyperknot

@hyperknot I'm not keen on magic like trying to recognise filename patterns. I'd much rather there was an explicit way to pass args in markdown.

Re the second point, it's a long time since I built it but IIRC by default Gatsby does the same as us for constrained images: it includes 1x and 2x, plus screen widths that are smaller than the 1x size. This is to allow for scaling-down on smaller screens. The difference with fullWidth is that it only uses the screen widths.

ascorbic avatar May 19 '25 08:05 ascorbic

@ascorbic You are right that it should not hardcoded filename patters. Yesterday, I dig into it and realized a very minimal, simple rehype plugin can do this task properly, no hard coding needed. So users themselves can set up whatever logic they want, they can use query strings, hash or even alt text hints.

It's as simple as this:

import { visit } from 'unist-util-visit'

export function rehype2xImages() {
  return (tree: any) => {
    visit(tree, 'element', (node) => {
      if (node.tagName === 'img' && node.properties.src && node.properties.src.includes('_2x.')) {
        node.properties['data-retina2x'] = true
      }
    })
    return tree
  }
}

Now the part which I could not figure out is how to integrate it into the rest of the pipeline. I guess outside of forking Astro, it's not possible today?

What I did was the same client side JS hack, like I used on Ghost:

<script>
document.querySelectorAll('img[data-retina2x="true"]').forEach(img => {
  const width = parseInt(img.getAttribute('width'));
  img.style.maxWidth = (width / 2) + 'px';
});
</script>

This works, but give a flash + downloads the wrong image initially + probably Google also doesn't like it. So it'd be really nice to have this integrated in the image pipeline.

About the magic string "data-retina2x", of course that's just a string for my use case. A better one would be "dpr" for Device Pixel Ratio. Or devicePixelRatio and then we are consistent with the Web API.

So my proposal is the following:

  1. Users come up with whatever logic they want, and in a rehyde plugin they set
node.properties['devicePixelRatio'] = 2.0 // or 3.0 etc.
  1. The image pipeline recongises properties.devicePixelRatio and calculates the target width accordingly.

I think this way it'd be universal, yet very user-friendly, only requiring a tiny rehype plugin.

hyperknot avatar May 19 '25 10:05 hyperknot

@ascorbic I'd be happy to start contributing to this PR. What is the best way to connect? Discord #contribute channel?

hyperknot avatar May 23 '25 12:05 hyperknot

@hyperknot we don't usually have more than one contributor to an RFC PR. People can give feedback, but the author of the PR is the one who would integrate any changes. I'm happy to consider specific proposals and suggestions.

ascorbic avatar May 23 '25 15:05 ascorbic

OK, my proposal is as simple as accepting node.properties.devicePixelRatio on Markdown images and <Image devicePixelRatio={}> on Image tags, based on what I wrote before.

hyperknot avatar May 23 '25 15:05 hyperknot

I am making this a call for consensus with a goal to get this in 5.10. @withastro/tsc

ascorbic avatar Jun 16 '25 10:06 ascorbic