Your images are processed in your browser — Cloud HD (Pro, opt-in) is the only exception
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, GIF · square 512×512+ works best · non-square images are padded, never cropped · processed in your browser

Favicon Setup for VueFree, 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 eight files with fixed names, site.webmanifest included. 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.
site.webmanifestpublic/site.webmanifestThe file the last two exist for. Copied to dist/ verbatim like the icons.

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.

The manifest — no Vue scaffold creates one, so the generator does

The last line of both blocks above links a manifest, and neither create-vue nor Vue CLI generates that file. Ship the link tag without the file and every page load requests site.webmanifest and gets a 404 — or, on a host with a SPA fallback rule, gets index.html back and the browser fails to parse HTML as JSON. The generator above downloads it with the icons: put it in public/ beside them, change name and short_name, and the tag has something to resolve to. It is copied to dist/ verbatim like everything else in that folder.

This is what the two android-chrome PNGs are for. Nothing links them from HTML, so a project without a manifest ships two files no browser ever requests, and the installed app on Android gets a generic icon and a blank splash screen.

Vue CLI users should note what the relative src values in that file are doing, because this is the one place the BASE_URL machinery cannot help you. A manifest is a static JSON file and no placeholder is ever substituted inside one, so a publicPath of "/app/" leaves a hardcoded "/android-chrome-192x192.png" pointing at the domain root while the manifest itself resolves perfectly — which is why this failure only shows up at install time on a real Android device. A relative path is resolved against the manifest's own URL instead, so it inherits whatever prefix the manifest was served under. Keep the manifest in public/ with the icons and publicPath stops mattering.

public/site.webmanifest — as downloaded, edit name and short_name

{
  "name": "Your App",
  "short_name": "App",
  "start_url": ".",
  "display": "standalone",
  "icons": [
    {
      "src": "android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

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. Drop site.webmanifest in public/: No Vue scaffold ships one and the link tag above 404s without it, so the generator downloads it. Put it beside the icons and edit name and short_name. Its relative icon paths mean a Vue CLI publicPath needs no hand-written prefix.
  5. Rebuild and check dist/: Confirm the eight 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.
Do I need to write site.webmanifest myself?
No. Neither create-vue nor Vue CLI generates one, so the tool above downloads it with the icons — copy it into public/ and change name and short_name. Its icon paths are relative, so unlike a hand-written manifest it survives a Vue CLI publicPath other than "/" without you typing the prefix in, as long as it sits in the same folder as the PNGs.
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

AI Tools

Solutions by use case