How to Add Custom Dynamic Favicons in React & Next.js

Browsers ship with a ton of ways to make your web app your own. With CSS, HTML, and JavaScript, we can do whatever we want within the walls of the browser tab. But it doesn’t include a ton of ways to customize the outside of that experience except tools like favicons, which are almost as old as the web itself! How can we take advantage of favicons in the modern world of React and Next.js?

What's Inside 🧐

What is a favicon?

If you haven’t heard of the term favicon, you’ve likely already seen one, just maybe never knew what it’s called.

Favicons are the little images (icons) that appears in the browser tab when you’re visiting a website.

Browser tabs with arrows pointing to favicons
Several browser tabs with favicons

It was originally added to IE5 by Microsoft, the idea being that whenever you bookmarked a page, it would include the icon.

The goal is to provide an easy way to distinguish a website from others. If you have a few bookmarks or tabs open, being able to see that big Google logo helps you to recognize and switch to it quicker.

But sites like Gmail in the screenshot above have taken that a step further, where not only can you use a favicon for a simple logo, but you can use it for easily identifiable information, such as how many messages are unread.

How does Next.js use favicons?

There’s really nothing “special” about using a favicon with Next.js.

Favicons are a web standard, so pertaining to Next.js, it’s really just about how to add it to the page using the standardized method and making sure it properly renders with the rest of the page’s HTML.

Step 0: Creating a new Next.js app with Create Next App

We’re going to start off with a new Next.js app using Create Next App.

Inside of your terminal, run:

yarn create next-app my-favicon-app
# or
npx create-next-app my-favicon-app

Note: feel free to use a different value than my-favicon-app as your project name!

Once installation has finished, you can navigate to that directory and start up your development server:

cd my-favicon-app

yarn dev
# or
npm run dev

And once loaded, you should now be able to open up your new app at http://localhost:3000!

New web app showing Welcome to NExt.js
New Next.js app

Follow along with the commit!

Step 1: Adding a custom favicon to a Next.js app with Favicon Generator

When we create a new Next.js application with Create Next App, we actually get a favicon by default.

Browser tab showing Next.js default favicon
Default Next.js favicon

The only issue, that’s the Vercel logo! But luckily, it shows that out of the box, favicons “just work” with Next.js.

If we look inside of pages/index.js our homepage, we can see that this is getting added using the Next.js Head component, which along with our favicons, can help manage other important SEO metadata.

<Head>
  <title>Create Next App</title>
  <meta name="description" content="Generated by create next app" />
  <link rel="icon" href="/favicon.ico" />
</Head>

So we’re already a good chunk of the way there.

Now the standard favicon file that’s been around for ages is the favicon.ico file. This isn’t a typical image file that we can just export from anywhere.

Luckily, there are a wide variety of options of how we can generate ICOs from images online. My favorite as of writing this is Favicon Generator.

To start, we need an image to use. We can really use any image we want with Favicon Generator, as it gives some options for how we can tweak it even if it’s not square, but the image should ideally be square and easily visible when tiny.

If you want to follow along, you can use my favicon from spacejelly.dev.

spacejelly.dev favicon
spacejelly.dev favicon

Download at https://spacejelly.dev/favicon-1024×1024.png

Notice I’m using a PNG with a circle in it. The cool thing is we can use transparency to avoid having to use square images (though square images are totally fine!).

Now heading over to Favicon Generator at realfavicongenerator.net, the first thing you’ll see at the top of the page is a button that says Select your Favicon Image.

Favicon Generator highlighting Select your Favicon image button
Selecting an image on Favicon Generator

Once you do, Favicon Generator will load up a preview for how your favicon will look on your site with a few different examples. It even gives you options for how you can change how it looks, like adding a background or changing app settings.

Now as a quick aside here, different native devices allow you to specify settings for how your website or app will appear on the web.

Favicon Generator showing preview of Android Chrome app icon
Android Chrome settings on Favicon Generator

Android Chrome for instance allows you to set a “theme” color, which changes how your website looks when someone opens it on their device.

Feel free to update these settings to your liking, but we’ll be focusing on the icon itself for now.

Once you’re ready, head down to the bottom of the page and click Generate your Favicons and HTML code.

Favicon Generator will build your icons to a ZIP that you can download and provide you with a snippet of HTML that you can add right into your app.

Favicon Generator installation instructions including downloading favicon package and HTML snippet
Generated favicon settings

Go ahead and click Favicon package and unzip the file.

Mac Finder showing arrow to drag favicons to Next.js public folder
Dragging favicon files to the public folder

Then drag all of those files into your public directory including replacing the favicon.ico file with the new one.

Note: The public directory is used in Next.js to serve static files. For instance, dragging favicon.ico into public will make favicon.ico available at yourwebsite.com/favicon.ico.

Now that our image files are all ready, let’s head back to Favicon Generator and copy our code snippet and paste it into the Head component of our app.

After pasting, be sure to update the snippet to conform to JSX standards, meaning, adding a closing tag to all of the lines or self-closing each one.

