Vite (create-vue) and the legacy Vue CLI template

Vue Favicon Generator

Generate the complete set, then drop it into public/ and edit the right index.html. create-vue and the older Vue CLI template disagree about where that file lives and whether the href needs a placeholder.

Drop an image
Drop a logo or image
PNG, JPG, WebP · square 512×512+ works best · non-square images are padded, never cropped · processed in your browser

Favicon Setup for Vue — Free, Fast & Private

Vue itself has no opinion about favicons — there is no Vue API for them, and no single-file component can declare one, because SFCs render into the body long after the head was parsed. Everything here is a property of the build tool underneath: Vite for anything scaffolded with create-vue, webpack for the older Vue CLI projects still in service.

Where each generated file goes in Vue

The generator above emits seven files with fixed names. Static assets in Vue live in public/, and the markup is written in index.html (create-vue) or public/index.html (Vue CLI). This is the mapping between the two.

Generated fileGoes toNotes
favicon.icopublic/favicon.icoReplaces the Vue logo favicon the scaffold ships.
favicon-16x16.pngpublic/favicon-16x16.pngExplicit link tag in index.html.
favicon-32x32.pngpublic/favicon-32x32.pngExplicit link tag in index.html.
favicon-48x48.pngpublic/favicon-48x48.pngOptional — the .ico already carries this size.
apple-touch-icon.pngpublic/apple-touch-icon.pngiOS home screen, 180×180.
android-chrome-192x192.pngpublic/android-chrome-192x192.pngManifest only.
android-chrome-512x512.pngpublic/android-chrome-512x512.pngManifest only, PWA splash.

create-vue: root index.html, absolute paths

A project scaffolded with npm create vue@latest is a Vite project with Vue plugins attached. index.html sits at the project root and is the true entry point of the build — Vite parses it, follows the module script into src/main.js, and rewrites what it needs to. public/ is the untouched-assets folder beside it, copied to dist/ verbatim.

That means you reference favicons with a plain absolute path and nothing clever happens to them. The scaffold already ships public/favicon.ico with the Vue logo and a single link tag pointing at it, so the minimum viable change is overwriting that one file. Adding the PNG tags is what actually earns you a sharp icon on a retina tab and a real icon on an iOS home screen.

Vite deliberately does not fingerprint public/ files, so the URLs are stable and the browser cache is durable. If you rebrand, the honest options are versioning the filename or appending a query string to the href — there is no build step that will do it for you.

index.html — create-vue / Vite

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48">
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
    <link rel="manifest" href="/site.webmanifest">
    <title>Your App</title>
  </head>
  <body><div id="app"></div><script type="module" src="/src/main.js"></script></body>
</html>

Vue CLI: public/index.html and the BASE_URL placeholder

Projects built on vue-cli-service keep index.html inside public/ and run it through an HTML template step before emitting it. The generated file uses an EJS-style placeholder for the deployment path, and the scaffolded favicon tag looks like href="<%= BASE_URL %>favicon.ico" rather than a bare slash. That placeholder resolves to the publicPath value from vue.config.js, which is why a Vue CLI build can be dropped into a subdirectory without every asset breaking.

The trap is copy-pasting a snippet from somewhere else into that file. A hardcoded /favicon.ico looks identical in local development, where publicPath is "/", and 404s the moment the app is deployed under a path. If your project has a public/index.html, keep the placeholder on every icon href.

Vue CLI is in maintenance mode and the Vue docs now point new projects at create-vue. If you are migrating, the favicon work is one of the easy parts: the files stay in public/, index.html moves up one directory, and every <%= BASE_URL %> becomes a plain leading slash.

public/index.html — Vue CLI template syntax

<link rel="icon" href="<%= BASE_URL %>favicon.ico" sizes="16x16 32x32 48x48">
<link rel="icon" type="image/png" sizes="32x32" href="<%= BASE_URL %>favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="<%= BASE_URL %>favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="<%= BASE_URL %>apple-touch-icon.png">
<link rel="manifest" href="<%= BASE_URL %>site.webmanifest">

