Next JS Favicon: A Guide for Perfect App Branding
Master your Next JS favicon setup. This guide covers the App Router, Pages Router, apple-touch-icons, and optimization tips for perfect branding on any device.
You deploy your app, open the production URL, and the tab still shows a generic icon. Sometimes it’s the default platform icon. Sometimes it’s your old favicon from three deployments ago. Sometimes Safari shows one thing, Chrome shows another, and iOS ignores both.
That’s the point where favicon setup stops being a basic checklist item and turns into a production issue.
A polished next js favicon setup affects how your product looks in browser tabs, bookmarks, install prompts, shared screens, and saved home screen shortcuts. If you’re shipping a SaaS app, those tiny assets carry more brand weight than is typically anticipated. They’re one of the few visual elements users keep seeing long after the landing page is gone.
Why Your Next JS Favicon Matters More Than You Think
A weak favicon setup makes a finished app look unfinished.
The common failure mode is simple. The UI is polished, auth works, billing works, onboarding works, and then the browser tab shows the wrong icon. That small mismatch makes the product feel less cared for. Users won’t say “the favicon broke trust,” but they will register that the app feels rough around the edges.
![]()
Tiny asset, real brand signal
Favicons matter most in the places developers often forget:
- Tab-heavy workflows where users keep your app beside Stripe, Linear, Notion, and Slack
- Bookmarks and reading lists where the icon becomes the primary visual identifier
- Home screen installs on mobile where the app icon needs to look intentional
- Shared demos and screen recordings where the tab strip is visible
If you’re working on launch polish, brand consistency matters just as much as copy or screenshots. That’s part of the same awareness problem discussed in this guide on building brand awareness.
Why Next.js made this easier
Next.js cleaned up a lot of old favicon pain with the App Router.
Next.js introduced automated favicon handling in its App Router with version 13, and for the 2.6 million active websites using the framework, developers can place a favicon.ico file in the /app root and let Next.js generate the needed tags automatically. The related static optimization can reduce bundle sizes by up to 90% via caching (Next.js app icons documentation).
That’s the happy path, and it works well.
Practical rule: If your favicon setup requires a lot of manual
<head>maintenance in a modern App Router app, start by asking whether you’re fighting the framework instead of using it.
Where teams still get tripped up
The problem isn’t the first setup. It’s everything after that.
A favicon can be technically present and still wrong in production. The usual reasons are stale browser caches, CDN caching, duplicate icon declarations, route-level assumptions that don’t match how Next.js handles root app icons, and platform-specific files that were never generated in the first place.
The rest of this guide focuses on those real issues. Not just “how to add favicon.ico,” but how to make sure your next js favicon setup stays correct after deploys, device installs, and icon updates.
Generating and Organizing Your Favicon Files
Don’t start in layout.tsx. Start with the assets.
Most favicon problems come from using one file for every context and hoping browsers sort it out. They won’t. A modern app needs a small set of purpose-built files, each named and sized for a specific job.
![]()
Start with a clean source asset
Design the icon from a square master file.
If you’re designing in Figma, build the favicon as a simple mark, not a full logo lockup. Tiny sizes punish detail. Thin strokes, text, and gradients often collapse badly. If your design team works across tools, the trade-offs in Figma vs Adobe XD matter less than exporting a clean source asset that survives downscaling.
Use a favicon generator if you want speed and consistency. RealFaviconGenerator is a practical choice because it outputs the common files teams usually forget.
The file set that actually matters
Here’s the set I’d prepare before touching code:
| File | Where it’s used | Notes |
|---|---|---|
favicon.ico |
Traditional browser tab icon | Keep this even if you also use PNG or SVG |
icon.png |
General modern browser icon | Good fallback bitmap asset |
icon.svg |
Modern scalable favicon | Sharp in supported browsers |
apple-icon.png or apple-touch-icon.png |
iOS home screen shortcut | Use a clean square icon with padding awareness |
manifest.json |
PWA metadata | Connects your install experience to icon assets |
web-app-manifest-192x192.png |
Android install surface | Used by manifest-driven installs |
web-app-manifest-512x512.png |
Large install and PWA contexts | Useful for higher-resolution install assets |
mask-icon.svg |
Safari pinned tab | Monochrome use case, separate from your main favicon |
Required versus nice-to-have
Not every file carries the same weight.
- Must have:
favicon.ico - Must have for mobile polish: Apple touch icon and manifest icons
- Worth adding: SVG favicon for modern browsers
- Situational: Safari pinned tab assets, extra desktop PNG sizes, larger social images
The icon that looks great at marketing-site size often looks muddy at tab size. Test the smallest version first, not last.
File naming discipline saves time
Name files exactly once and stick to the convention.
Avoid patterns like:
favicon-new.icofavicon-final-final.icoapple-touch-icon-v2.png
Those names create cache confusion and make your metadata harder to audit. Use stable names for stable assets, and only add versioning when you’re intentionally cache-busting.
A clean working set looks like this:
- Core browser asset:
favicon.ico - Vector version:
icon.svg - Bitmap fallback:
icon.png - Apple asset:
apple-icon.png - Manifest:
manifest.json - PWA icons:
web-app-manifest-192x192.png,web-app-manifest-512x512.png
What usually doesn’t work well
Some choices are technically valid but operationally messy.
- Over-detailed logos: Fine lines and text disappear.
- Transparent edges without testing: They can look soft or awkward on some surfaces.
- Single-format strategy: Relying only on SVG or only on ICO creates avoidable compatibility gaps.
- Manual resizing in random tools: It often introduces blur, inconsistent padding, or export artifacts.
If you prepare the asset set carefully, implementation becomes straightforward. If the files are inconsistent, no amount of metadata cleanup will make the result look polished.
Implementing Favicons in the Next JS App Router
If you’re on the App Router, the best setup is usually the simplest one.
Put the right files in the right place and let Next.js do its job first. Add manual metadata only when you need more control.
![]()
The default file-based setup
For a modern App Router app, place favicon files in the root of /app.
A clean structure looks like this:
app/
layout.tsx
page.tsx
favicon.ico
icon.png
icon.svg
apple-icon.png
manifest.json
This is the setup many projects should start with.
Next.js recognizes file conventions and generates the relevant tags for you. That means less manual <head> code, less duplication, and fewer mistakes from stale legacy patterns.
What filenames Next.js expects
These are the practical filenames to know:
favicon.icofor the classic browser iconicon.*for general icons, including formats like PNG or SVGapple-icon.*for Apple-specific home screen usagemanifest.jsonfor install metadata
A few implementation details matter:
- Put
favicon.icoat the root of/app, not inside a nested route. - Keep
icon.*files in/approot if they represent the global app identity. - Don’t duplicate the same icon in both file convention and manual metadata unless you mean to override behavior.
- Use root-level naming consistently, because favicons are a global concern in most apps.
The happy path in practice
If all you need is one global favicon, this may be enough:
app/
favicon.ico
apple-icon.png
icon.png
manifest.json
layout.tsx
And your root layout can stay minimal:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
That’s it. No manual <link rel="icon"> tags required for the basic case.
If the App Router file convention already expresses your intent, prefer it over custom head markup. Simpler setups are easier to debug after deployment.
When to use the metadata object
Use the metadata export when you need explicit control.
Examples include:
- separate light and dark mode icons
- explicit type declarations
- multiple icon entries
- a manifest path you want to define centrally
- a deliberate override of defaults
A practical root layout example looks like this:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Acme',
icons: {
icon: [
{ url: '/favicon.ico' },
{ url: '/icon.png', type: 'image/png' },
{ url: '/icon.svg', type: 'image/svg+xml' },
],
apple: [{ url: '/apple-icon.png' }],
},
manifest: '/manifest.json',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
This is useful when you want the intent visible in code, not only in filesystem conventions.
Here’s a walkthrough if you want to see the broader App Router structure in action before layering on custom icon behavior:
Common mistakes in App Router projects
The problems I see most often are avoidable.
Duplicate icon declarations
Teams add favicon.ico in /app, then also hardcode <link rel="icon"> tags in a custom head component, then also define icons in metadata. Browsers may still pick one, but debugging becomes painful.
Choose one primary strategy.
Wrong file location
Putting favicon.ico in /public can still make sense in some manual setups, but if you’re relying on App Router file conventions, put it in /app root.
Expecting route-specific file conventions
Favicons are not like route-level Open Graph images. Many developers assume they can drop route-specific favicons into nested folders and get automatic per-route behavior. That’s where reality pushes you toward custom metadata or manual link management.
My production default
For most SaaS apps, I’d use this order of preference:
| Need | Best approach |
|---|---|
| One global app favicon | /app file convention only |
| Global icon plus explicit metadata clarity | /app files + metadata.icons |
| Runtime or conditional favicon logic | Manual link management with custom control |
That last case needs extra care. That’s where caching, deployment behavior, and browser quirks start to matter.
Legacy Support for the Next JS Pages Router
If your app still uses the Pages Router, you’re in the manual world.
That doesn’t mean the setup is bad. It just means you are responsible for wiring the favicon tags correctly. Many older production apps still run this way, and if the codebase is stable, there’s no need to force a migration just to fix icons.
Put files in public
For Pages Router projects, store favicon assets in the /public directory.
A typical structure looks like this:
public/
favicon.ico
apple-touch-icon.png
icon.png
icon.svg
manifest.json
web-app-manifest-192x192.png
web-app-manifest-512x512.png
Files in /public are served from the root path, so public/favicon.ico becomes /favicon.ico.
Add the tags in _document.tsx
Use a custom _document.tsx when you need stable, global <head> tags across the app.
A practical example:
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head>
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/icon.png" type="image/png" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="apple-mobile-web-app-title" content="Acme" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
This setup is explicit, readable, and easy to audit.
When this approach is still the right one
Use the Pages Router pattern when:
- You’re maintaining an older app that hasn’t migrated to
/app - Your team wants explicit
<head>control without mixing conventions - You need consistency in a codebase already built around
_document.tsx
What to watch out for
Pages Router apps tend to accumulate old favicon tags.
Check for:
- duplicate icon tags in
_app.tsx, page-levelHead, and_document.tsx - outdated filenames no longer present in
/public - a mix of old Apple tag names and new asset names
- stale
manifest.jsonreferences after asset renames
If a Pages Router app shows the wrong favicon, inspect the final rendered HTML first. The bug is often a duplicate tag, not the file itself.
This method is more manual than the App Router path, but it’s predictable. That predictability is useful in mature codebases where a full migration isn’t on the roadmap.
Advanced Favicon Techniques and Troubleshooting
Most favicon guides stop right after “put favicon.ico in your app folder.” That’s fine for local development. It’s not enough for production.
The hard part starts when the icon won’t update after deploy, or when you need the favicon to change based on state, environment, or user context.
![]()
Why your favicon looks stuck
A favicon can get cached at multiple layers:
- Browser cache
- CDN cache
- Hosting platform behavior
- Preloaded metadata from earlier deploys
That’s why replacing the file alone often doesn’t seem to work.
Handling dynamic or conditional favicons is a significant challenge, with over 30% of “Next.js favicon not updating” queries stemming from caching issues on platforms like Vercel. Static metadata exports often cause default icons to override custom ones. An effective solution involves disabling default injection with unstable_noFaviconInjection: true and manually managing <link> tags, a pattern that has shown to resolve up to 20% of favicon mismatches in A/B deployment tests (favicon.im on adding a favicon to a Next.js project).
That tracks with real production behavior. The browser may still request an old file path, and the platform may still serve a cached result.
How to force-update a favicon
If you need an update to stick, use a deliberate cache-busting process.
Change the URL, not just the file
Browsers are much more willing to fetch a new favicon when the URL changes.
Instead of:
<link rel="icon" href="/favicon.ico" />
use:
<link rel="icon" href="/favicon.ico?v=2" />
For higher confidence after a rebrand or major update, rename the asset entirely:
<link rel="icon" href="/favicon-2026.ico" />
That’s blunt, but it works.
Clear duplicate declarations
If one part of your app points to /favicon.ico and another points to /icon.png?v=2, browsers can behave inconsistently. Audit the final <head> output and remove every stale declaration.
Rebuild the manifest relationship
If users install your app on mobile, update the icon file references inside manifest.json too. Teams often bust the browser tab icon and forget the install icons entirely.
A strong SEO and polish mindset applies here too. Little consistency issues tend to pile up, just like the broader technical details covered in this guide on how to improve search engine rankings.
Dynamic favicons that actually work
Changing the favicon based on app state sounds easy. In practice, Next.js metadata caching can get in the way.
Typical use cases:
- unread notifications
- environment badge for staging
- tenant-specific branding
- light and dark mode variants
- status indicators for live jobs or incidents
If you need true runtime behavior, the safest pattern is usually manual control.
Disable default injection when needed
When Next.js auto-injects favicons and you also want dynamic behavior, conflicts can happen.
In cases where the default injection is the source of mismatch, use unstable_noFaviconInjection: true and manage the <link rel="icon"> tags yourself.
A manual layout approach might look like this:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
<link rel="icon" href="/favicon.ico?v=2" />
<link rel="apple-touch-icon" href="/apple-icon.png?v=2" />
</head>
<body>{children}</body>
</html>
)
}
For client-driven state, you can update the favicon directly:
'use client'
import { useEffect } from 'react'
export function NotificationFavicon({ hasUnread }: { hasUnread: boolean }) {
useEffect(() => {
const link =
document.querySelector("link[rel='icon']") ||
document.createElement('link')
link.setAttribute('rel', 'icon')
link.setAttribute('href', hasUnread ? '/favicon-unread.ico' : '/favicon.ico')
if (!link.parentNode) {
document.head.appendChild(link)
}
}, [hasUnread])
return null
}
This isn’t the prettiest solution, but it’s reliable for real runtime changes.
“If the favicon must react to client state, treat it like UI state. Don’t expect static metadata to behave like a live component.”
What usually fails
A few patterns sound elegant and then waste hours:
- Relying on
generateMetadataalone for per-request favicon changes when your deployment path still behaves statically - Keeping auto-generated and manual icons active together
- Assuming Vercel deploy equals browser refresh
- Testing only in one browser
Chrome, Safari, and mobile install surfaces all have their own habits. If the favicon matters to your brand, test all three.
Frequently Asked Next JS Favicon Questions
Can I use SVG for a next js favicon
Yes, and it’s a good option for modern browsers.
Still keep favicon.ico as a fallback. SVG is sharp and flexible, but a production setup shouldn’t depend on one format alone. For broad compatibility, use SVG as one layer in a multi-file favicon strategy, not the entire plan.
How do I support light mode and dark mode favicons
Use separate icon entries with a media condition when you’re managing metadata manually or explicitly.
A common pattern is:
export const metadata = {
icons: {
icon: [
{ url: '/icon-light.svg', media: '(prefers-color-scheme: light)' },
{ url: '/icon-dark.svg', media: '(prefers-color-scheme: dark)' },
],
},
}
Keep both icons visually simple. Mode-specific swaps are useful, but only if each icon stays legible at small sizes.
Does manifest.json replace normal favicon tags
No.
The manifest helps with install surfaces and PWA behavior. It doesn’t replace the standard browser favicon relationship. You still need your regular icon declarations or App Router file-based setup for browser tabs and bookmarks.
Can I set different favicons for dynamic routes
Not with the basic root file convention alone.
If you want route-specific or tenant-specific icons in paths like app/blog/[slug]/page.tsx, move into explicit metadata logic or manual <link> control. That’s especially true if the icon depends on runtime state or branding tied to the request.
Why does the old favicon keep showing after deployment
Because browsers cache favicons aggressively, and your platform may cache them too.
The usual fixes are:
- Change the icon URL
- Remove duplicate declarations
- Verify the rendered head
- Update the manifest icons if installs are involved
When should I bring in outside React help
If your app already has metadata conflicts, mixed router patterns, white-label branding, and runtime icon requirements, this can turn into a broader front-end cleanup task. In that case, a specialized team can help untangle the app shell, metadata, and asset pipeline. If you need that kind of support, it’s reasonable to hire ReactJS developers who have handled production Next.js architecture rather than treating the favicon issue as an isolated bug.
If you’re preparing a SaaS launch, the little details matter. A clean favicon setup makes your product look finished everywhere people discover it. When you’re ready to put that product in front of more early users, list it on SubmitMySaas.