· tutorials · 5 min read

Dynamically Importing Images in Astro Components the Right Way

I’ve been a fan of static sites for a long time thanks to their speed, ease of deployment, and ability to manage multiple development environments easily. Jekyll has been powering my blog for several years, but recently I’ve been experimenting with Astro, a web framework designed to build fast content sites, powerful web applications, dynamic server APIs, and everything in-between.

Although I’ve been having great results using Astro so far, there’s been a few things that I’ve needed to figure out, including how to dynamically import images in Astro components. Fortunately after some research and experimentation I was able to come up with a working solution.

This guide will show you how to dynamically import images in Astro components the right way by using Astro’s <Image /> component in conjunction with Vite’s import.meta.glob() and will also provide the explanation behind why this works.

Astro’s <Image /> Component

Astro has the ability to transform, bundle, and optimize your images for you using Astro’s built-in <Image /> component, and this confers numerous benefits over using a standard HTML <img> tag including:

  • The ability to transform an image’s dimensions so that the resulting <img> tag includes alt, loading, and decoding attributes.
  • Inferring image dimensions to avoid Cumulative Layout Shift (CLS), which forms part of Google’s Lighthouse score.

Given these benefits, it only makes sense to use <Image /> in place of <img>. However, in order to do so, we need to import the image component and the image. Consider the following code I’ve placed inside index.astro:

---
// import the Image component and the image
import { Image } from 'astro:assets';
import myImage from "../assets/my_image.png";
---

// alt is mandatory on the Image component
<Image src={myImage} alt="A description of my image." />

For the curious, the above <Image /> tag is translated by Astro into an <img> tag with the following attributes:

// Output
// Image is optimized, proper attributes are enforced
<img
  src="/_astro/my_image.hash.webp"
  width="1600"
  height="900"
  decoding="async"
  loading="lazy"
  alt="A description of my image."
/>

The Challenge

What if we want to pass an image path to an <Image /> which resides in a component we’ve written? Unfortunately, just trying to pass the image source as a prop to the <Image /> component won’t work. For example, create a Card.astro component and add the following:

---
import { Image } from 'astro:assets';

const { imgSrc } = Astro.props;
---

<div>
    <Image src={imgSrc} alt="" />
</div>

and paste this into index.astro

---
import Card from '../components/Card.astro';
---

<main>
    <Card imgSrc ="../assets/path.jpg" />
</main>

Trying to do the above causes the following error:

Astro dynamic import LocalImageUsedWrongly

LocalImageUsedWrongly
Local images must be imported.
Image's and getImage's src parameter must be an imported image or an URL, it cannot be a string filepath. Received ../assets/pat.jpg

One common pitfall is to try and use a dynamic import to import the image:

import { Image } from 'astro:assets';

const { imgSrc } = Astro.props;
---

<div>
    <Image src={import(imgSrc)} alt="" />
</div>

Although the above example works (with warnings) while using the dev server (npm run dev), on build it generates an error while generating static routes due to limitations in Vite’s Dynamic Import feature:

 generating static routes 
▶ src/pages/index.astro
 error   Cannot find module '/home/markjames/tmpPrj/ex/dist/chunks/assets/path.jpg' imported from /home/markjames/tmpPrj/ex/dist/chunks/pages/index_e2f36079.mjs

The Solution

Fortunately, an elegant solution to this problem exists by making use of Vite’s import.meta.glob() function. Re-write the component like this:

import { Image } from 'astro:assets';

const { imgSrc } = Astro.props;
const images = import.meta.glob("../assets/*");
---

<div>
    <Image src={images[imgSrc]()} alt="" />
</div>

Now reload the page and everything should work:

Working dynamic image

Note that import.meta.glob() is async, and it is awaited automatically by <Image />’s src, so if you want to move the call outside the component, you’ll need to use await.

How this Works

Vite’s import.meta.glob() is a way to load many local files into your static site setup. For example, using Vite’s import.meta.glob(), the following import statement:

const images = import.meta.glob('../assets/*')

becomes:

// code produced by vite
const images = {
  './assets/path.jpg': () => import('./assets/path.jpg'),
  './assets/car.jpg': () => import('./assets/car.jpg'),
}

Observe that the function returns a list of assets as well as their paths which can then be used to match our image with an import. Also note that Astro.glob() is a wrapper to import.meta.glob(), but the reason we use import.meta.glob() and not Astro.glob() is twofold:

  • We want access to each file’s path. import.meta.glob() returns a map of a file’s path to its content, while Astro.glob() returns a list of content.
  • All imports are lazy (unless explicitly made eager through the eager:true parameter) and they map to import statements so we don’t run things we don’t need.

Conclusion

As we’ve seen above, it can be a bit tricky to pass image paths dynamically to <Image /> components within other Astro components, but fortunately there’s an easy way to do so using import.meta.glob(). Also, don’t forget that Astro’s <Image /> component requires that you pass alt text (for accessibility reasons), so make sure that you provide alt text where applicable.

If you have any questions or additions to this guide, don’t hesitate to leave a comment or contact me.

Back to Blog