Favicon ToolsVerifyBuild
Framework

Remix Favicon Generator

Remix (and React Router framework mode, which is the same route API under renamed packages) does not pick up icons from a special filename. You put files in public/ and return them from the root route's links() export so <Links /> can render them into the document head.

public/ plus a root links() export

Anything under public/ is copied to the client build root, so public/favicon.ico is served at /favicon.ico. That alone is not enough for a complete set: browsers still need link tags for PNG sizes, the apple-touch-icon, and a manifest, and those tags come from route modules rather than from a static HTML file.

The root route is the right place. Its links() return value is aggregated with every other matched route and rendered by <Links /> in the document shell, so a declaration there covers every page including error boundaries that still render inside the root layout.

Remix v2 vs React Router framework mode

React Router (current)

Start here

Framework mode, new apps

  • Packages are react-router and @react-router/*
  • Same links() / <Links /> API as Remix v2
  • public/ still holds stable static filenames
  • Root route file is usually app/root.tsx

Remix v2

Existing apps

  • Packages are @remix-run/*
  • Upgrade path is mostly import renames once future flags are on
  • app/root.tsx and public/ layout are the same idea
  • Icon setup does not change when you rename packages

Where the files go

  • my-remix-app/
    • public/copied to the client build root
      • favicon.icoadd
      • favicon-32x32.pngadd
      • apple-touch-icon.pngadd180x180
      • icon-192.pngadd
      • icon-512.pngadd
      • site.webmanifestadd
    • app/
      • root.tsxlinks() lives here
      • routes/

Same layout whether the package.json still says remix or already says react-router.

app/root.tsx

import { Links, Meta, Outlet, Scripts } from "react-router";
// Remix v2: import those from "@remix-run/react" instead.

export function links() {
  return [
    { rel: "icon", href: "/favicon.ico", sizes: "any" },
    {
      rel: "icon",
      type: "image/png",
      sizes: "32x32",
      href: "/favicon-32x32.png",
    },
    { rel: "apple-touch-icon", href: "/apple-touch-icon.png" },
    { rel: "manifest", href: "/site.webmanifest" },
  ];
}

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}

Paths are root-absolute against public/. Dropping <Links /> from the shell silently drops every route's link declarations, including the favicon.

Design for the smallest size first

Example favicon at 16 by 16 pixels
16px
Example favicon at 32 by 32 pixels
32px
Example favicon at 48 by 48 pixels
48px
Example favicon at 96 by 96 pixels
96px
Example favicon at 180 by 180 pixels
180px
Rendered at true pixel dimensions. The 16px swatch is the one that decides whether a mark works as a favicon.

Remix favicon FAQ

Can I put the tags in a raw HTML file instead?

Framework mode apps do not ship a hand-edited index.html the way a Vite SPA does. The document shell is the root route component. If the tags are missing from links() or <Links /> is missing from the shell, they will not appear in the served HTML.

Should icons go through the Vite import graph?

No. Importing an image from app/ fingerprints the filename, which breaks every fixed href and every browser request for /favicon.ico. Keep icons in public/ so the path stays stable.

I upgraded from Remix to React Router and the icons vanished.

Check that root.tsx still exports links() and still renders <Links /> after the import rename. The public/ files do not move. A missing <Links /> in the new root shell is the usual cause.

Related platforms