AnimatEmojisAnimatEmojis
How to Create an Animated Favicon with JavaScript (That Actually Works)

How to Create an Animated Favicon with JavaScript (That Actually Works)

A

Alex Rivera

April 5, 2026 · 5 min read

tutorialjavascriptfaviconanimationweb developmentgifuct-js

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. emoji

Firefox does. Safari doesn't. Chrome shows the first frame and calls it a day.

Here's how to make it work everywhere. emoji

emoji 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.

emoji The Problem

Try this and see what happens:

<link rel="icon" type="image/gif" href="/animated.gif" />

Firefox: Animates perfectly. emoji
Chrome/Edge: Shows first frame only. Static. emoji
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.

emoji The Solution: Canvas + gifuct-js

The trick is to:

  1. Decode the GIF into individual frames using gifuct-js
  2. Render each frame to an offscreen canvas
  3. Convert the canvas to a PNG data URL
  4. Set it as the favicon href
  5. 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>
  );
}

emoji 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

emoji 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.

emoji 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. emoji

emoji 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.

emoji 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

emoji 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.

See It In Action

Check the browser tab!

gifuct-js on npm

GIF frame decoder


Sources & References


emoji 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.

Laptop Fire emoji

Laptop Fire

for production incidents

Fire emoji

Fire

for ships and bugs alike

Rocket emoji

Rocket

deploy day essential

Sparkles emoji

Sparkles

for clean refactors

Heart Pixel Spin emoji

Pixel Heart

retro game vibes

100 emoji

100

perfect test coverage

Party Parrot emoji

Party Parrot

the OG dev emoji

Nyan Cat emoji

Nyan Cat

endless CI running

Browse all 1,100+ animated emojis →