This article is written for Vue 3 and Nuxt 3.
If you are looking for the Nuxt 2/Vue 2 version of this article, please follow this link to the older Nuxt 2 / Vue 2 version.
When using Vue or Nuxt, we have are spoilt for choice when it comes to asset handling: Do we put them in the
Read further to find out why loading static assets from neither folder is no problem at all and which pattern to use when the asset path or name must be dynamic. In case you want to skip the internals and explanations, you can do so and go straight to the solution, but you will miss out some in-depth information!
Let's define our experimental use case first: Imagine a component called
The only thing our components needs is a template with a single image tag using the desired path as
When using the
<img src="@/assets/doggos/riley.jpg">
<img src="../assets/doggos/riley.jpg">
In case you are using the
<img src="/doggos/riley.jpg">
So far so good -- but what if we have a list of cute puppies and the user can decide which image to display on the page?
One suitable way is to put all the images in the
Let's say we load the dynamic image source via Vue’s binding system. Imagine we have a small component set up where we can select a puppy:
<script setup lang="ts">
const dogNames = ['Riley', 'Annie', 'Marvin'];
const selectedDog = ref('');
</script>
<template>
<div>
<label v-for="doggo in dogNames" :key="doggo" style="margin-right: 2rem">
<input type="radio" :value="doggo" v-model="selectedDog" />
{{ doggo }}
</label>
<img :src="`/doggos/${selectedDog.toLowerCase()}.jpg`" width="500" :alt="selectedDog" />
</div>
</template>
All that is left to do is to retrieve the correct image for the
<img :src="`/doggos/${selectedDog.toLowerCase()}.jpg`" :alt="selectedDog">
And it works! We can now select a puppy and the image will be displayed. Open this CodeSandbox to see the code up and running.
This approach has a few downsides though:
Let's take a look how the approach for the
Alright, let's take the code from above as base. As a naive approach, why not just replace the paths using the path to assets instead?
<img :src="`../assets/doggos/${selectedDog.toLowerCase()}.jpg`" :alt="selectedDog">
Let’s add that line quickly and see what happens when we push the button mapped to Riley…
Bummer, a broken image and only the alt tag! Let us take a look at the DOM. It contains the following image tag:
<img src="../assets/doggos/riley.jpg" alt="Riley">
It means that the asset path hasn’t been replaced. It is the string that the expression in our template string above evaluates to, but no bundler magic happens.
So, what now?
The solution to this problem depends on the bundler you are using. If you are using Webpack with Vue 3, which is rather uncommon, you can follow the solution from the Vue 2 / Nuxt 2 post.
We will focus on Vite here, as it is the default bundler for Vue 3 and Nuxt 3.
As Vite does not support
Instead of
Let's start simple and grab all
<script setup lang="ts">
const glob = import.meta.glob('@/assets/doggos/*.jpg', { eager: true })
</script>
If we stringify the result, we can see that we get an object with the local image path as key and the image path in a nested object as value. The
{
"/assets/doggos/annie.jpg": {
"default": "/_nuxt/assets/doggos/annie.jpg"
},
"/assets/doggos/marvin.jpg": {
"default": "/_nuxt/assets/doggos/marvin.jpg"
},
"/assets/doggos/riley.jpg": {
"default": "/_nuxt/assets/doggos/riley.jpg"
}
}
This is close to what we want, so we need to do some transformations. We will take the entries of the object and map over them, eventually assembling them into an object again. In the
<script setup lang="ts">
import { filename } from 'pathe/utils'
const glob = import.meta.glob('@/assets/doggos/*.jpg', { eager: true })
const images = Object.fromEntries(
Object.entries(glob).map(([key, value]) => [filename(key), value.default])
)
</script>
This is also the suggested workaround from Daniel Roe for achieving a "require-like" behavior with Vite.
Now, let's put it all together!
<script setup lang="ts">
import { filename } from 'pathe/utils'
const dogNames = ['Riley', 'Annie', 'Marvin'];
const selectedDog = ref('');
const glob = import.meta.glob('@/assets/doggos/*.jpg', { eager: true })
const images = Object.fromEntries(
Object.entries(glob).map(([key, value]) => [filename(key), value.default])
)
</script>
<template>
<div>
<label v-for="doggo in dogNames" :key="doggo" style="margin-right: 2rem">
<input type="radio" :value="doggo" v-model="selectedDog" />
{{ doggo }}
</label>
<img
:src="images[`${selectedDog.toLowerCase()}`]"
width="500"
:alt="selectedDog"
/>
</div>
</template>
To see the code in action, you can also check out the StackBlitz.
And we achieve a similar result to the
But it has also some downsides:
Both approaches have their pros and cons as you can see. I'd recommend using the
The
If you want to replicate a webpack-like behavior in Vite, loading images with dynamic paths is not as easy as it might seem at first glance. But with the help of
Still have questions? No problem, drop me a Tweet (or however it is called now) at @TheAlexLichter, reach out on the Vue/Nuxt Discord or write me a mail (blog at lichter dot io).
I hope you enjoyed this article and learned something new! If you did, please consider sharing it with your friends and colleagues. Thanks for reading!

I'm Alex, a German web engineering consultant and content creator. Helping companies with my experience in TypeScript, Vue.js, and Nuxt.js is my daily business.
More about meSentry is a great tool to track errors and performance issues in your application - but the Nuxt module is not Nuxt 3 compatible yet. In this article, I'll show you how to integrate Sentry into your Nuxt 3 application, even before the module is ready and also share why it takes longer than you might think to build the module.
Recently I stumbled upon a very interesting code sample which I had to review. As I'm a huge clean code advocate, I'll dissect the small code piece with you and explain several techniques that help to write clean, human-readable and maintainable code.