Platform Guides

React Favicon Setup: Complete Implementation Guide

How to properly add favicons to your React application using Vite. Covers public directory setup and HTML configuration.

5 min read
Free Guide

React Favicons: Simpler Than You Think

Here's a secret: adding favicons to a React app is refreshingly straightforward. No webpack configs, no complex build processes - just put files in the right place and reference them correctly. Whichever build tool you're on, the principles are the same.

This guide targets Vite, which is the default choice for a client-side React SPA these days, with Next.js the usual pick for full-stack React apps. The React team deprecated Create React App on 14 February 2025 and now points new projects at a framework or at a build tool such as Vite. If you're still on a CRA project, everything below still applies - the file layout is nearly identical, and there's a section further down on what changes when you move across.

The Public Directory Method

Basic Setup (Vite)

In a Vite-powered React app, your project structure looks like this:

my-react-app/
├── public/
│   └── favicon.ico
├── src/
└── index.html

Simply place your favicon in the public directory and reference it in your index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="icon" type="image/x-icon" href="/favicon.ico">
    <title>My React App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

The leading slash is crucial - it ensures the favicon works regardless of your routing.

Multiple Favicon Sizes

For comprehensive device support, add multiple files:

public/
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
└── android-chrome-192x192.png

Update your index.html:

<head>
  <!-- Standard favicons -->
  <link rel="icon" type="image/x-icon" href="/favicon.ico">
  <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
  <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">

  <!-- Apple devices -->
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">

  <!-- Android devices -->
  <link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png">
</head>

Dynamic Favicons in React

Want to change favicons based on app state? Here's how:

React Hook for Dynamic Favicons

// hooks/useFavicon.ts
import { useEffect } from 'react';

export const useFavicon = (href: string) => {
  useEffect(() => {
    const link: HTMLLinkElement =
      document.querySelector("link[rel*='icon']") ||
      document.createElement('link');

    link.type = 'image/x-icon';
    link.rel = 'shortcut icon';
    link.href = href;

    document.getElementsByTagName('head')[0].appendChild(link);
  }, [href]);
};

Usage in your app:

// App.tsx
import { useFavicon } from './hooks/useFavicon';

function App() {
  const hasNotifications = useNotificationCount() > 0;

  useFavicon(hasNotifications ? '/favicon-alert.ico' : '/favicon.ico');

  return <YourApp />;
}

Environment-Based Favicons

Show different favicons for dev/staging/production:

// App.tsx
const getFaviconByEnv = () => {
  if (import.meta.env.DEV) return '/favicon-dev.ico';
  if (import.meta.env.VITE_APP_ENV === 'staging') return '/favicon-staging.ico';
  return '/favicon.ico';
};

function App() {
  useFavicon(getFaviconByEnv());
  // ...
}

SVG Favicons

SVG favicons offer perfect scaling and tiny file sizes:

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

Create a simple SVG favicon:

<!-- public/favicon.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <rect width="32" height="32" rx="4" fill="#000"/>
  <text x="16" y="24" text-anchor="middle" fill="#fff" font-family="Arial" font-size="20">R</text>
</svg>

Dark Mode Support with SVG

