Vite and React SPAs serve favicons from the public/ folder and declare them in index.html. Unlike Next.js, there is no metadata API. You hand-write link tags or use a PWA plugin. Because SPAs serve the same index.html for every route, root-relative paths usually work, but base URL and deploy path need attention.
This guide covers the standard Vite setup, PWA manifest icons, React Router edge cases, and how to verify icons on your built and deployed app.
Quick answer: public folder + index.html
Default Vite structure:
my-app/
public/
favicon.ico
favicon-32x32.png
favicon-16x16.png
apple-touch-icon.png
site.webmanifest
index.html
src/
main.tsx
Files in public/ copy to the build root unchanged. Reference them from index.html:
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <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>My App</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body></html>Run vite build. Icons appear at /favicon-32x32.png on your deployed origin.
Why index.html matters for SPAs
Crawlers and browsers read index.html before JavaScript runs. Putting favicon logic only in React (react-helmet, useEffect DOM writes) means:
- Tags may not exist in the initial HTML response
- Some tools never execute JS and miss your icons
Rule: Declare favicons in index.html. Use React head libraries only for per-route title or OG tags that truly change, not for the site-wide favicon.
Vite base option and subdirectory deploys
If vite.config.ts sets a non-root base:
export default defineConfig({ base: '/my-app/',})Icon paths must include the base:
<link rel="icon" href="/my-app/favicon.ico" />Or use Vite's %BASE_URL% placeholder in HTML (supported in processed index.html):
<link rel="icon" href="%BASE_URL%favicon.ico" />After build, confirm paths in dist/index.html. A wrong base causes 404s on every icon. Scan the deployed URL with Favicon Check to catch this instantly.
PWA manifest with vite-plugin-pwa
For installable apps, use @vitejs/plugin-pwa:
// vite.config.tsimport { VitePWA } from 'vite-plugin-pwa'export default defineConfig({ plugins: [ VitePWA({ registerType: 'autoUpdate', manifest: { name: 'My App', icons: [ { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png', }, { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png', }, ], }, }), ],})Place pwa-192x192.png and pwa-512x512.png in public/. The plugin generates manifest and service worker references at build time.
Tab favicon and PWA icons are separate surfaces. You need both for full coverage. Sizes: Favicon Sizes Guide.
React Router and client-side routing
React Router does not change favicon paths if you use root-relative /favicon.ico. Icons load from the origin, not the route.
Watch out for:
- Hash routers (
#/about) - favicons still load from origin root, no special config - Browser router on static hosts - server must redirect all paths to
index.html(Netlify_redirects, Vercel rewrites, nginxtry_files) - Wrong relative paths -
href="favicon.ico"breaks on/dashboard/settingsbecause the browser requests/dashboard/favicon.ico
Always use root-relative paths starting with /.
Dynamic favicons in React (when needed)
Rare cases: unread badge, user avatar as icon. Change at runtime:
useEffect(() => { const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement if (link) { link.href = '/notification-favicon.png' }}, [hasUnread])Keep a default favicon in index.html as fallback before JS loads. Do not rely on runtime-only tags for launch testing.
Create React App vs Vite
CRA uses the same public/ + index.html pattern. Migration to Vite:
- Move
public/assets unchanged - Copy
linktags from CRAindex.htmlto Viteindex.html - Remove
%PUBLIC_URL%prefixes (use/or%BASE_URL%) - Re-test production build
Common Vite/React favicon mistakes
Icons in src/assets/ without import
Files in src/ get hashed filenames unless imported. Favicons should live in public/ for stable URLs like /favicon.ico.
Missing favicon.ico
Browsers request /favicon.ico automatically. Include it in public/ even when PNG tags exist.
Only 32×32 PNG
Add 16×16, 180×180 apple-touch-icon, and manifest sizes for mobile. One file is not enough for home screen installs.
Forgetting rebuild after icon swap
Vite copies public/ at build time. Changing icons locally without vite build and redeploy leaves production stale.
Helmet duplicates
react-helmet-async adding favicon on every route can duplicate tags. Set favicon once in index.html.
Testing Vite/React favicons
Local preview
npm run buildnpm run previewOpen http://localhost:4173 (default preview port). Check tab icon and View Source for tags.
Production scan
Deploy dist/ to your host. Run Favicon Check on the live URL.
Verify:
- Each icon URL returns 200
/favicon.icoreachable- Manifest valid if using PWA plugin
Full workflow: How to Test Favicons Before Launch.
Open Graph separately
SPAs often inject OG tags client-side, which crawlers miss. Favicon in index.html is fine; OG tags should be server-rendered or prerendered. Test shares with Open Graph Check.
Production file checklist
public/
favicon.ico
favicon-16x16.png
favicon-32x32.png
apple-touch-icon.png
android-chrome-192x192.png # if manifest
android-chrome-512x512.png
site.webmanifest
index.html # all link tags
vite.config.ts # base + PWA plugin if needed
HTML baseline reference: How to Add a Favicon (HTML).
Netlify, Cloudflare Pages, and AWS S3 deploys
Static hosts serve dist/ as-is. Common issues:
Netlify: _redirects must not catch icon paths and return HTML. Add explicit rules if SPA fallback is too broad:
/favicon.ico 200
/favicon-32x32.png 200
Cloudflare Pages: Same SPA fallback risk. Verify icon files exist in build output, not only in repo root.
S3 + CloudFront: Upload public/ assets with correct Content-Type. ICO files served as application/octet-stream confuse some clients. Set image/x-icon metadata on S3 objects.
After any static host deploy, scan the live URL. Build logs do not confirm icons are reachable from the public internet.
Environment variables and multi-brand SPAs
White-label React apps sometimes swap favicons per tenant at runtime. For launch testing:
- Test each tenant subdomain separately
- Keep default favicon in
index.htmlbefore tenant JS loads - Document which tenants use custom icons in your audit checklist
Runtime swaps are valid but harder to QA. Automated scans should run per tenant URL, not only the main marketing domain.
Turborepo and shared asset packages
In monorepos, favicon files sometimes live in a shared @brand/assets package. Ensure Vite copies them into public/ during build, not only imports them from src/.
Pattern that works:
packages/assets/icons/ # source of truth
apps/web/public/ # copied at build via script
Add a prebuild step:
"scripts": { "prebuild": "cp -r ../packages/assets/icons/* public/"}Forgotten copy scripts cause CI builds without icons even when local dev symlinks work.
Vitest and E2E smoke tests
Add a minimal E2E assertion after deploy preview:
test('favicon.ico returns 200', async () => { const res = await fetch(`${baseUrl}/favicon.ico`) expect(res.status).toBe(200)})Pair with HTML parse for link[rel="icon"]. Visual regression on 16×16 pixels is optional; HTTP coverage catches most deploy mistakes.
Lighthouse and Core Web Vitals note
Favicon requests are tiny and rarely affect LCP or CLS scores. Do not skip proper icon setup for performance reasons. The bytes are negligible compared to hero images and JavaScript bundles.
Do prioritize correct Content-Type headers. Mislabeled ICO files cause retry behavior in some browsers, which adds noise when debugging network waterfalls.
Quick reference: index.html head block
Copy this starter block into Vite index.html and replace filenames:
<link rel="icon" href="/favicon.ico" sizes="any"><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">After first deploy, validate with Favicon Check and expand only if the scan reports gaps.
Keep a public/ README for designers listing required filenames. That reduces back-and-forth when marketing drops a new logo pack into Slack without size variants.
Document your base value in the same README if you deploy to GitHub Pages or a subdirectory. Future you will forget why icons 404 in production.
FAQ
Where do I put favicon in Vite React?
In public/ at project root. Reference from index.html with root-relative paths like /favicon.ico.
Does Vite process favicon files?
Files in public/ copy as-is to dist/ root. No bundling or hashing. Stable URLs for favicons.
How do I add favicon to Vite PWA?
Use @vitejs/plugin-pwa manifest icons plus standard link tags in index.html for browser tabs.
Why is favicon 404 on GitHub Pages?
GitHub Pages project sites use subdirectory base (/repo-name/). Set base in vite.config.ts and update icon hrefs accordingly.
Can I use SVG favicon in Vite?
Yes. Put favicon.svg in public/ and add <link rel="icon" type="image/svg+xml" href="/favicon.svg">. Ship PNG fallback for Safari.
React Helmet for favicon?
Possible but not recommended for site-wide icons. Use index.html for stable server-delivered tags.
Why blurry favicon in React app?
Wrong source dimensions or upscaled small PNG. Export proper sizes. See Favicon Looks Blurry.
Favicon vs OG image in React SPA?
Different tags, different files. Favicon in index.html. OG needs SSR/prerender for reliable shares. See Favicon vs Open Graph Image.
Conclusion
Vite React favicons belong in public/ with link tags in index.html. Set base correctly for subdirectory deploys, add manifest icons for PWA, and avoid client-only favicon injection.
Build, deploy, and scan with Favicon Check. For share metadata, fix OG tags separately and validate with Open Graph Check.