How to Create an Animated Favicon with JavaScript (That Actually Works)
Alex Rivera
April 5, 2026 · 5 min read
You've seen sites with animated favicons — that little icon in the browser tab dancing, spinning, or cycling through frames. But if you've tried to just set a GIF as your favicon, you've discovered the painful truth: Chrome doesn't animate GIF favicons. ![]()
Firefox does. Safari doesn't. Chrome shows the first frame and calls it a day.
Here's how to make it work everywhere. ![]()
What You'll Build
A favicon that plays animated GIF frames by decoding them with gifuct-js, rendering to canvas, and swapping the favicon href at 60fps. Works in Chrome, Firefox, and Edge.
The Problem
Try this and see what happens:
<link rel="icon" type="image/gif" href="/animated.gif" />
Firefox: Animates perfectly. ![]()
Chrome/Edge: Shows first frame only. Static. ![]()
Safari: Shows first frame. Sometimes shows nothing.
The reason? Chrome intentionally doesn't animate favicons to save CPU/battery. There's an open Chromium issue about this from years ago. It's not a bug — it's a deliberate choice.
The Solution: Canvas + gifuct-js
The trick is to:
- Decode the GIF into individual frames using
gifuct-js - Render each frame to an offscreen canvas
- Convert the canvas to a PNG data URL
- Set it as the favicon href
- Repeat at the GIF's native frame rate
Step 1: Install gifuct-js
npm install gifuct-js
Step 2: The Component (React/Next.js)
"use client";
import { useEffect, useRef } from "react";
import { parseGIF, decompressFrames } from "gifuct-js";
export function AnimatedFavicon() {
const frameIdx = useRef(0);
const timerRef = useRef(undefined);
useEffect(() => {
// Create favicon link element
const link = document.createElement("link");
link.rel = "icon";
link.type = "image/png";
document.head.appendChild(link);
// Canvas for rendering frames
const canvas = document.createElement("canvas");
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext("2d");
let frames = [];
let delays = [];
let stopped = false;
// Decode the GIF
fetch("/favicon-animated.gif")
.then(r => r.arrayBuffer())
.then(buff => {
const gif = parseGIF(buff);
const decoded = decompressFrames(gif, true);
// Composite each frame
const temp = document.createElement("canvas");
temp.width = 32; temp.height = 32;
const tCtx = temp.getContext("2d");
for (const frame of decoded) {
const patch = new ImageData(
new Uint8ClampedArray(frame.patch),
frame.dims.width, frame.dims.height
);
if (frame.disposalType === 2)
tCtx.clearRect(0, 0, 32, 32);
const p = document.createElement("canvas");
p.width = frame.dims.width;
p.height = frame.dims.height;
p.getContext("2d").putImageData(patch, 0, 0);
tCtx.drawImage(p, frame.dims.left, frame.dims.top);
frames.push(tCtx.getImageData(0, 0, 32, 32));
delays.push(frame.delay || 80);
}
if (!stopped) drawFrame();
});
function drawFrame() {
if (stopped || !frames.length) return;
ctx.putImageData(frames[frameIdx.current], 0, 0);
link.href = canvas.toDataURL("image/png");
const delay = delays[frameIdx.current] || 80;
frameIdx.current = (frameIdx.current + 1) % frames.length;
timerRef.current = setTimeout(drawFrame, delay);
}
return () => { stopped = true; clearTimeout(timerRef.current); link.remove(); };
}, []);
return null;
}
Step 3: Use It
// In your layout.tsx or _app.tsx
import { AnimatedFavicon } from "./AnimatedFavicon";
export default function Layout({ children }) {
return (
<html>
<body>
<AnimatedFavicon />
{children}
</body>
</html>
);
}
How It Works Under the Hood
1️⃣
Fetch GIF
Download as ArrayBuffer
2️⃣
Decode Frames
gifuct-js extracts each frame + delay
3️⃣
Composite
Handle disposal, draw patches onto canvas
4️⃣
Swap Favicon
canvas.toDataURL → link.href on timer
Why not just use drawImage on an <img>?
You might think: load the GIF in an <img>, draw it to canvas each frame. The problem is Chrome's drawImage() only captures the current displayed frame of a GIF — and Chrome doesn't advance frames for offscreen images. You get a static image. The gifuct-js approach decodes all frames upfront, bypassing this limitation entirely.
Bonus: Rotating Multiple Animated Emoji
Want to cycle through different animated emoji in your favicon? Create a combined GIF with all emoji playing in sequence:
# Python — combine multiple GIFs into one favicon
from PIL import Image
emojis = ['party-popper', 'fire', 'rocket', 'heart']
frames, durations = [], []
for name in emojis:
with Image.open(f'{name}.gif') as gif:
for i in range(gif.n_frames):
gif.seek(i)
frame = gif.convert('RGBA').resize((32, 32))
frames.append(frame.copy())
durations.append(gif.info.get('duration', 80))
frames[0].save('favicon-animated.gif',
save_all=True, append_images=frames[1:],
duration=durations, loop=0, disposal=2)
This creates a single GIF that plays through each emoji in sequence. The JavaScript component above will handle the rest — it doesn't care if it's one emoji or twenty. ![]()
Performance Tip
Keep your favicon GIF at 32×32 pixels. Larger sizes mean larger data URLs on every frame swap, which can cause jank. 32px is the standard favicon size and Chrome/Firefox will display it at 16px anyway.
Browser Support
| Browser | Native GIF Favicon | With gifuct-js |
|---|---|---|
| Chrome | ❌ Static only | ✅ Animated |
| Firefox | ✅ Animated | ✅ Animated |
| Edge | ❌ Static only | ✅ Animated |
| Safari | ❌ Static only | ⚠️ Updates slowly |
Try It Live
This exact technique powers the animated favicon on AnimatedEmojies. Our favicon cycles through 20 different animated emoji — party popper, fire, rocket, heart, and more — all decoded and rendered frame-by-frame in the browser tab.
Sources & References
- The Making of an Animated Favicon — CSS-Tricks
- animated-favicon library — GitHub
- gifuct-js — GIF frame decoder — npm
- Chromium Animated Favicon Issue — Chromium Dashboard