Skip to content

Vite and Vue ​

Ready-made configurations for the Vite plugin in a Vue 3 project. Each recipe says when to use it, shows the complete config and the template, and states what you get. Choosing an Approach helps to pick one.

All recipes need sharp as a dev dependency, and thumbhash wherever a ThumbHash is computed:

bash
npm install -D sharp thumbhash

For typed imports, reference the bundled declarations once, for example in env.d.ts:

ts
/// <reference types="@macrulez/vue-image-kit/vite/client" />

Blur for an imported image ​

When to use it. A handful of images are imported into components, and all you want is a blur and the right size while they load. Nothing is resized and no file is written.

ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { vueImageKit } from '@macrulez/vue-image-kit/vite'

export default defineConfig({
  plugins: [vue(), vueImageKit({ generate: false })],
})

generate: false stops the plugin from processing a whole folder on start, so there is no input to configure — it only answers the imports below.

vue
<script setup lang="ts">
import photo from './assets/photo.jpg'
import placeholder from './assets/photo.jpg?placeholder'
</script>

<template>
  <VImage :src="photo" v-bind="placeholder" alt="Photo" />
</template>

What you get. ?placeholder returns { hazehash, placeholderColor, width, height } (blurhash when the hazehash package is not installed), computed from the real file at build time — the same props <VImage> takes, so v-bind is enough. The two imports of one file resolve to different things: the plain one is the image URL, the one with the query is the placeholder data.

Variations.

  • ?placeholder=blurhash — a BlurHash instead of the HazeHash.
  • ?placeholder=thumbhash — a ThumbHash instead of the HazeHash (handles transparency).
  • ?placeholder=color — only the dominant color and the size, the lightest option.
  • ?hazehash, ?blurhash or ?thumbhash — just the hash string, when you want to pass it yourself: <VImage :src="photo" :hazehash="hash" :width="800" :height="600" />.
  • ?placeholder=blurhash,size — any set of fields in one import, for example the hash plus the real width and height. The fields are hazehash, blurhash, thumbhash, color, size, preview and aspect; see Choosing what to compute.
  • ?color, ?size, ?aspect, ?preview — one bare value each: '#c82828', { width, height }, 2, a tiny PNG data URI.
  • ?blurhash&components=6x4 — settings for this one image only.

Blur for a whole folder with import.meta.glob ​

When to use it. Many images in a folder, and the component picks one by its file name or key.

ts
// vite.config.ts — the same as in the previous recipe
vueImageKit({ generate: false })
vue
<script setup lang="ts">
const images = import.meta.glob('./assets/gallery/*.jpg', { import: 'default', eager: true })
const placeholders = import.meta.glob('./assets/gallery/*.jpg', {
  query: '?placeholder',
  import: 'default',
  eager: true,
})

const names = ['sunrise', 'forest', 'lake']
const path = (name: string) => `./assets/gallery/${name}.jpg`
</script>

<template>
  <VImage
    v-for="name in names"
    :key="name"
    :src="images[path(name)]"
    v-bind="placeholders[path(name)]"
    :alt="name"
  />
</template>

What you get. Two objects keyed by the same file paths: the image URLs and their placeholder props. Adding a file to the folder is enough — no extra import to write.

Blur for images whose path comes from data ​

When to use it. The src is not written in a template: it comes from an array, a CMS or a JSON file (:src="category.image"). A single import per image can't work here, so give the plugin whole folders and it builds a manifest keyed by URL.

ts
// vite.config.ts
import { vueImageKit } from '@macrulez/vue-image-kit/vite'

export default defineConfig({
  plugins: [
    vue(),
    vueImageKit({
      generate: false,
      placeholders: { dirs: ['public/images'] },
    }),
  ],
})
ts
// main.ts
import { createApp } from 'vue'
import { VImageKitPlugin } from '@macrulez/vue-image-kit'
import placeholders from 'virtual:vue-image-kit/placeholders'
import App from './App.vue'

createApp(App).use(VImageKitPlugin, { placeholders }).mount('#app')
vue
<template>
  <VImage v-for="item in items" :key="item.id" :src="item.image" :alt="item.title" />
</template>

What you get. Every image under public/images gets a HazeHash (a BlurHash when hazehash is not installed), a color and its size in the manifest, keyed by the URL it is served at (/images/cat.jpg). <VImage> finds its entry by the exact src, so the template needs no props at all. The manifest is rebuilt when a file in the folder changes and cached between builds.

