Contents
  1. Key terms
  2. When a favicon loading indicator makes sense
  3. Choose an approach
  4. Prepare the vector artwork
  5. Swap the icon with JavaScript
  6. Alternative: animation inside the SVG
  7. Alternative: canvas progress for a percentage
  8. Verify the result
  9. Accessibility
  10. Gaps and cautions

A favicon cannot run its own loading logic, so the page shows a loading indicator by changing which icon file it points to. Keep a static vector favicon as the default and prepare one or more "busy" versions. A busy version can be a single icon with a badge or a short sequence of spinner frames. While work is in progress, use JavaScript to change the href of the page's icon link element, then restore the original icon when the work finishes. An SVG favicon with built-in animation is an alternative, but browsers handle animation inside favicons inconsistently. Treat it as an enhancement, not the only signal.

Key terms

  • Favicon: the small icon a browser shows in tabs, bookmarks and history. The page declares it with a link element whose rel attribute contains "icon".
  • SVG (Scalable Vector Graphics): a text-based vector format that describes shapes as geometry rather than pixels, so one file can render sharply at any size.
  • viewBox: the SVG attribute that defines the drawing's internal coordinate system. Keep it identical across every state of the icon so the artwork doesn't shift when the icon is swapped.
  • Path: a vector shape defined by points and curves. In a favicon, the logo and the spinner arc should both be paths or basic shapes, not live text or embedded images.
  • Frame: one still image in a sequence. Swapping frames quickly gives the impression of motion.
  • Data URL: an image encoded directly into a text string. It lets a script set a generated icon without a separate file.

When a favicon loading indicator makes sense

Browsers already replace the tab icon with their own spinner during full page navigation, so you don't need a custom indicator for normal page loads. A custom favicon state is useful for work the browser can't see. Examples include background data requests in a single-page app, uploads, exports, or long-running jobs. Its main value is telling someone in another tab that the work is still running or has finished. The indicator inside the page itself should remain the primary signal.

Choose an approach

  • Static state swap: two icons, idle and busy. For example, the busy version is the logo with a small arc or dot added. It is the most reliable option because it needs no timers and no animation support.
  • Frame cycling: several SVG frames in which only the spinner arc rotates, swapped on a timer. This produces visible motion. However, browsers commonly throttle timers in background tabs, so the motion may stutter or pause exactly when the user is looking from another tab.
  • Animated SVG favicon: CSS or SVG animation inside a single icon file. It is the simplest to deploy, but some browsers show only a static render of the file.
  • Canvas-drawn icon: a script draws the logo plus a progress arc onto a canvas and sets the result as the icon. This approach suits determinate progress, such as a percentage. The output is a raster image, not vector artwork.
  • Recommendation: start with the static swap. Add frame cycling or canvas progress only if the extra motion or detail serves a real purpose.

Prepare the vector artwork

A favicon is the logo at its smallest practical size. A loading indicator has to share that tiny space with the brand mark, so start from a simplified mark or monogram rather than the full lockup. Drop the wordmark, taglines and fine details that will not survive downscaling.

  • 1. Place the simplified mark on a square artboard and give the SVG a square viewBox.
  • 2. Decide where the indicator lives. A ring around the mark or a corner badge both work. Keep the mark itself static so the brand stays recognizable in every state.
  • 3. Draw the spinner as a single stroked arc on a circle that is concentric with the icon. Use round line caps, and make the stroke heavy enough to read at the smallest size you support.
  • 4. Convert any lettering to outlines. A favicon should not depend on a font being available.
  • 5. Keep each file self-contained, with no external images, stylesheets or scripts. Browsers render SVG favicons as images, and external resources and scripts are not available in that context.
  • 6. Check contrast against both light and dark browser themes. Some browsers honor a prefers-color-scheme media query inside the SVG, but don't rely on it alone. Choose colors that work on both backgrounds where possible.
  • 7. If you use frames, export every frame with the same viewBox and the same mark geometry, changing only the arc's rotation. This prevents the logo from jumping between frames.
  • 8. Keep a PNG or ICO fallback of the idle icon for browsers or contexts that don't use SVG favicons.

The snippet below shows the structure of a busy-state icon. The circle stands in for your outlined logo paths. Replace the namespace placeholder with the standard SVG namespace attribute value that your vector editor writes on export.

