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
Featured Animated Emojis for Developers
Drop these into your README, changelog, build status banner, or that one Slack channel your whole team watches. Free to download in WebP, GIF, or Lottie JSON — no account required.