<Head>
  <title>Create Next App</title>
  <meta name="description" content="Generated by create next app" />
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
  <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="manifest" href="/site.webmanifest" />
  <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
  <meta name="msapplication-TileColor" content="#da532c" />
  <meta name="theme-color" content="#ffffff" />
</Head>

You’ll also notice that the new snippet doesn’t include the favicon.io file. By default, browsers will look for a favicon.ico file in the root of the project. This means that we don’t necessarily need to include it.

But now, finally, if we open back up our app in our browser, we should now see our new favicon in our browser tab!

Browser tab showing custom favicon
Updated favicon in browser tab

Now to dig into the additional tags we added, we can take a quick glance at what they’re doing.

The easiest way to check this out is in Chrome under the Application tab in the Developer Tools.

If we start from the top, we’ll see there’s not much info, but the reason for that is we didn’t fill any of that out in our site.webmanifest file, but if we scroll down, we can see all of the app icons that we’re referencing in that file.

Next.js app with dev tools open showing web manifest with app icons
App icons showing in application manifest

Tip: you can fill out the rest of site.webmanifest which helps gives more context to your web app.

On top of that, if we navigate to our web app on our mobile devices that support adding bookmarks to the homescreen like iOS, we can see that we can now add it and our app icon shows up right on the homescreen for us!

Note: you likely can’t go to localhost:3000 directly on a mobile devices if the server isn’t running there. You can use tools like ngrok to test with a public address or simply test when the site is deployed.

iOS showing adding web app to homescreen with custom shortcut app icon
Adding the web app to the home screen in iOS

Follow along with the commit!

Step 2: Using _app.js to set global favicon and app icons in Next.js

Our favicon and app icons are looking great, but we have one issue. It’s only applying to the homepage.

Now technically, as I mentioned in Step 1, because the file is in the root of our project, browsers will automatically see that favicon.ico and load it, so if we create a new page, it would still kind of work.

But we don’t necessarily want to use the ICO file, we want to load the PNG files, which are high resolution and will look nicer.

To move our favicon to a globally available spot, we can use the App file, which is a Next.js-specific file that wraps the component tree of all pages.

If we open up pages/_app.js, we won’t see much in there, but let’s move everything inside of our Head component in our homepage to that file.

To start, at the top of pages/_app.js import the Head component:

import Head from 'next/head';

Then, we want to add our Head component and favicon tags to the component tree. We can do that by creating a React Fragment, which we can then put our Head.

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
        <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="manifest" href="/site.webmanifest" />
        <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
        <meta name="msapplication-TileColor" content="#da532c" />
        <meta name="theme-color" content="#ffffff" />
      </Head>
      <Component {...pageProps} />
    </>
  );
}

You’ll notice I also moved the title and description to our App file. The added benefit here is that we can then have a global default value for our title and description, so that no matter what, we always have one set.

At this point, if you reload the homepage, nothing should be different, however to test this out, we can create a quick new page.

Let’s duplicate our pages/index.js file to something like pages/admin.js. Feel free to update it to however you’d like, but I added the title of Admin and I additionally added a link using the Next.js Link component back to the homepage.

Note: See my code in the commit!

On the homepage, we can similarly add a link to the Admin page (or the new page you created) so that we have a way to navigate to it.

Note: See my code in the commit!

But now, if we navigate over to our Admin page, we can see the favicon working! And if we open up the developer tools to check the source, we can see that without adding the Head component and tags to our Admin page, we have our favicons!

Next.js with dev tools open showing HTML with favicon
Favicon tags showing in developer tools source

Follow along with the commit!

Step 3: Adding unique favicons to different pages

We have global favicons working well, but what if we wanted to change our favicon depending on the page we’re on?

The nice thing about the Head component is we can use it anywhere in our application and override the global instance of it.

To test this out, I created a new icon that you can follow along with.

Favicon variation of spacejelly.dev with lock for admin
Admin variation of a favicon

Download at https://github.com/colbyfayock/my-favicon-app/blob/main/public/favicon-admin-1024×1024.png

We’re going to use this icon for our admin page.

Like before, we can head over to our Favicon Generator and load this up, getting our variations.

This time however, when we unpack the files, the only 2 that we’re going to use in this example are:

  • favicon-16×16.png
  • favicon-32×32.png

Now we already have files named that way, so we can name them a different variation so that we can avoid conflicts when referencing them.

  • favicon-admin-16×16.png
  • favicon-admin-32×32.png

We’ll want to drop these in our public folder like earlier.

Now let’s head over to pages/admin.js and inside, we should already have the Head component imported at the top.

That means we can add our head component right at the top of the component:

<Head>
  <title>Admin</title>
  <link rel="icon" type="image/png" sizes="32x32" href="/favicon-admin-32x32.png" />
  <link rel="icon" type="image/png" sizes="16x16" href="/favicon-admin-16x16.png" />
</Head>

If we open up our app then navigate to our Admin page, we should now see the updated favicon!

Next.js app with dev tools showing updated HTML with favicon variant
Updated icon for Admin page

Now we have one problem though. If we click back to the homepage, we notice that the favicon actually doesn’t revert back to the original! 😱

