You scaffold a fresh Vue 3 project with Vite, fire up the dev server, and check the browser tab. There's the default Vite logo sitting where your brand icon should be. The fix lives in your project's public/ folder, and wiring it correctly takes about three minutes once you know the Vite-specific gotchas.

New to favicons in Vite? Our Vite favicon guide covers the general setup; this page walks through the Vue project layout step by step.

Prerequisites

You'll need a Vue 3 project running on Vite, plus the favicon files you want to drop in. If you don't have them yet, generate a complete bundle (ICO, PNG, and SVG) using Mzu favicondl and download it ready to use. No webpack quirks, no copy-webpack-plugin rules — Vite handles the public folder as static assets, no configuration needed.

Step 1: Open the public/ Folder

Every Vue + Vite scaffold ships with a public/ directory at the project root. According to Vite's documentation on the public directory, anything you place here is copied to the build output unchanged and served at the root path.

That means public/favicon.ico is reachable as /favicon.ico, never /public/favicon.ico. Missing that distinction is the single most common reason Vue apps ship a broken tab icon to production.

Step 2: Drop In Your Favicon Files

Place your entire favicon package directly into public/:

public/
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
├── site.webmanifest
└── favicon.svg

Vite copies the whole folder to your dist/ output as-is — no imports, no transforms. Don't import these files from any Vue component; they're meant to stay outside the bundle. The complete breakdown of which sizes actually matter today lives in our Favicon Sizes Guide.

Step 3: Add link Tags to index.html

Open index.html at the project root — the same file where Vite mounts your Vue app. Add link rel="icon" declarations inside <head>:

<head>
  <meta charset="UTF-8" />
  <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
  <link rel="icon" href="/favicon-32x32.png" type="image/png" sizes="32x32" />
  <link rel="icon" href="/favicon.ico" sizes="any" />
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
  <link rel="manifest" href="/site.webmanifest" />
</head>

Order matters here. Browsers pick the first rel="icon" entry they support, so place the SVG first for crisp scaling, PNG fallbacks next, and the legacy ICO last. All paths start with /, never /public/.

Step 4: Add Dark Mode SVG Support

If you want a dark mode swap, drop the media query inside the SVG itself rather than wiring up Vue reactivity for it:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    circle { fill: #1a1a1a; }
    @media (prefers-color-scheme: dark) {
      circle { fill: #fafafa; }
    }
  </style>
  <circle cx="16" cy="16" r="14" />
</svg>

Browsers swap the fill automatically — no watch() calls, no lifecycle hooks needed in the tab. For a deeper dive into the patterns, check our Favicon Dark Mode Guide.

Step 5: Verify in Dev and Production

Run npm run dev and confirm three things:

  1. DevTools → Network → filter "favicon" shows every file returning 200 OK.
  2. The browser tab shows your icon, not the default Vite mark.
  3. curl http://localhost:5173/favicon.svg returns the SVG bytes.

Then run npm run build and inspect dist/. Every file from public/ should appear at the root of dist/, unchanged. If something is missing in production, your hosting's rewrite rules are likely stripping the path — check vercel.json or _redirects.

Common Pitfalls

Wrapping Up

The Vue + Vite public folder workflow is one of the cleanest favicon setups in modern web dev. Drop your files, wire up the link tags in index.html, and Vite copies everything to dist/ exactly where you placed it. No plugin config, no head injection quirks, no Vue Router intercepting your requests — just static assets served from the root path.

Generate a complete favicon package with Mzu favicondl, paste the link tags into your index.html, and your Vue app will look polished in every browser tab.