Where the files go

Vite serves everything in public/ at the site root without processing it. That is exactly what favicons need, because /favicon.ico has to answer at that literal path.

public/
  favicon.ico
  icon.svg
  favicon-96x96.png
  apple-touch-icon.png
  icon-192.png
  icon-512.png
  icon-mask.png
  manifest.webmanifest

The index.html snippet

Replace the starter's vite.svg link in the <head> of index.html with:

<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon-96x96.png" type="image/png" sizes="96x96">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/manifest.webmanifest">

Do not import favicons through src/

Anything imported from src/ gets a content hash in its filename at build time, such as favicon-3f9a1c.ico. That is right for app assets, but crawlers and feed readers request /favicon.ico directly and would get a 404. Keep favicons in public/ and reference them with root-relative paths.

Any framework on Vite

The setup is the same for React, Vue, Svelte, Solid and Preact apps built with Vite, because the favicon lives in index.html, not in components. For a React-specific overview, including Next.js and React Router, see React.

Questions

Why does /favicon.ico 404 in my Vite build?

Usually because it was placed in src/ or assets/ and got a hashed filename. Move it to public/.

Do I need to restart the dev server?

Files in public/ are served as they are, but browsers cache favicons hard. Hard-reload, or add ?v=2 to the link while testing.

My app is deployed under a sub-path. Does that change anything?

The tab icons can follow your base path, but /favicon.ico at the domain root is still what crawlers request. If you control the root, put a copy there.