UI Patterns
Hooks

Favicon

Dynamically update the browser favicon — swap icons, draw badges, show progress, or render emoji.

The favicon is a tiny surface, but a useful one. Gmail overlays an unread count, Slack flips between grey and white to signal activity, and Google Calendar renders today's date. All of these rely on swapping or drawing on the <link rel="icon"> element at runtime.

Simple swap

The most common case is switching between a fixed set of icons (e.g. light vs dark, active vs idle). Every major React hooks library ships a useFavicon hook that does this:

import { useFavicon } from "react-use"; // or @mantine/hooks, ahooks, usehooks-ts

useFavicon("/favicons/active.png");

Under the hood, all of them do the same thing:

document.querySelector("link[rel~='icon']").href = newUrl;

If you only need to swap between known URLs, this is all you need.

React 19+

React 19 hoists <link> tags rendered inside components into <head> automatically, so you can skip the hook entirely:

function App({ active }: { active: boolean }) {
  return <link rel="icon" href={active ? "/active.png" : "/idle.png"} />;
}

Canvas-based drawing

When you need to draw on top of the existing favicon — a notification dot, an unread count, a progress arc — you need a canvas. The react-usefavicon package wraps this pattern in a clean hook:

npm install react-usefavicon
import { useFavicon, drawCircle, drawTextBubble } from "react-usefavicon";

function Notifications({ count }: { count: number }) {
  const { drawOnFavicon, restoreFavicon } = useFavicon();

  useEffect(() => {
    if (count > 0) {
      drawOnFavicon(drawTextBubble, { label: String(count) });
    } else {
      restoreFavicon();
    }
  }, [count, drawOnFavicon, restoreFavicon]);

  return <span>{count} notifications</span>;
}

The hook returns four stable functions:

FunctionPurpose
drawOnFaviconCanvas-draw on top of the current favicon
restoreFaviconReset to the original favicon
setFaviconHrefSwap to any URL or data URI
svgToFaviconRender a JSX <svg> element as the favicon

Built-in draw helpers

Three helpers cover the most common overlays:

import { drawCircle, drawTextBubble, drawSquare } from "react-usefavicon";

// Red notification dot in the bottom-right corner
drawOnFavicon(drawCircle, { fillColor: "red", radius: 40, x: 200, y: 200 });

// Rounded badge with a number
drawOnFavicon(drawTextBubble, {
  label: "3",
  color: "orangered",
  fontSize: 128,
});

// Filled square
drawOnFavicon(drawSquare, { fillColor: "black", length: 50, x: 200, y: 200 });

All options have sensible defaults — drawOnFavicon(drawCircle) with no options gives you a red dot in the bottom-right corner.

Custom draw callback

For anything beyond the built-ins, pass your own callback. It receives the canvas context and the favicon size:

drawOnFavicon((ctx, size) => {
  // Green status dot
  ctx.fillStyle = "limegreen";
  ctx.beginPath();
  ctx.arc(size - 30, size - 30, 25, 0, Math.PI * 2);
  ctx.fill();
});

Successive drawOnFavicon calls stack. Call restoreFavicon() first if you want a clean slate.

Emoji favicon

A quick way to set a distinctive favicon without any image assets:

import { useFavicon, emojiSvg } from "react-usefavicon";

const { setFaviconHref } = useFavicon();
setFaviconHref(`data:image/svg+xml,${emojiSvg("🔥")}`);

This works by embedding the emoji in an SVG <text> element and encoding it as a data URI. No canvas, no image files.

Without the library, the same trick in plain HTML:

<link
  rel="icon"
  href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔥</text></svg>"
/>

Dark mode favicon

SVG favicons can embed a prefers-color-scheme media query, so the icon adapts to the OS theme without any JavaScript:

<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    rect { fill: #111; }
    @media (prefers-color-scheme: dark) {
      rect { fill: #eee; }
    }
  </style>
  <rect width="32" height="32" rx="4" />
</svg>

For broader browser support, use separate <link> tags with media attributes — this works everywhere including Safari:

<link
  rel="icon"
  href="/favicon-light.png"
  media="(prefers-color-scheme: light)"
/>
<link
  rel="icon"
  href="/favicon-dark.png"
  media="(prefers-color-scheme: dark)"
/>

Note: the media query responds to the OS-level dark mode setting, not a custom theme toggle on your site. If your app has its own dark mode switch, you need JavaScript to swap the favicon.

Browser support

FeatureChromeFirefoxSafari
Dynamic href swapYesYesNo
Canvas → data URI faviconYesYesNo
SVG faviconYesYesPartial
SVG prefers-color-schemeYesYesNo
<link media="..."> (PNG)YesYesYes

Safari intentionally blocks JavaScript-based favicon updates after page load. There is no workaround. If Safari support matters, use the <link media="..."> approach for dark mode, and fall back to document.title for notification counts (e.g. (3) My App).

Framework-agnostic alternatives

If you're not using React, these vanilla JS libraries offer similar canvas-based favicon drawing:

  • magic-favicon — ~2.5 KB, zero deps, actively maintained. Progress bars, pie charts, badges, status icons, pulse/spin animations.
  • Tinycon (~5.1k stars) — lightweight alert bubbles. Dormant but functional.
  • favico.js (~8.7k stars) — the original. Badges, images, animated overlays. Unmaintained.

Further reading

On this page