Vite and Create React App — the two layouts differ

React Favicon Generator

One set of files, two project layouts. Vite puts index.html at the project root and takes plain absolute paths; Create React App puts it in public/ and needs the %PUBLIC_URL% prefix. Both are covered below.

Drop an image
Drop a logo or image
PNG, JPG, WebP · square 512×512+ works best · non-square images are padded, never cropped · processed in your browser

Favicon Setup for React — Free, Fast & Private

Both toolchains use a folder called public/, and both serve its contents from the URL root without hashing or transforming them. The difference is where index.html lives — and in CRA, that the href needs a build-time placeholder rather than a plain slash.

Where each generated file goes in React

The generator above emits seven files with fixed names. Static assets in React live in public/, and the markup is written in index.html (Vite) or public/index.html (CRA). This is the mapping between the two.

Generated fileGoes toNotes
favicon.icopublic/favicon.icoOverwrite the React logo one the starter shipped.
favicon-16x16.pngpublic/favicon-16x16.pngLinked explicitly from the head.
favicon-32x32.pngpublic/favicon-32x32.pngLinked explicitly from the head.
favicon-48x48.pngpublic/favicon-48x48.pngOptional loose copy — already bundled inside the .ico.
apple-touch-icon.pngpublic/apple-touch-icon.pngiOS home-screen bookmark, 180×180.
android-chrome-192x192.pngpublic/android-chrome-192x192.pngReferenced from manifest.json, not from HTML.
android-chrome-512x512.pngpublic/android-chrome-512x512.pngReferenced from manifest.json, not from HTML.

Vite: index.html sits above public/, not inside it

The single most confusing thing about a Vite React project for anyone arriving from Create React App is that index.html is not in public/. It is at the project root, a sibling of public/ and src/, because Vite treats it as the actual entry point of the build graph rather than a template to be copied. You edit that file directly and reference public assets with a plain absolute path starting with a slash.

public/ itself behaves the way you would hope: files are copied to the build output root byte-for-byte, with no fingerprinting and no transformation. That is a deliberate escape hatch for assets whose exact filename matters, which is precisely the favicon case — /favicon.ico has to be at that literal path because browsers request it by convention even when no link tag mentions it.

The trade is that you get no cache busting. Import an image from src/ and Vite hashes it; drop one in public/ and the URL is stable forever. When you change your logo, returning visitors keep seeing the old tab icon until their cache gives up. Live with it, or append a version query string in the link tag and bump it on redesigns.

index.html — project root, plain absolute paths

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />
    <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>Your App</title>
  </head>
  <body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body>
</html>

Create React App: public/index.html and the %PUBLIC_URL% prefix

CRA keeps index.html inside public/ and treats it as a template. Before writing the built HTML it substitutes %PUBLIC_URL% for whatever the app is actually served from, which is the homepage field in package.json or the PUBLIC_URL environment variable. That indirection exists so a CRA build can be dropped into a subdirectory without every asset path breaking, and it means a bare href="/favicon.ico" is wrong in a CRA project deployed anywhere other than the domain root.

A fresh CRA project ships three files you should delete: favicon.ico with the React atom, logo192.png and logo512.png. The generated manifest.json points at those last two, so replace the entries there as well or you will ship an installable app whose home-screen icon is still the React logo. This is the single most common half-finished favicon swap in React projects.

CRA has been unmaintained for a while and its docs still recommend a longer legacy icon list. Ignore that. The seven files from this generator cover every platform that currently requests anything, and the shorter head keeps the render-blocking work down.

public/index.html — CRA, with the build-time placeholder

<link rel="icon" href="%PUBLIC_URL%/favicon.ico" sizes="16x16 32x32 48x48" />
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

One HTML document, every route

A React SPA ships exactly one HTML file. React Router, TanStack Router and every other client-side router swap components underneath a shell that was already sent to the browser, so the head you wrote in index.html is the head for every URL in your app. There is no per-route favicon to configure and no reason to reach for react-helmet-async to set one.