When the icon has to change at runtime

Some Vue apps want a live favicon: an unread badge on a messaging view, a red dot while a job is failing, a light and dark variant that tracks the OS theme. None of that belongs in index.html. Reach for useHead from unhead — the same package Nuxt uses internally, and installable in a plain Vue app through @unhead/vue — or just mutate the existing link element, which is a couple of lines and no dependency.

Whichever route you take, leave a static tag in index.html. It is what the browser sees during the first paint, before your bundle has parsed, and on the many occasions when a bot or a link preview service fetches the HTML without executing any JavaScript at all.

useHead in a Vue component

import { useHead } from '@unhead/vue';
import { computed } from 'vue';

const unread = ref(0);
useHead({
  link: [{
    rel: 'icon',
    href: computed(() => (unread.value > 0 ? '/favicon-badge.png' : '/favicon-32x32.png')),
  }],
});

Vitepress, Nuxt, and other Vue-shaped things

Not everything written in Vue is a Vue app in the sense this page means. A VitePress documentation site has no index.html of yours to edit — you put the files in the .vitepress/public directory and declare the tags through the head array in .vitepress/config.js, because the HTML is generated per page from your markdown.

Nuxt is further away still: public/ is the right folder but the head is configured from nuxt.config.ts and from useHead calls, never from a checked-in HTML file. If your project has a nuxt.config file, follow the Nuxt instructions instead of these — the file placement happens to match, and everything about the markup does not.

Whatever you paste, the artwork itself never leaves your machine. The set above is rendered with the Canvas API and the .ico container is assembled in JavaScript on your device, so an unreleased logo stays unreleased. If the 16px preview looks like mush, that is the honest signal to simplify the mark before it ships to a Vue tab near you.

How it works

  1. Generate the set: Feed the tool above a square logo at 512px or larger. Everything is rendered locally in your browser.
  2. Drop the files in public/: Both create-vue and Vue CLI use public/. Overwrite the Vue logo favicon.ico the scaffold shipped.
  3. Find your index.html: create-vue: project root, use plain /paths. Vue CLI: public/index.html, keep the <%= BASE_URL %> placeholder on every href.
  4. Rebuild and check dist/: Confirm the seven files landed at the top level of dist/ and that the emitted HTML has real paths, not an unresolved placeholder.

Frequently asked questions

Where do I put the favicon in a Vue 3 project?
public/ at the project root. Vite copies that folder to dist/ verbatim without hashing, so public/favicon.ico ends up at /favicon.ico — the exact path browsers request by convention when nothing else tells them otherwise.
Why does my Vue index.html use <%= BASE_URL %>?
That is the Vue CLI template syntax, resolved at build time to the publicPath value from vue.config.js. It lets a build be deployed into a subdirectory without breaking every asset path. Projects scaffolded with create-vue have no such placeholder — they use plain absolute paths.
Can I set the favicon from a Vue component?
Not with plain template syntax. Components render into the body after the head has been parsed. Use useHead from @unhead/vue if you want it reactive, or mutate the existing link element directly. Either way, keep a static tag in index.html for the first paint.
My Vue app still shows the default Vue logo icon.
The scaffold ships public/favicon.ico with the Vue mark and a link tag already pointing at it. Overwriting that file is the fix. If the tab updated but an installed PWA did not, the manifest is still listing the old icons.
Which index.html do I edit?
If there is a file at the project root beside package.json, that is a Vite project and it is the one. If index.html only exists inside public/, that is Vue CLI and you edit the templated copy there. Editing the wrong one silently does nothing.
How do I show an unread badge on the favicon?
Bind the href through useHead to a computed value, or draw the badge onto a canvas at runtime and set the link href to the canvas data URL. Both approaches replace the same element; neither survives a hard reload, so the static tag remains your baseline.
Does this apply to VitePress and Nuxt?
Only partly. VitePress reads static files from .vitepress/public and takes head tags from the head array in its config. Nuxt uses public/ but configures the head in nuxt.config.ts. Both generate their HTML, so there is no index.html of yours to edit.

All Image Tools

Solutions by use case