Animated Favicons: Make Your Browser Tab Move (With a Live Demo)

6 min read Original article ↗

You probably noticed something move at the top of this tab while reading. That's an animated favicon — the same trick Gmail uses for new mail counts and Discord uses for notification dots, except yours can do basically anything you can draw on a canvas.

This article includes a live demo you can poke at. Click a button, watch your actual browser tab change. No screenshots, no embedded videos — the favicon you're looking at right now becomes the demo.

Why Animate a Favicon at All?

Honestly, most sites shouldn't. A spinning icon in every tab gets annoying fast, and it eats CPU. But there are a few cases where it's genuinely useful:

  • Loading or processing states. Long-running uploads, exports, or builds. Users switch tabs while they wait, and an animated favicon tells them the work is still happening.
  • Notification badges. New messages, mentions, alerts. A red dot that subtly pulses gets noticed faster than a static one.
  • Live data feeds. Trading dashboards, monitoring tools, sports scores — anywhere the tab title isn't enough.
  • Brand moments. A holiday spinner, a launch-day celebration. Use sparingly.

If your use case isn't one of those, skip the animation. A good static SVG favicon already wins on file size, dark mode, and battery life.

Live Demo: Try It Right Now

Pick an animation. Then look at your browser tab — that tiny icon up top is what's being redrawn.

The real favicon is 16×16 pixels. Hard to see detail at that size, so the box on the left mirrors the same canvas at 8× with nearest-neighbor scaling.

Status: idle

The animation loop now runs entirely inside a Web Worker, just like favicon_worker.js in the Aymkdn library. Every 20ms (50fps) the worker draws to its OffscreenCanvas, exports it via convertToBlob + FileReader, and posts the resulting data URL back to the page. The main thread only does one thing: assign that string to faviconLink.href. That's why the icon now moves as smoothly as the GitHub demo.

Want to see this pattern shipped in a real product? Random Picker Wheel uses an animated favicon that mirrors its spinning wheel — a rotation-based UI is one of the rare cases where a moving icon genuinely fits the product. Open the page, give the wheel a spin, and watch the tab icon spin right along with it.

How It Actually Works

Three steps. That's the whole technique:

// 1. Get or create the favicon link element
let link = document.querySelector('link[rel~="icon"]');
if (!link) {
  link = document.createElement('link');
  link.rel = 'icon';
  document.head.appendChild(link);
}

// 2. Draw a frame to a hidden canvas
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext('2d');

function drawFrame(t) {
  const scale = 0.5 + 0.5 * Math.abs(Math.sin(t / 400));
  ctx.clearRect(0, 0, 32, 32);
  ctx.fillStyle = '#ef4444';
  ctx.beginPath();
  ctx.arc(16, 16, 14 * scale, 0, Math.PI * 2);
  ctx.fill();

  // 3. Export the canvas to a data URL and assign it to the favicon
  link.href = canvas.toDataURL('image/png');

  requestAnimationFrame(drawFrame);
}

requestAnimationFrame(drawFrame);

That's roughly 15 lines for a working pulsing dot. Everything fancier is just drawing different shapes on the canvas.

The Browser Support Reality

Here's where it gets ugly. Browsers vary a lot in how aggressively they animate the favicon:

  • Firefox: animates smoothly even when the tab isn't focused. It's the gold standard.
  • Chrome / Edge: animate while the tab is active. When you switch away, requestAnimationFrame throttles down to roughly once per second, so animation slows or pauses.
  • Safari: animates while focused but sometimes only updates the icon at slow intervals. Don't count on smooth motion.

This is actually fine for the most common use cases — notification dots and progress states only need to update every second or so anyway. Smooth 60fps spinners are mostly cosmetic.

The Web Worker Trick (for Background Tabs)

The "GitHub-Style Flip" button above is a direct port of the Aymkdn/animated-favicon library's signature animation: hold icon A for 3 seconds, cosine-compress its width down to zero, swap to icon B, expand it back. The math comes straight from the library's favicon_worker.jswidth = canvas.width * Math.abs(Math.cos(progress * Math.PI)), with the second image taking over once progress crosses 0.5.

Aymkdn's library goes one step further than what we're doing here: it runs that loop inside a Web Worker. Workers don't get throttled when the tab is in the background, so the animation continues, and OffscreenCanvas lets the worker render frames without touching the DOM.

The pattern looks roughly like this:

// In your page
const worker = new Worker('favicon-worker.js');
worker.onmessage = (e) => {
  if (e.data.type === 'updateFavicon') {
    document.querySelector('link[rel~="icon"]').href = e.data.dataUrl;
  }
};
worker.postMessage({ type: 'init', images: ['icon-a.png', 'icon-b.png'] });

// In favicon-worker.js
const canvas = new OffscreenCanvas(16, 16);
const ctx = canvas.getContext('2d');
// ...draw a frame...
const blob = await canvas.convertToBlob();
const reader = new FileReader();
reader.onloadend = () => self.postMessage({ type: 'updateFavicon', dataUrl: reader.result });
reader.readAsDataURL(blob);

Worth it if your app is the kind of thing users park in a background tab — chat clients, build dashboards, monitoring tools. Otherwise the main-thread version is simpler and good enough.

A Few Things Worth Knowing

Use 16×16 or 32×32, not bigger. The favicon is rendered tiny anyway, and bigger canvases mean larger data URLs and more CPU per frame. 32×32 with crisp pixels is the sweet spot.

Set the favicon as PNG, not ICO. canvas.toDataURL('image/png') is the only thing that works reliably. Don't try to encode ICO yourself.

Restore the original favicon when you stop. Save link.href before you start animating and restore it on beforeunload or when the operation completes. Tabs that keep showing a half-broken animation after navigation look buggy.

Don't animate forever. Even a subtle pulse drains battery on mobile. Stop the animation when the loading state ends, when the user reads the notification, or when the tab loses focus.

Skip it for simple cases. If you just want to show "1 unread message", change the favicon to a static red-dot version. No animation needed. Most of what people use animated favicons for is overkill.

When to Reach for This

Animated favicons are a great fit when:

  • The animation reflects real state the user cares about (uploading, processing, new message)
  • The page is the kind of thing that lives in a background tab
  • A static badge or just a tab title change wouldn't communicate the same thing

They're a bad fit when:

  • It's just decoration
  • The animation runs the entire time the tab is open
  • You're targeting users on Safari and Mobile where it barely works

Build the demo above into your own project, swap the colors and shapes for your brand, and ship it. The full source is right here in this page — view source, copy, adapt.

References

  1. Aymkdn/animated-favicon on GitHub — Web Worker-based animated favicon library that keeps animating in inactive tabs
  2. The Making of an Animated Favicon — CSS-Tricks — Chris Coyier's walkthrough of the canvas-to-favicon technique
  3. How to animate a favicon? — Stack Overflow — The classic thread with multiple approaches and browser support notes
  4. OffscreenCanvas — MDN — The API that makes the Web Worker animation pattern possible