<svg xmlns="[standard SVG namespace]" viewBox="0 0 32 32">
  <!-- Static mark: replace with your outlined logo paths -->
  <circle cx="16" cy="16" r="8" fill="#1a1a1a"/>
  <!-- Busy arc: a quarter circle around the mark -->
  <path d="M16 3 A13 13 0 0 1 29 16"
        fill="none" stroke="#1a1a1a" stroke-width="4"
        stroke-linecap="round"/>
</svg>

To make rotation frames, wrap the arc in a group and change only its transform from frame to frame. Use rotate(90 16 16), rotate(180 16 16) and so on, so the arc rotates around the center of the 32-unit viewBox.

Swap the icon with JavaScript

The script below targets the SVG icon link specifically. Many sites declare an ICO or PNG fallback alongside the SVG, and changing the wrong link has no visible effect. The script remembers the original icon, cycles through busy frames while work is running, and shows a single still busy frame for users who have asked their system to reduce motion. The file names and the interval value are examples to replace and tune.

const link = document.querySelector('link[rel~="icon"][type="image/svg+xml"]');
const IDLE_ICON = link.href;
const BUSY_FRAMES = [
  'favicon-busy-0.svg',
  'favicon-busy-1.svg',
  'favicon-busy-2.svg',
  'favicon-busy-3.svg'
];

let busy = false;
let timer = null;
let frame = 0;

function showFaviconLoading() {
  if (busy) return;
  busy = true;
  frame = 0;
  link.href = BUSY_FRAMES[0];
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  timer = setInterval(() => {
    frame = (frame + 1) % BUSY_FRAMES.length;
    link.href = BUSY_FRAMES[frame];
  }, 150);
}

function hideFaviconLoading() {
  busy = false;
  clearInterval(timer);
  timer = null;
  link.href = IDLE_ICON;
}

Wrap the work so the idle icon always comes back, even if the task fails:

showFaviconLoading();
try {
  await runLongTask();
} finally {
  hideFaviconLoading();
}

Each frame is fetched the first time it is shown, which can cause a brief flicker. Serve the frame files with caching enabled, or embed them as data URLs in the script, so swaps after the first cycle are immediate. For a static state swap, use a one-item frame list. The interval then never matters.

Alternative: animation inside the SVG

You can put a CSS keyframe rotation or an SVG animate element on the arc group and point the favicon at that single file. This avoids timers entirely. The tradeoff is that some browsers render favicons as a still image, in which case users see only the starting position of the arc. Design that starting position so it still reads as a busy state on its own. Also include a prefers-reduced-motion media query inside the SVG to stop the rotation where users have requested less motion.

Alternative: canvas progress for a percentage

If the task reports progress, a script can draw the idle icon onto a canvas, stroke an arc whose length matches the percentage, and set the canvas output as the icon's href. The vector artwork remains the source, but the delivered icon is a raster PNG. Draw at the pixel size the browser displays, or a multiple of it, to keep edges clean. Redraw only when the percentage changes rather than on every animation tick.

Verify the result

  • Start a task, switch to another tab, and confirm the busy icon appears in the original tab's icon.
  • Let the task finish and confirm the idle icon returns. Repeat with a task that fails, to confirm the finally block restores it.
  • Trigger the task twice in quick succession and confirm the frames don't speed up or get stuck. The busy flag is there to prevent this.
  • Turn on the operating system's reduce-motion setting and confirm a still busy icon is shown.
  • Check the icon in both light and dark browser themes and at the smallest tab size.
  • Open the page in each browser you support. Note which ones animate, which show a still frame, and which use the PNG or ICO fallback instead of the SVG.
  • In the developer tools element inspector, confirm that the href of the SVG icon link changes as expected.

Accessibility

Screen readers do not announce favicon changes, and some users never look at the tab strip. Pair the favicon state with an in-page status message in an aria-live region. Optionally, also update the document title, for example by prefixing it while work is running, because the title is visible in the tab and available to assistive technology.

Gaps and cautions

No confirmed evidence was supplied about which browser versions animate SVG favicons, how aggressively each browser throttles background timers, or how each browser chooses between multiple declared icons. Treat those points as things to test in your target browsers, not as settled behavior. When sourcing logo artwork for the icon, don't assume a file is fully vector or editable just because it ends in .svg. Open it and check that it contains paths rather than an embedded raster image. The file format also says nothing about whether you have permission to reuse or modify the logo.