Dynamic Favicons: A Practical Guide
Most sites treat the favicon as a static asset—set once in the <head> and forgotten. But the favicon is just an image source, and nothing stops you from updating it at runtime. Some sites use this for notifications, swapping in a red dot or badge when something needs attention. The same mechanism can do a lot more. Here's a clean way to make the favicon reflect the current time, using nothing but a canvas-generated data URL and a bit of JavaScript—plus a React-friendly wrapper for component-based projects.
Generating an emoji favicon
The core trick is drawing the emoji onto a canvas and exporting it as a data URL. This function takes an emoji and returns a valid image source:
// Thanks to https://formito.com/tools/favicon
const faviconHref = emoji =>
`data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22256%22 height=%22256%22 viewBox=%220 0 100 100%22><text x=%2250%%22 y=%2250%%22 dominant-baseline=%22central%22 text-anchor=%22middle%22 font-size=%2280%22>${emoji}</text></svg>`
To actually swap the favicon, target the existing <link rel="icon"> element and replace its href attribute. If that link doesn't exist, create it first:
const changeFavicon = emoji => {
// Ensure we have access to the document, i.e. we are in the browser.
if (typeof window === 'undefined') return
const link =
window.document.querySelector("link[rel*='icon']") ||
window.document.createElement("link")
link.type = "image/svg+xml"
link.rel = "shortcut icon"
link.href = faviconHref(emoji)
window.document.getElementsByTagName("head")[0].appendChild(link)
}
You can test both functions immediately in your browser's DevTools console. Paste them in and call changeFavicon("💃")—the favicon will update on the spot.
Mapping the time to an emoji
Clock emojis exist for each hour and half-hour mark. To pick the right one, round the current time to the nearest half-hour. For instance, anything from 9:45 to 10:14 should show the 10:00 clock; 10:15 to 10:44 shows the 10:30 clock. This function handles that mapping:
const currentEmoji = () => {
// Add 15 minutes and round down to closest half hour
const time = new Date(Date.now() + 15 * 60 * 1000)
const hours = time.getHours() % 12
const minutes = time.getMinutes() < 30 ? 0 : 30
return {
"0.0": "🕛",
"0.30": "🕧",
"1.0": "🕐",
"1.30": "🕜",
"2.0": "🕑",
"2.30": "🕝",
"3.0": "🕒",
"3.30": "🕞",
"4.0": "🕓",
"4.30": "🕟",
"5.0": "🕔",
"5.30": "🕠",
"6.0": "🕕",
"6.30": "🕡",
"7.0": "🕖",
"7.30": "🕢",
"8.0": "🕗",
"8.30": "🕣",
"9.0": "🕘",
"9.30": "🕤",
"10.0": "🕙",
"10.30": "🕥",
"11.0": "🕚",
"11.30": "🕦",
}[`${hours}.${minutes}`]
}
With the helper in place, a setInterval keeps the favicon current:
// One minute
const delay = 60 * 1000
// Change the favicon when the page gets loaded...
const emoji = currentEmoji()
changeFavicon(emoji)
// ... and update it every minute
setInterval(() => {
const emoji = currentEmoji()
changeFavicon(emoji)
}, delay)
That's all it takes for a vanilla JavaScript approach.
Bringing it into React
The imperative nature of this code—directly manipulating the DOM and running on an interval—doesn't map neatly to React's declarative model. The solution is a custom hook that encapsulates the side effect. Dan Abramov's article on making setInterval declarative explains the pattern: use useEffect to set up the interval and useState to hold the current emoji. Here's how it looks:
import { useEffect } from "react"
import useInterval from "./useInterval"
const delay = 60 * 1000
const useTimeFavicon = () => {
// Change the favicon when the component gets mounted...
useEffect(() => {
const emoji = currentEmoji()
changeFavicon(emoji)
}, [])
// ... and update it every minute
useInterval(() => {
const emoji = currentEmoji()
changeFavicon(emoji)
}, delay)
}
Calling useTimeFavicon() from the root component is enough to activate the behaviour site-wide.
Working with borrowed blocks
This example is a small case of a familiar engineering principle: break a problem into pieces, and re-use existing solutions where they fit. The canvas-to-data-URL conversion, the link manipulation, and the time-rounding logic each came from separate sources and were combined with minimal glue. The result is a favicon that updates itself every minute—whether you're using it for time, notifications, or something else entirely—without reinventing any of the underlying machinery.



