If you have just spun up a new Remix project, you probably noticed a glaring omission: there is no index.html file sitting in a public folder waiting for your <link> tags. Getting your remix favicon setup right requires shifting how you think about document heads. In traditional React single-page applications, you just dump an .ico file in the root directory and call it a day. Remix handles things differently, leaning heavily on route-based exports to manage document metadata.

Look at GitHub's browser tabs. They do not just serve a static image; their favicon changes based on system themes and even updates dynamically to show a blue dot when you have unread notifications. To achieve that level of polish in Remix, you cannot rely on magic file resolution. You need to explicitly define your assets.

The Remix Philosophy: Explicit Over Implicit

Unlike the Next.js App Router, which automatically scans your app directory for files named favicon.ico or icon.svg, Remix prefers explicit declarations. I strongly prefer this approach. Magic file routing is great until it breaks, and then you are left digging through framework source code to figure out why your icon is caching incorrectly.

In Remix, you use the LinksFunction export. This function returns an array of objects that Remix maps directly to HTML <link> tags. Since your favicon needs to appear on every single page of your application, the only logical place to put this is in your app/root.tsx file.

Step 1: Prepare Your Asset Stack

Before writing any code, you need the actual image files. In 2026, you do not need 15 different icon sizes. You only need three specific files placed in your public directory:

If you only have a high-resolution logo, run it through Mzu favicondl. It will generate this exact modern stack and strip out the unnecessary metadata that bloats SVG files.

Step 2: The Links Export in root.tsx

Open your app/root.tsx file. If you used the default Remix template, you likely already have a links function exporting your stylesheet. We are going to expand that array to include our favicon stack.

import type { LinksFunction } from '@remix-run/node';

export const links: LinksFunction = () => [
  // Legacy fallback
  { rel: 'icon', href: '/favicon.ico', sizes: '48x48' },
  // Modern scalable icon
  { rel: 'icon', href: '/icon.svg', type: 'image/svg+xml' },
  // Apple device support
  { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }
];

Remix takes these objects and injects them into the <Links /> component rendered inside your document's <head>. Order matters here. Browsers read from top to bottom, and modern browsers will correctly prioritize the SVG over the ICO file if they support it.

Step 3: Handling Dark Mode

If your logo is black, it will completely disappear when a user switches their browser to dark mode. Because Remix's links array accepts any valid HTML link attribute, we can use the media property to swap icons based on the user's system preferences.

export const links: LinksFunction = () => [
  { rel: 'icon', href: '/favicon.ico', sizes: '48x48' },
  {
    rel: 'icon',
    href: '/icon-light.svg',
    type: 'image/svg+xml',
    media: '(prefers-color-scheme: light)'
  },
  {
    rel: 'icon',
    href: '/icon-dark.svg',
    type: 'image/svg+xml',
    media: '(prefers-color-scheme: dark)'
  }
];

This is much cleaner than writing inline CSS inside a single SVG file, and it ensures the browser only requests the asset it actually needs.

Advanced: Dynamic Favicons with Loader Data

Here is a common pitfall: developers try to use the links export to show a notification badge on their favicon. The problem? The LinksFunction does not have access to your useLoaderData(). It is evaluated before your component renders.

If you need a dynamic favicon based on user state (like unread messages), you must bypass the links export for that specific tag and render a standard HTML element manually inside the <head> of your root component.

export default function App() {
  const data = useLoaderData<typeof loader>();
  const faviconUrl = data.hasUnread ? '/icon-unread.svg' : '/icon.svg';

  return (
    <html lang='en'>
      <head>
        <Meta />
        <Links />
        {/* Dynamic favicon rendered manually */}
        <link rel='icon' href={faviconUrl} type='image/svg+xml' />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

This approach gives you the best of both worlds. You keep your static assets (like the Apple Touch Icon) in the clean links export, while handling dynamic state directly in your React tree. If you are migrating from a standard React favicon setup, this pattern will feel very familiar.

Final Testing

Once deployed, browsers can be notoriously stubborn about updating cached icons. If you refresh and still see the old Remix logo, you are likely dealing with a local cache issue. Do not waste hours debugging your code—read up on how to force a favicon cache clear first.

Remix forces you to be explicit about your document head, which ultimately leads to fewer bugs and better performance. Stick to the SVG + ICO fallback pattern, utilize the links export for static assets, and you will have a professional browser tab presence that works flawlessly across all devices.