Blinking Terminal
This site's favicon is a tiny terminal prompt with a blinking cursor, which is my personal favourite. Here's how to animate a favicon reliably with a canvas, two frames, and a timer.
This site's favicon blinks for only 30 seconds. If you're here for longer, you may not see the favicon blink, and you might need to reload the page. See Q3 in the Production-Grade section to understand why.
This blog has long code examples. For the best experience, view on desktop or landscape mode.
I spent almost a week animating a 16-pixel favicon. Why? Because it's the only UI element visible when your page isn't active, or buried under 20 other tabs.
The result: a dark retro terminal prompt that blinks steadily in the browser tab. Here's the complete blueprint—and why it's harder than it should be.
The Swapping File Approach
The intuitive approach is putting two static PNG files in /public (favicon-on.png and favicon-off.png) and running a setInterval timer to swap the href attribute of <link rel="icon">:
In real browsers, this fails because:
- Aggressive Browser Caching: Browsers cache favicon URLs aggressively in memory threads and often ignore HTTP cache headers.
- Competing Link Tags: Multiple fallback tags (like
apple-touch-icon.pngorfavicon.ico) cause browsers to pin one static candidate and ignore dynamic updates.
Why? A Working Demo.
To reproduce and verify why static file swapping fails, you can test this in your locale environment, by creating a BrokenFaviconTest() component.
Open DevTools → Network tab → check "Disable cache". Watch the browser tab favicon for 10 seconds:
- Static file swaps: Produce
0requests after initial load (favicon stays frozen). - Data URLs: Update instantly with no additional network requests.
The Solution: Canvas Data URLs
To bypass browser file-caching and file-swapping limitations, we generate a unique in-memory base64 Data URL PNG on the fly using HTML5 Canvas 2D and dynamically swap the <link rel="icon"> tag in document.head.
*What is a Data URL?* A Data URL (e.g. data:image/png;base64,iVBORw0KG...) embeds raw pixel data directly inside a text string. Because the browser receives the raw image payload inline rather than requesting a file from a server over HTTP, frame swaps execute instantly without browser caching locks or network requests.
Project Setup & Fallback Strategy
You can set up your project in two ways:
- Option 1: Clone the pre-configured Playground Starter Repository for a ready-to-go Vite + React + Tailwind CSS environment.
- Option 2: Initialize a fresh Vite + React + TypeScript app using
npm create vite@latest my-terminal-app -- --template react-ts. or bootstrap it with your favourite flavor.
Always keep a static favicon.ico in /public as a fallback, while our JS engine dynamically takes over the browser tab at runtime. Web crawlers (Googlebot), RSS readers, and mobile home screen shortcuts cannot execute JavaScript animation loops.
Canvas 2D Frame Generator Engine
Create src/utils/blinkingFavicon.ts. This file contains the vector rendering logic, presets, and error handling:
Atomic DOM Link Tag Replacer
In the same file src/utils/blinkingFavicon.ts, implement the DOM updater function:
React Lifecycle Custom Hook
Create src/hooks/useBlinkingFavicon.ts to connect the engine to your React application lifecycle:
Calling & Rendering in Your Application
Now simply call your hook in src/App.tsx using presets or custom options:
How to Inspect Tab Favicons
The easiest way to inspect such dynamic favions is:
- Inspect Element: Open Chrome/Firefox DevTools, expand
<head>, and select the<link rel="icon">node. You can observe thehrefattribute toggling between two Data URLs in real-time.

- Network Tab Filtering: Filter DevTools Network panel by "data:" or "Img" to verify no external network roundtrips are occurring during frame swaps.
Performance & Optimization Highlights
Pre-Rendered Buffer Benchmark: We call drawFrame only once per state change up front. Each frame swap is a ~0.3ms DOM operation vs. ~8ms canvas render per cycle. With pre-rendering, you trade negligible memory (~500 bytes) for 26× faster frames without triggering canvas rasterization inside setInterval.
Accessibility: Respects prefers-reduced-motion: reduce system preferences automatically.
Clean Unmount: clearInterval ensures background timers don't persist across route transitions.
Before rolling out dynamic favicons, keep these platform-specific behaviors in mind:
- iOS Safari: Completely ignores dynamic favicon Data URLs and only respects the static
apple-touch-icon.pngdefined at initial page load. - Firefox Caching: Firefox caches Data URLs differently than Chromium engines; tearing down old
<link>elements explicitly viael.remove()is required to prevent stale tab states. - OS Dark Mode Overrides: Operating system dark/light mode preference media queries (
prefers-color-scheme) do not apply to base64 Data URLs inside<link rel="icon">. All themes must be styled directly inside the canvas renderer.
Production-Grade Disclaimer & Critical Engineering Q&A
Before deploying a dynamic blinking favicon to a high-traffic production application, consider these real-world engineering constraints:
Q1: Will this override the browser being idle or drain laptop battery?
Yes, if unoptimized. Standard setInterval timers run continuously. Modern browsers throttle background tab timers to 1Hz or 1 update/minute to save power, but active tab execution keeps CPU cores awake.
Production Fix: Use the Page Visibility API. Pause the timer when document.hidden === true and resume when the tab regains focus:
Q2: Will it eat RAM or cause memory leaks over time?
Not if pre-rendered.
- Heap Allocation: Generating new Data URLs inside the interval loop allocates ~1.2KB base64 strings every cycle, causing unnecessary Garbage Collection (GC) pauses. Pre-rendering
frameOnandframeOffinto two static string references once up front completely eliminates heap allocation during animation cycles. - DOM Node Leaks: Always invoke
el.remove()on old<link>elements before appending new ones. If omitted, thousands of orphaned DOM nodes will accumulate in<head>.
Q3: Is it really good practice to ship this in Production?
Not really. Use sparingly.
- Visual Distraction: A constantly flashing icon in a user's tab strip creates visual noise and can be distracting when users have 20+ tabs open.
- Production Recommendation:
- Stop the blink loop after 10–15 seconds (or 30 seconds) and lock the cursor lit:
- Or use dynamic favicons exclusively for state changes (e.g. flashing when an unread notification arrives or a background deployment completes).
Q4: Does this break SEO or Mobile PWA installation?
No, as long as static fallbacks exist. Web crawlers (Googlebot) and PWA manifest engines read static favicon.ico and apple-touch-icon.png directly from static HTTP requests without executing JavaScript.
Q5: Can I animate other browser UI with canvas data URLs?
Yes, but with caveats. Canvas Data URLs can also be applied to apple-touch-icon, PWA manifest icons, and <meta name="theme-color"> content preview elements. However, browser tab favicons (<link rel="icon">) are by far the most widely supported and reliable target for dynamic Data URL updates.
Try the Interactive Playground
Experiment below with real-time browser tab favicon synchronization, customize blink speeds, select retro CRT color palettes (Mint Emerald vs Retro Crimson), and inspect the 2D canvas frame buffer output live!
Engine Configuration
2D Canvas Buffer Inspector
Have feedback or edge-case browser findings?
If you spot an edge-case browser behavior or have suggestions to improve this blueprint, reach out directly using our interactive contact form.