You’ve added favicon.ico to your app directory and it works. But what happens when you need different icons for marketing pages versus the dashboard, or when the icon must reflect a user’s theme preference without a full page reload? The file-based convention is convenient, yet it cannot cover every scenario.

Next.js exposes an icons field inside the metadata object that lets you declare icons programmatically. This approach gives you granular control over rel, sizes, type, and even media attributes — all from layout.tsx or a specific page.tsx. We’ll walk through the practical steps to migrate from static files to a metadata-driven setup.

Prerequisites

Step 1: Define a reusable icon configuration

Create a small helper so you can reuse the same icon definitions across layouts. This keeps your metadata exports clean and makes future updates painless.

// lib/icons.ts
import { Metadata } from 'next';

export const iconConfig = {
  icon: [
    { url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
    { url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
    { url: '/icons/icon.svg', sizes: 'any', type: 'image/svg+xml' },
  ],
  apple: [
    { url: '/icons/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
  ],
  shortcut: '/icons/favicon.ico',
  other: [
    {
      rel: 'mask-icon',
      url: '/icons/safari-pinned-tab.svg',
      color: '#0066cc',
    },
  ],
} satisfies Metadata['icons'];

Step 2: Apply the configuration in your root layout

Import the helper and spread it into the icons field of your metadata export. This replaces the need for favicon.ico, icon.png, and apple-icon.png files sitting directly in app/.

// app/layout.tsx
import type { Metadata } from 'next';
import { iconConfig } from '@/lib/icons';

export const metadata: Metadata = {
  title: 'My App',
  icons: iconConfig,
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body>{children}</body>
    </html>
  );
}

Step 3: Override icons for a specific route

Because metadata merges down the route tree, a page can extend or replace the root icon set. This is useful for section-specific branding — think a distinct icon for a blog subdirectory or a tenant dashboard.

// app/dashboard/layout.tsx
import type { Metadata } from 'next';
import { iconConfig } from '@/lib/icons';

export const metadata: Metadata = {
  icons: {
    ...iconConfig,
    icon: [
      { url: '/icons/dashboard-32x32.png', sizes: '32x32', type: 'image/png' },
      { url: '/icons/dashboard.svg', sizes: 'any', type: 'image/svg+xml' },
    ],
  },
};

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <section>{children}</section>;
}

Step 4: Generate icons dynamically from data

Need an icon that changes based on a feature flag, user role, or A/B variant? Fetch the data in a server component and build the icons object on the fly. The example below picks a high-contrast variant when the theme cookie signals dark mode.

// app/settings/page.tsx
import { cookies } from 'next/headers';
import type { Metadata } from 'next';

export async function generateMetadata(): Promise<Metadata> {
  const cookieStore = await cookies();
  const theme = cookieStore.get('theme')?.value ?? 'light';
  const suffix = theme === 'dark' ? '-dark' : '';

  return {
    icons: {
      icon: [
        { url: `/icons/settings${suffix}-32x32.png`, sizes: '32x32' },
        { url: `/icons/settings${suffix}.svg`, sizes: 'any' },
      ],
      apple: [{ url: `/icons/settings${suffix}-apple.png`, sizes: '180x180' }],
    },
  };
}

export default function SettingsPage() {
  return &lt;main&gt;Settings&lt;/main&gt;;
}

Step 5: Add dark-mode SVG with media queries

If you prefer a single SVG that adapts via CSS, declare it once and let the media attribute handle the rest. This avoids duplicate assets and keeps the &lt;head&gt; lean.

// lib/icons.ts (addition)
export const adaptiveIconConfig = {
  icon: [
    {
      url: '/icons/adaptive.svg',
      sizes: 'any',
      type: 'image/svg+xml',
      media: '(prefers-color-scheme: light)',
    },
    {
      url: '/icons/adaptive-dark.svg',
      sizes: 'any',
      type: 'image/svg+xml',
      media: '(prefers-color-scheme: dark)',
    },
  ],
} satisfies Metadata['icons'];

Common pitfalls

Verification checklist

  1. Open DevTools → Network → filter “img” and confirm each declared icon returns 200.
  2. Inspect &lt;head&gt; for &lt;link rel='icon'&gt;, &lt;link rel='apple-touch-icon'&gt;, and &lt;link rel='mask-icon'&gt; tags.
  3. Test dark-mode switching in OS settings; the correct SVG should load without a refresh.
  4. Run a quick audit with Mzu favicondl’s online checker to catch missing sizes or MIME-type mismatches.

When to stick with file-based conventions

If your project only needs a single, static favicon set, the file-based approach (app/favicon.ico, app/icon.png, etc.) remains the simplest path. It requires zero TypeScript, zero runtime, and works out of the box. Reach for the metadata API when you need per-route overrides, dynamic selection, or advanced attributes like media and color.

For a deeper dive on the file-based workflow, see our guide on adding a Next.js App Router favicon in minutes. And if you’re building a PWA, the Web App Manifest favicon guide covers the manifest icons you’ll need alongside these metadata declarations.

Check the documented requirements

Next.js documents favicons and app icons in its metadata file conventions.