How to set up a favicon in React (Vite and CRA)
React itself has almost nothing to do with favicons. A favicon is a set of static files plus a few <link> tags in the HTML shell that loads before React starts — which is good news: you set it up once, in one place, and every client-side route in your single-page app gets the same icon. What does differ is the build tool. Vite and Create React App (CRA) put that HTML shell and the static folder in different places and prefix asset URLs differently, and most favicon problems in React apps are really path problems from mixing the two recipes up.
So this guide splits cleanly: use the Vite section for new apps (Vite is the standard scaffold for a React single-page app today, and Create React App was officially deprecated in 2025), and the CRA section if you are maintaining an existing project. Both end up serving the same favicon package.
First, the package: the files every React app needs
Whichever tool you use, generate the same small set. One image in, these files out:
| File | Size | Used by |
|---|---|---|
favicon.svg | Scalable | Modern desktop browsers; the preferred tab icon, and the only format that can adapt to dark mode |
favicon-96x96.png | 96x96 | Fallback where SVG favicons are unsupported, and Google search results |
favicon.ico | 16x16, 32x32 and 48x48 in one file | Older browsers and tools, and non-HTML pages such as PDFs, which request /favicon.ico by convention |
apple-touch-icon.png | 180x180, opaque | iPhone and iPad home screens |
web-app-manifest-192x192.png | 192x192 | Android Chrome, via the web app manifest |
web-app-manifest-512x512.png | 512x512 | Android home screens, install prompts and splash screens |
Start from a square master of at least 512x512 (or your original SVG logo) so every export is scaled down, never up. Resizing all of this by hand is the tedious part, so generate it instead: the favicon generator for React produces exactly this package, names the files as above, assembles the manifest and writes the markup for your stack — entirely in your browser, so an unreleased logo never leaves your machine. Our favicon sizes and formats cheat sheet explains why this short list is enough and which legacy files you can skip.
Vite (new React apps)
1. Put the files in public/
A Vite React project — whether you scaffolded it with the react or react-ts template; TypeScript makes no difference here — has a public/ folder at the project root. Drop the whole package there, next to anything already in it:
public/
favicon.svg
favicon-96x96.png
favicon.ico
apple-touch-icon.png
web-app-manifest-192x192.png
web-app-manifest-512x512.png
site.webmanifest
Files in public/ are copied verbatim to the root of dist/ on every build and are not processed, hashed or bundled. Do not import your favicons from src/ for the <link> tags: an imported asset gets a hashed file name and becomes part of the JavaScript module graph, which is the wrong machinery for files the browser must find before any JavaScript runs. Keep the scaffold's default public/vite.svg out of the picture — once you replace its <link> tag in the next step, you can delete the file.
2. Add the markup to the root index.html
Note the location: in Vite, index.html sits at the project root, not inside public/. Add the tags to its <head>, replacing the default line that points at /vite.svg:
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="shortcut icon" href="/favicon.ico" sizes="32x32" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
Order matters: browsers use the first icon they support, so the SVG comes first and the ICO last. The paths start with / because that is where public/ contents are served — at the site root in development and in the production build alike. And because the tags live in index.html rather than in a component, the icon is present in the very first HTML response: no waiting for React to mount, and no need for React 19's metadata hoisting or a head-management library just for a favicon.
3. Deploying under a subpath? Use %BASE_URL%
Root-absolute paths like /favicon.svg break when the app is not served from the domain root — a GitHub Pages project site at example.com/my-app/ is the classic case. If you set a non-default base in vite.config.js, let Vite fill the prefix in for you. Vite replaces environment constants in index.html, including %BASE_URL%:
<link rel="icon" type="image/svg+xml" href="%BASE_URL%favicon.svg" />
Apply the same prefix to the other icon and manifest tags. If your app is served from the domain root (the usual case for a deployed SPA), plain /favicon.svg is correct and you can ignore this.
Create React App (existing apps)
Do not start a new project with CRA — it is deprecated and unmaintained — but if your app already runs on it, the favicon workflow still works fine and there is no need to migrate just for this.
1. Put the files in public/
As with Vite, the icons go in public/, alongside the existing index.html. CRA copies that folder into build/ untouched. Only files inside public/ can be referenced from public/index.html at all; files in src/ cannot, which is the CRA-era version of the same rule as above.
2. Add the markup to public/index.html, with %PUBLIC_URL%
CRA's difference is the prefix. Reference every icon through %PUBLIC_URL%, which CRA replaces at build time with the correct public path — including a non-root path from the homepage field in package.json, or a CDN origin if you serve static assets from one:
<link rel="icon" type="image/svg+xml" href="%PUBLIC_URL%/favicon.svg" />
<link rel="icon" type="image/png" href="%PUBLIC_URL%/favicon-96x96.png" sizes="96x96" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" sizes="32x32" />
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
Two clean-up jobs come with the CRA template. First, it already ships a favicon.ico and a <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> line — overwrite the file with your generated multi-size ICO rather than adding a second one. Second, the template's public/manifest.json points at the default logo192.png and logo512.png. Either replace those files with your own artwork at the same sizes, or update the manifest's icon entries to the generated 192x192 and 512x512 PNGs (next section) — leaving the React logo in there means Android home screens get the wrong icon even when your tab icon is perfect.
The web app manifest, whichever tool you use
Android does not take its home-screen icon from the <link> tags; Chromium browsers read the web app manifest you link to. With Vite it is conventionally public/site.webmanifest; CRA calls its default file public/manifest.json. Either name works — what matters is that the rel="manifest" tag points at it and its icon entries point at real files:
{
"name": "Your app name",
"short_name": "Your app",
"icons": [
{ "src": "/web-app-manifest-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "/web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Adjust the src paths to your deployment: under a subpath or with CRA's %PUBLIC_URL% setup, make them relative to where the manifest itself is served, or the icons will 404 even though the files exist. The maskable entry promises the icon survives Android's circular and squircle crops, so keep the essential artwork inside the central safe zone — roughly a circle 80% of the image's diameter — and let the background fill the rest. If you generate the package with the React favicon generator, this manifest comes written for you; just set your app's real name and theme colour.
Do you ever need to change the favicon from a component?
For a static brand icon: no. The tags in index.html cover every route, because a single-page app serves the same HTML shell for all of them. The one legitimate exception is a dynamic favicon — a notification badge on the icon, or a different icon per tenant. That has to happen in JavaScript, and a small effect is all it takes:
useEffect(() => {
const link = document.querySelector("link[rel~='icon']");
if (link) link.href = "/favicon-alert.svg";
}, []);
Treat that as an enhancement layered on top of the static tags, never a replacement for them: until React runs, the browser still needs the declared icon, and crawlers read the HTML, not the effect.
Build, preview, then check the live site
- Build and inspect the output. Run
npm run build, then look indist/(Vite) orbuild/(CRA): every icon, the manifest andindex.htmlshould be there, with the built HTML referencing the paths you expect. If a file is missing from the output, it was not inpublic/. - Preview the production build.
npm run preview(Vite) or a static server over the build folder shows what visitors actually get — the dev server can mask path mistakes that only appear oncebaseorhomepageapplies. - Open an icon URL directly. Visit
/favicon.svgon the running site. A 404 means a path, case-sensitivity (Linux hosts treatFavicon.svgas a different file) or subpath problem, not a React problem. - Check the deployed URL. Run your live address through the favicon checker: it fetches the page, validates every declared file and the manifest, and previews the icon in browser tabs, Google results, iOS and Android, so a wrong-sized or transparent-where-it-should-be-opaque touch icon shows up before your users see it.
When the old icon will not go away
If the tab still shows the previous icon — or Vite's lightning bolt, or CRA's atom — after a correct build, you are almost certainly fighting the favicon cache, which browsers keep separately from the page cache and a normal refresh does not clear. Test in a private window first: if the new icon appears there, your deployment is right. Then version the URLs (/favicon.svg?v=2) so the browser has to fetch fresh copies, and purge the icon URLs from your CDN if you use one. Google Search keeps its own copy on its own schedule and can take days to weeks to update even when everything is correct; our guide to fixing a favicon that is not showing up walks through that and the other causes in order.
The short version
Generate the standard package — SVG, 96x96 PNG, multi-size ICO, opaque 180x180 Apple touch icon, and 192x192/512x512 manifest icons — and put it in public/. In Vite, declare it in the project-root index.html with / paths (or %BASE_URL% under a subpath); in an existing CRA app, declare it in public/index.html with %PUBLIC_URL% and clean the template's default icons out of its manifest. Nothing belongs in a component, and nothing needs importing. Build, preview, and run the result through the favicon checker — and if you would rather not assemble the package by hand, the favicon generator for React produces the files, the manifest and the exact markup for your tool in one download.