Skip to content

Responsive images#1051

Merged
ascorbic merged 7 commits into
mainfrom
responsive-images
Jun 16, 2025
Merged

Responsive images#1051
ascorbic merged 7 commits into
mainfrom
responsive-images

Conversation

@ascorbic
Copy link
Copy Markdown
Contributor

@ascorbic ascorbic commented Nov 4, 2024

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 ascorbic mentioned this pull request Nov 4, 2024

For that reason, this RFC includes image service crop support as a goal, though it is not a blocker for the initial feature.

#### New `ImageTransform` properties
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a technical detail, but it might be the perfect time to add ImageTransform to the interfaces we allow users to extend (in packages/astro/src/types/public/extendables.ts in next) so that image services can define some other props they support.

Comment thread proposals/0053-responsive-images.md Outdated
@ascorbic
Copy link
Copy Markdown
Contributor Author

ascorbic commented Nov 7, 2024

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

@carlcs
Copy link
Copy Markdown

carlcs commented Nov 20, 2024

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?

@ascorbic
Copy link
Copy Markdown
Contributor Author

@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.

@carlcs
Copy link
Copy Markdown

carlcs commented Nov 20, 2024

@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` 		 

@ascorbic
Copy link
Copy Markdown
Contributor Author

@carlcs withastro/astro#12482

@flavianh
Copy link
Copy Markdown

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

@jasikpark
Copy link
Copy Markdown
Contributor

jasikpark commented Mar 7, 2025

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>

@ascorbic
Copy link
Copy Markdown
Contributor Author

@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>

@yinhx3
Copy link
Copy Markdown

yinhx3 commented Apr 22, 2025

withastro/astro#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.

@ascorbic
Copy link
Copy Markdown
Contributor Author

@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.

@connor-baer
Copy link
Copy Markdown

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?

@yinhx3
Copy link
Copy Markdown

yinhx3 commented Apr 22, 2025

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

@ascorbic
Copy link
Copy Markdown
Contributor Author

ascorbic commented Apr 22, 2025

@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

@yinhx3
Copy link
Copy Markdown

yinhx3 commented Apr 22, 2025

@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.

@ascorbic
Copy link
Copy Markdown
Contributor Author

@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.

@yinhx3
Copy link
Copy Markdown

yinhx3 commented Apr 22, 2025

@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.

@ascorbic
Copy link
Copy Markdown
Contributor Author

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.

@sarah11918
Copy link
Copy Markdown
Member

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!

@ascorbic
Copy link
Copy Markdown
Contributor Author

ascorbic commented Apr 24, 2025

@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
Copy link
Copy Markdown
Contributor Author

This has now been updated with the changes to the layout name and simplified styles, included in withastro/astro#13677

@hyperknot
Copy link
Copy Markdown

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 withastro/astro#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
Copy link
Copy Markdown

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

@ascorbic
Copy link
Copy Markdown
Contributor Author

@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.

@hyperknot
Copy link
Copy Markdown

hyperknot commented May 19, 2025

@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
Copy link
Copy Markdown

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

@ascorbic
Copy link
Copy Markdown
Contributor Author

@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.

@hyperknot
Copy link
Copy Markdown

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

@ascorbic
Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor

@matthewp matthewp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great

@ascorbic ascorbic merged commit 4278c0c into main Jun 16, 2025
@ascorbic ascorbic deleted the responsive-images branch June 16, 2025 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.