avicon
Tutorial

How to Add a Favicon to a React App (Vite and Create React App)

Where favicon files actually live in a Vite or CRA project, and how to wire up every size correctly.

· 4 min read

Quick answer: put your favicon files in the public/ folder and reference them with <link> tags in index.html — Vite and Create React App are plain client-side bundlers with no icon convention of their own, so favicon setup is just editing static HTML, the same as any HTML site.

It's worth saying this plainly because it surprises people coming from Next.js or another framework with file-based routing: neither Vite nor Create React App (CRA) has any special knowledge of favicons. There's no icon.png naming convention, nothing gets auto-detected, and nothing gets injected into <head> for you. You own index.html directly, and a favicon is just an asset in public/ referenced by a <link> tag, exactly like it would be on a hand-built HTML page.

That's actually simpler than it sounds — there's just one file to edit and one folder to drop images into. Here's exactly where everything goes.

Vite: replacing the default vite.svg

A freshly scaffolded Vite React project ships with a placeholder icon already wired up. You'll find vite.svg sitting in public/, referenced from index.html:

<link rel="icon" type="image/svg+xml" href="/vite.svg" />

To replace it, delete vite.svg, add your own favicon files to public/, and update the href (and type, if you're switching from SVG to PNG or ICO) to match. Anything in public/ gets copied to the output root untouched during a build, so a file at public/favicon.ico is served at /favicon.ico in both dev and production — no import, no bundler processing.

public/ vs. importing an icon from src/

Vite can technically also handle an icon imported from inside src/ and processed as a module, with a hashed filename and everything, the same way it handles an imported image in a component. For a favicon, don't bother. A favicon needs a stable, predictable URL, because it's requested directly by the browser and referenced by fixed paths in your manifest and touch-icon tags — a hashed build filename that changes on every deploy actively works against that. Keep favicon files in public/, referenced by a fixed path, and save the src/ import pattern for images that actually appear in your JSX.

Create React App: what's already there

CRA's default public/index.html references a few things out of the box:

<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

%PUBLIC_URL% is a CRA-specific placeholder that gets swapped for the correct base path at build time — leave it as-is even if your app isn't served from the domain root. The default manifest.json also lists logo192.png and logo512.png as manifest icons, which is why CRA scaffolds ship those two files in public/ alongside favicon.ico. Swap all three files for your own art at the same filenames and dimensions, or rename them and update both index.html and manifest.json to match.

A complete <head> example

Whichever tool you're using, the actual markup you need in <head> looks the same, because it's just standard HTML. A reasonably complete set covering tabs, iOS, and Android/PWA looks like this:

<head>
  <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" href="/apple-touch-icon.png" />
  <link rel="manifest" href="/site.webmanifest" />
</head>

Every file referenced here needs to physically exist in public/ (or %PUBLIC_URL%/ for CRA) with those exact names, or the browser requests will 404 silently in the background. If you're not sure what each of these tags is actually for, our full HTML favicon tag guide breaks down every line. The favicon converter generates this entire file set — ICO, both PNG sizes, the touch icon, and the manifest — from a single source logo, so you're not manually resizing five images by hand.

Nothing auto-injects tags for you

This is the core difference from a framework like Next.js: Vite and CRA are bundlers, not frameworks with an opinion about your <head>. Whatever's in index.html is exactly what ships, verbatim, with no metadata layer sitting on top generating or deduplicating tags for you. If you copy a favicon snippet from two different places, you can end up with duplicate or conflicting <link> entries, and nothing will warn you.

That also means per-route favicon or title changes (a common ask in multi-page SPAs) don't come free the way file-based conventions give you in other frameworks. If you need to swap metadata per route in a React app, a library like react-helmet (or react-helmet-async for concurrent rendering) lets you manage <head> tags declaratively from within your components — worth knowing about, though for a single static favicon set it's more machinery than most projects need.

Checking it actually works

Because both Vite and CRA serve public/ assets as static passthrough, testing is straightforward: run your dev server, open the Network tab, and confirm /favicon.ico and the other icon paths return 200 instead of 404. Browsers also cache favicons unusually aggressively, sometimes surviving what looks like a hard refresh, so if you swap the file and the tab still shows the old icon, that's very likely the cache and not your markup — our guide on stale favicon caching covers how to force it to update. For anything still not showing up correctly after that, run the deployed URL through a favicon checker to see exactly what's being served versus what you expect, or check common favicon-not-showing causes for the usual culprits.

Try the Converter

Already have a logo, icon or PNG? Drop it in, adjust the background and padding, and download a complete favicon package — .ico, PNGs, Apple touch, manifest and the HTML snippet.

Open Converter