SVG favicons can adapt to dark mode:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    rect { fill: #000; }
    text { fill: #fff; }
    @media (prefers-color-scheme: dark) {
      rect { fill: #fff; }
      text { fill: #000; }
    }
  </style>
  <rect width="32" height="32" rx="4"/>
  <text x="16" y="24" text-anchor="middle" font-family="Arial" font-size="20">R</text>
</svg>

Web App Manifest

For PWA capabilities, add a manifest:

// public/manifest.json
{
  "name": "My React App",
  "short_name": "React App",
  "icons": [
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

Link it in your HTML:

<link rel="manifest" href="/manifest.json">

If You're Still on Create React App

React deprecated Create React App on 14 February 2025. It's in maintenance mode, not development - a final release shipped for React 19, new installs print a deprecation warning, and the official guidance is to move to a framework or to a build tool like Vite. So treat this section as a migration note rather than a setup guide.

A CRA project keeps the same files, in the same place:

public/
├── favicon.ico          # Automatically linked
├── logo192.png          # PWA icon
├── logo512.png          # PWA icon
├── manifest.json        # PWA manifest
└── index.html           # References icons via %PUBLIC_URL%

Replace the default files with your own and CRA picks them up. The one thing that trips people up on the way out is the placeholder: CRA writes %PUBLIC_URL%/favicon.ico into public/index.html, and Vite doesn't substitute it. When you migrate:

  1. 1Move index.html from public/ to the project root
  2. 2Replace every %PUBLIC_URL%/ with a plain / - so /favicon.ico, /logo192.png, /manifest.json
  3. 3Add <script type="module" src="/src/main.tsx"></script> before the closing body tag
  4. 4Leave the icon files where they are - public/ means the same thing in both tools

Your manifest.json needs no changes at all, as long as the src values start with a slash.

Handling Cache Issues

Favicons are aggressively cached. Force updates with cache busting:

<link rel="icon" href="/favicon.ico?v=2">

Or programmatically:

const updateFavicon = (href: string) => {
  const link = document.querySelector("link[rel*='icon']") as HTMLLinkElement;
  link.href = `${href}?v=${Date.now()}`;
};

Testing Your Favicons

Development Testing

Production Testing

# Build and preview locally
npm run build
npm run preview
  1. 1Check the Network tab: Ensure favicon loads without 404s
  2. 2Test different routes: React Router shouldn't break favicon paths
  3. 3Try incognito mode: Bypasses aggressive caching

Check that all favicon paths resolve correctly in the production build.

Common Pitfalls and Solutions

Favicon Not Showing?

Works in Dev, Not in Production?

  • Missing leading slash: Use /favicon.ico, not favicon.ico
  • Wrong public path: Ensure Vite/webpack base URL is configured
  • Cache issues: Hard refresh or clear browser data

Check your build tool's public path configuration:

// vite.config.js
export default {
  base: '/your-subdirectory/', // If not serving from root
}

React Router Breaking Favicons?

Ensure you're using absolute paths (/favicon.ico) not relative paths.

Advanced Patterns

Notification Badges

Add notification counts to your favicon:

const addBadgeToFavicon = (count: number) => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const img = new Image();

  img.onload = () => {
    canvas.width = 32;
    canvas.height = 32;
    ctx.drawImage(img, 0, 0);

    if (count > 0) {
      // Draw red circle
      ctx.fillStyle = '#ff0000';
      ctx.beginPath();
      ctx.arc(24, 8, 8, 0, 2 * Math.PI);
      ctx.fill();

      // Draw count
      ctx.fillStyle = '#ffffff';
      ctx.font = 'bold 10px Arial';
      ctx.textAlign = 'center';
      ctx.fillText(count.toString(), 24, 12);
    }

    // Update favicon
    const link = document.querySelector("link[rel*='icon']") as HTMLLinkElement;
    link.href = canvas.toDataURL('image/png');
  };

  img.src = '/favicon-32x32.png';
};

Animated Favicons

While not recommended for most apps, it's possible:

const animateFavicon = (frames: string[], interval: number = 500) => {
  let currentFrame = 0;

  setInterval(() => {
    const link = document.querySelector("link[rel*='icon']") as HTMLLinkElement;
    link.href = frames[currentFrame];
    currentFrame = (currentFrame + 1) % frames.length;
  }, interval);
};

Creating Favicons for React

Need to generate all these favicon sizes? Try Unwrite's Favicon Generator. Upload your logo once and get all the sizes you need for your React app, from tiny 16x16 favicons to large 512x512 PWA icons. Everything processes in your browser - your logo stays private.

Quick Reference Checklist

  • [ ] Add favicon.ico to public directory
  • [ ] Include multiple sizes for better device support
  • [ ] Add Apple touch icon for iOS
  • [ ] Create manifest.json for PWA support
  • [ ] Use absolute paths in HTML references
  • [ ] Test in production build
  • [ ] Consider SVG for scalability
  • [ ] Implement dynamic favicons if needed

Remember: favicons in React are just static files. Don't overcomplicate it. Put them in public, reference them in HTML, and move on to building features that matter.