Next.js homepage with devtools showing stale favicon
Homepage showing the wrong favicon

I don’t know this for a fact, but my guess is that when the favicon is added to the page, it updates, but removing it, doesn’t “update” the page, causing it to stay to the Admin variation.

To fix this, since we’re only updating those two specific icons, we can simply re-add those back to the homepage.

Inside of pages/index.js add:

<Head>
  <title>Home</title>
  <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
  <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
</Head>

And now if we refresh the page and navigate back and forth, we can see we’re back to our original favicon.

Now part of the reason this is happening is due to us using the _app.js file to add the global favicons. That “component” isn’t going to refresh on page change, meaning, the favicons in that file are going to be stale as we navigate around.

This is totally okay if we only plan on having a single favicon across the site, but we we saw here, it can lead to troubles if we need that to be dynamically updated.

We’re not going to cover this here, but if you plan on using dynamic icons, I recommend alternatively using a Layout component that wraps the content of each page along the lines of:

const Layout = ({ children }) => {
  return (
    <>
      <Head>
        <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
        <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
      </Head>
      { children }
    </>
  )
}

<Layout>
  <Head>
    <title>Admin</title>
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-admin-32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-admin-16x16.png" />
  </Head>
  <h1>Admin</h1>
</Layout>

It lends itself a bit better to managing dynamic page content like in our use case here or if you need to handle other dynamic bits when composing your global page layout.

But in the meantime, we successfully set up our favicon to change when navigating to different pages of our web app!

Follow along with the commit!

Step 4: Dynamically updating favicons based on user interaction

Changing the favicon on page change is one thing, but how about dynamically updating that icon on the same page?

Given we’re in React-land and the way the Head component works, we can apply the exact same concepts we would to any other React component.

To test this out, we’re going to use React state along with a button to trigger an update.

First, at the top of pages/index.js , let’s import useState:

import { useState } from 'react';

Next, let’s define a new instance of state along with a function that we’ll use to update that state when we click our button:

const [favicon, setFavicon] = useState('');
console.log(`favicon state: ${favicon}`);
function handleOnClick() {
  setFavicon('favorite');
  setTimeout(() => setFavicon(''), 3000);
}

Here’s what we’re doing:

  • Creating a new instance of state called favicon
  • That state has a default of an empty string, as we’ll use this later for a dynamic string value
  • We’re logging out that value so we can test and make sure it’s working
  • We’re defining a handleOnClick function that when fired, will update the state to a string called favorite then 3 seconds later set it back to an empty string

And finally, we can add the button to trigger this:

<p>
  <button onClick={handleOnClick} style={{
    color: 'white',
    backgroundColor: 'blueviolet',
    padding: '.6em .8em',
    border: 0,
    cursor: 'pointer'
  }}>
    ❤️ Smash that Like button!
  </button>
</p>

If we open that up in the browser and look in our web log, we should see an empty console log statement, but when we click the button, we should see a string of favorite followed 3 seconds later by another empty log statement.

Next.js app with dev tools showing console logs with favicon state on click
Console logs showing active state

So now, let’s use that to dynamically change our favicon!

To do this, we’re going to create yet another favicon.

Here’s the image I’ll use, which you can feel free to convert over at Favicon Generator like the previous examples.

Heart variation of favicon

Download at https://github.com/colbyfayock/my-favicon-app/blob/main/public/favicon-favorite-1024×1024.png

Now similar to before, we want to use the two files, favicon-16x16.png and favicon-32x32.png but we want to rename them to:

  • favicon-favorite-16×16.png
  • favicon-favorite-32×32.png

When ready, drop those in the public directory.

Next, we’re going to update our favicons in the Head component with the following:

<Head>
  <title>Home</title>
  <link rel="icon" type="image/png" sizes="32x32" href={favicon ? `/favicon-${favicon}-32x32.png` : `/favicon-32x32.png`} />
  <link rel="icon" type="image/png" sizes="16x16" href={favicon ? `/favicon-${favicon}-16x16.png` : `/favicon-16x16.png`} />
</Head>

What we’re doing here is dynamically setting the value of our favicons, so that when there is no favicon state, we return it like we did previously, but when there is a state, we include it as part of the path to the favicon.

But now, when we go ahead and smash that Like button (click the button), we can now see it updates the favicon dynamically!

Updating the favicon on button click

Follow along with the commit!

What else can we do?

Shared layout

As I mentioned in Step 3, using the _app.js file for dynamic content has it’s drawbacks.

I like to recommend using a shared Layout component that gets used to wrap all pages. It also gives a bit more flexibility with controlling the Layout from the page, such as changing headers on a specific page.

Check out how I used a shared Layout component for spacejelly.dev!

https://github.com/colbyfayock/spacejelly.dev/blob/main/src/components/Layout/Layout.js

Optimize images

While the images are small, it’s always a good idea to optimize the images, especially transparent PNGs, to make sure you’re delivering as little data as possible for your visitors to download. It also makes it faster!

You can use tools like TinyPNG to make sure you’re serving your images as small as possible without sacrificing quality.