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-usefaviconimport { 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:
| Function | Purpose |
|---|---|
drawOnFavicon | Canvas-draw on top of the current favicon |
restoreFavicon | Reset to the original favicon |
setFaviconHref | Swap to any URL or data URI |
svgToFavicon | Render 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
| Feature | Chrome | Firefox | Safari |
|---|---|---|---|
Dynamic href swap | Yes | Yes | No |
| Canvas → data URI favicon | Yes | Yes | No |
| SVG favicon | Yes | Yes | Partial |
SVG prefers-color-scheme | Yes | Yes | No |
<link media="..."> (PNG) | Yes | Yes | Yes |
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
- Joel Califa — Tiny Wins — the case for small UX details like dynamic favicons.
- CSS-Tricks — SVG Favicons and All the Fun Things We Can Do With Them — dark mode, gradients, and dynamic SVG favicons.
- CSS-Tricks — Emojis as Favicons — the emoji-in-SVG technique.