Things to know.

  • A file under the public directory is keyed by its path from there. A folder outside it needs a urlPrefix — see the next recipe.
  • An image imported into a component has a hashed URL after the build and is never found by the manifest. Use the placeholder import for those.

Folders served from another URL ​

When to use it. The images are kept in the repository, but served from somewhere else — a CDN, an uploads route, a prefix added by a proxy.

ts
vueImageKit({
  generate: false,
  placeholders: {
    dirs: ['public/images', { dir: 'content/photos', urlPrefix: 'https://cdn.example.com/photos' }],
  },
})

What you get. Files in content/photos are keyed https://cdn.example.com/photos/<path>, which is exactly the src a template uses when it points at the CDN. Without a urlPrefix a folder outside public/ is keyed by its project path (/content/photos/a.jpg), which only matches the dev-server URL, and the build warns about it.

Remote and CDN images in the manifest ​

When to use it. The images live on another host and are not in the repository.

ts
vueImageKit({
  generate: false,
  placeholders: {
    urls: ['https://res.cloudinary.com/demo/image/upload/sample.jpg'],
    timeout: 20000,
  },
})

What you get. The plugin downloads each URL during the build and adds its placeholder to the manifest under that exact URL. A URL from a recognised CDN is fetched as a small rendition, and its size is read from the start of the original file. Results are cached, so a rebuild doesn't download again; set refreshRemote: true to force it.

A different kind of placeholder, and tuning ​

When to use it. The default HazeHash isn't what you want: a BlurHash or a ThumbHash, only a color for the lightest result, or a bigger HazeHash (tuning.budget).

ts
vueImageKit({
  generate: false,
  placeholders: {
    dirs: ['public/images'],
    mode: 'thumbhash',
    tuning: { components: [6, 4], sample: 128, color: 'average' },
  },
})
  • mode — 'hazehash' (default when the hazehash package is installed), 'blurhash' (default otherwise), 'thumbhash' or 'color'. It also sets what a bare ?placeholder import returns.
  • tuning.components — BlurHash components per axis, [4, 3] by default. More keep more detail and make the string longer.
  • tuning.sample — the size, in pixels, the image is downscaled to before hashing.
  • tuning.color — 'dominant' (the most frequent color) or 'average'.

Every placeholder the plugin computes uses the same settings, including ?placeholder imports. Changing them drops the cache. See Tuning the hashes.

Responsive variants for a folder ​

When to use it. You want WebP/AVIF files in several widths made ahead of time, and a srcset built from them.

ts
vueImageKit({
  input: './src/images',
  output: './public/images',
  widths: [400, 800, 1200],
  formats: ['webp', 'avif', 'jpg'],
  manifest: './src/assets/images.ts',
})
vue
<script setup lang="ts">
import { images } from './assets/images'

const hero = images.find((image) => image.name === 'hero')
</script>

<template>
  <VImage v-if="hero" :image="hero" alt="Hero" />
</template>

What you get. On every build the plugin resizes each image in input into output and writes a TypeScript manifest next to your code: an images array with one entry per source file, whose name is the file name without its extension. In vite dev it is incremental: changing one source file reprocesses just that file. All the options of the generate command are accepted.

Variants for one image ​

When to use it. A single hero image needs variants, and you don't want a folder-wide step.

vue
<script setup lang="ts">
import hero from './assets/hero.jpg?vik'
</script>

<template>
  <VImage :image="hero" alt="Hero" />
</template>

What you get. ?vik resizes that one file into output, and returns the full metadata — URLs, srcset, size, BlurHash and ThumbHash — which image accepts as it is. The variants go into the plugin's output folder (./public/images by default). Add generate: false when you don't want the rest of input processed as well:

ts
vueImageKit({ generate: false, output: './public/images', widths: [400, 800, 1200] })

Variants and a folder manifest together ​

When to use it. Some images get generated variants, others are served as they are and only need a blur.

ts
vueImageKit({
  input: './src/images',
  output: './public/images',
  widths: [400, 800, 1200],
  placeholders: { dirs: ['public/uploads'] },
})

What you get. The two jobs are independent: the batch run processes input, and the virtual manifest covers public/uploads. Register the manifest as in the folder recipe above.

Variants made on request in dev ​

When to use it. You don't want a build step while developing: images are resized on request and cached.

ts
vueImageKit({ generate: false, dev: { onDemand: true } })
html
<img src="/_vik/image?src=/photos/cat.jpg&w=800&format=webp" />

What you get. A handler mounted at /_vik/image during vite dev only. For production without a CDN, use the self-hosted server.