React

Favicons in React — Vite, Create React App, and everything else

React itself has no favicon convention. Your bundler does. This guide covers the modern setup for Vite-powered React, Create React App, and framework-less setups.

React is a library, not a framework. It has no favicon convention. Your bundler does. This guide covers the three setups in use today: Vite (the modern default), Create React App (legacy but still alive), and framework-less or other bundlers.

Vite projects have a /public folder at the project root. Drop favicons there:

public/
├── favicon.svg
├── favicon.ico
├── favicon-96x96.png
├── apple-touch-icon.png
├── web-app-manifest-192x192.png
├── web-app-manifest-512x512.png
└── site.webmanifest

Link them from index.html (also at the project root):

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png" />
    <link rel="shortcut icon" href="/favicon.ico" />
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
    <link rel="manifest" href="/site.webmanifest" />
    <meta name="theme-color" content="#0a0a0a" />
    <title>My App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Create React App (legacy)

CRA’s /public folder behaves the same as Vite’s. Same file layout, same link tags, same paths. Edit /public/index.html:

<link rel="icon" type="image/svg+xml" href="%PUBLIC_URL%/favicon.svg" />
<link rel="icon" type="image/png" sizes="96x96" href="%PUBLIC_URL%/favicon-96x96.png" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png" />
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />

%PUBLIC_URL% is CRA’s placeholder for the public base path. It gets replaced at build time. Absolute /favicon.svg works too if your app is always deployed at the domain root.

CRA is on maintenance. New React projects should use Vite, Next.js, Remix, or TanStack Start.

Deploying under a subpath

If your app is served from a subpath like https://example.com/app/, favicon paths need the subpath prefix. Vite:

// vite.config.js
export default defineConfig({
  base: "/app/",
});

With base set, Vite rewrites absolute paths in index.html to include the prefix. Your <link rel="icon" href="/favicon.svg" /> becomes /app/favicon.svg in the built HTML.

For client-side references inside components, use import.meta.env.BASE_URL:

<img src={`${import.meta.env.BASE_URL}favicon.svg`} alt="" />

Favicons themselves do not need this because they are referenced from index.html, which Vite processes.

Why not import the favicon in a component

Do not write:

import faviconUrl from "./assets/favicon.svg";
<link rel="icon" href={faviconUrl} />

This bundles the favicon with a content hash (favicon.abc123.svg). The browser caches favicons per-hostname, so a hash-rotating URL defeats caching and changes on every build. Favicons need stable paths. Put them in /public and reference by absolute path.

The exception is if your app is entirely client-rendered and you want favicon hash-busting on deploy. Even then, query strings on the <link> tag work better (/favicon.svg?v=2) than import-based hashing.

Common pitfalls

  • Broken paths after subpath deploy. Forgot base in vite.config.js.
  • Missing apple-touch-icon. CRA and Vite ship with only favicon.ico by default. The full modern set has five files plus a manifest.
  • Raw <head> access in React. React itself does not manage the document head. Use react-helmet-async only for per-page titles and metadata, not for favicons. Favicons belong in index.html.

Generate the set

Drop your logo SVG into Faviconry, unzip the download into /public, paste the five <link> tags into index.html. Works identically in Vite, CRA, and any bundler-less setup.

Frequently asked questions

Why does my favicon path break in production but work in dev?

Vite and CRA serve the /public folder at the root in dev and at the base URL in production. If you deploy under a subpath like /app/ without setting the base option, absolute paths like /favicon.svg break. Use the BASE_URL variable or set base in vite.config.js.

Where do favicons go in a Vite React project?

The /public folder. Files there are served at the root in dev and copied verbatim into /dist on build. Reference them with absolute paths in your index.html. No imports, no hashing.

Can I import favicons as ES modules?

You can but should not. Importing a favicon into a component bundles it with a content hash, so the path changes on every build. Favicons need stable root paths. Keep them in /public and link them from index.html.