Where a helmet library does earn its place is a favicon that changes at runtime — an unread-count badge, a red dot during a build failure, a light and dark variant. That is a DOM mutation, not a build concern: find the existing link element and repoint its href. Keep a canonical tag in index.html as the fallback so the icon is correct before your JavaScript has parsed.

Swapping the icon at runtime

function setFavicon(href) {
  const link = document.querySelector("link[rel~='icon']");
  if (link) link.href = href;
}

// e.g. respond to the user's colour scheme
const dark = window.matchMedia('(prefers-color-scheme: dark)');
setFavicon(dark.matches ? '/favicon-dark.ico' : '/favicon.ico');

What to check after the build

Run the production build and inspect the output directory rather than trusting the dev server. In Vite that is dist/, in CRA it is build/. The seven files should be sitting at the top level of that folder, and the emitted index.html should contain your link tags with the paths already resolved — no leftover %PUBLIC_URL% strings, which would mean the template was copied rather than processed.

Then serve the build locally and request /favicon.ico directly in the address bar. That single check catches the two failure modes that matter: a file that never got copied, and a host rewriting unknown paths to index.html so the browser receives HTML where it expected an icon and quietly falls back to the grey globe.

Whatever you paste, the artwork itself never leaves your machine. The set above is rendered with the Canvas API and the .ico container is assembled in JavaScript on your device, so an unreleased logo stays unreleased. If the 16px preview looks like mush, that is the honest signal to simplify the mark before it ships to a React tab near you.

How it works

  1. Generate the set: Drop a square 512px PNG into the tool above and download favicon.ico plus the six PNGs.
  2. Copy into public/: Both Vite and CRA use public/ at the project root. Overwrite the starter favicon.ico while you are there.
  3. Edit the right index.html: Vite: the one at the project root, plain /paths. CRA: public/index.html, with the %PUBLIC_URL% prefix on every href.
  4. Fix the manifest: CRA ships manifest.json pointing at logo192.png and logo512.png. Repoint it at the android-chrome PNGs and delete the React logos.

Frequently asked questions

Where does the favicon go in a React project?
public/ at the project root, in both Vite and Create React App. Files there are copied to the build output root untouched, so public/favicon.ico is served at /favicon.ico — the path browsers request by convention regardless of what your link tags say.
Why is index.html not in public/ in my Vite project?
Vite treats index.html as the entry point of the build graph, not a template, so it lives at the project root beside public/ and src/. You edit it directly and reference public assets with a plain absolute path. CRA is the one that keeps it inside public/.
What is %PUBLIC_URL% and do I need it?
It is a Create React App build-time placeholder replaced with the path the app is served from, taken from the homepage field in package.json or the PUBLIC_URL environment variable. In CRA you need it; a bare /favicon.ico breaks as soon as the app is deployed into a subdirectory. In Vite it does not exist.
My React app still shows the React logo in the tab.
Two files are usually left over. Delete public/favicon.ico from the starter and overwrite it with yours, then check manifest.json — CRA generates it pointing at logo192.png and logo512.png, so the installed app keeps the React icon even after the tab is fixed.
Do I need react-helmet for a favicon?
No. A React SPA serves one HTML document for every route, so the tags in index.html already apply everywhere. A helmet library is only worth it if the icon has to change at runtime, and even then a direct DOM update on the existing link element is simpler.
How do I change the favicon dynamically in React?
Query the existing element with document.querySelector("link[rel~='icon']") and set its href. Keep a static tag in index.html as the pre-JavaScript fallback so the correct icon is already showing before your bundle parses.
Why does the favicon work in dev but not after deploying?
Usually the host. Request /favicon.ico directly on the deployed site: if you get HTML back, an SPA fallback rule is rewriting unknown paths to index.html and the browser is receiving a document where it expected an image. Exclude static file extensions from that rewrite.

All Image Tools

Solutions by use case