Iconello
Tutorial2026-03-08· 5 min read

How to Test Your Favicon Across All Browsers and Devices

Your favicon looks great in Chrome but broken on Safari? A systematic testing checklist with tools and common failure modes.

You uploaded a beautiful favicon, deployed your site, and it looks perfect in Chrome on your laptop. Then you open it on an iPhone and the home screen shows a blank white square. Your colleague on Windows sees a pixelated mess in their taskbar. A friend shares your link on Slack and the preview shows no image at all.

Favicon testing is unglamorous but necessary. Each browser and platform has its own rules about which file it picks, what size it expects, and how it handles missing formats. Here is a systematic way to catch problems before your users do.

The Testing Checklist

TestWhat to checkHow to test
Browser tab (Chrome)Icon is sharp, recognizableOpen site in Chrome
Browser tab (Firefox)Same icon, same qualityOpen in Firefox
Browser tab (Safari)Icon displays, not pixelatedOpen in Safari
Bookmark barRecognizable at small sizeBookmark the page, check bar
iOS home screen180x180 icon, no white boxAdd to Home Screen on iPhone
Android shortcutAdaptive icon, correct maskAdd to Home Screen on Android
Dark mode tabIcon visible on dark chromeEnable dark mode in OS
Social share previewOG image shows correctlyPaste URL in Slack/Twitter
Google search resultFavicon shows next to URLSearch for your domain

Chrome DevTools Method

The fastest way to debug favicon issues is Chrome DevTools. Open your site, go to the Application tab, and look at the Manifest section. It shows every icon your manifest declares, their sizes, and whether they loaded successfully.

For the basic favicon (tab icon), check the Network tab and filter by "favicon" or "icon". You will see which requests the browser made and which file it actually loaded. A 404 on any of these requests means a missing file.

Quick console check
// Run in browser console to check all favicon-related link tags
document.querySelectorAll('link[rel*="icon"]').forEach(el => {
  console.log(el.rel, el.href, el.sizes?.value || 'no size')
})

Chrome's favicon database

Chrome stores favicons in a separate SQLite database, not in the regular browser cache. On Windows it lives at %LOCALAPPDATA%\Google\Chrome\User Data\Default\Favicons, on macOS at ~/Library/Application Support/Google/Chrome/Default/Favicons, and on Linux at ~/.config/google-chrome/Default/Favicons. Clearing your browser cache does not touch this file. If you are stuck with a stale favicon during development, you can delete this database (with Chrome closed) to force a complete re-fetch of every favicon.

Automated Testing with Playwright

Manual testing catches obvious problems, but it does not scale. If you have dozens of pages or deploy multiple times a day, you need automated checks. Playwright can verify that every expected favicon file exists, returns the correct MIME type, and has the right dimensions.

The following test script hits your live (or staging) URL, fetches each favicon reference from the HTML, and validates the response. It catches broken paths, missing files, wrong content types, and incorrectly sized images — all before a human ever opens the page.

tests/favicon.spec.ts
import { test, expect } from '@playwright/test'

const BASE_URL = process.env.BASE_URL || 'http://localhost:3000'

test.describe('Favicon validation', () => {
  test('all favicon link tags return valid responses', async ({ page }) => {
    await page.goto(BASE_URL)

    // Collect every <link> tag related to icons
    const iconLinks = await page.$$eval(
      'link[rel*="icon"]',
      (els) => els.map((el) => ({
        rel: el.getAttribute('rel'),
        href: el.getAttribute('href'),
        sizes: el.getAttribute('sizes'),
        type: el.getAttribute('type'),
      }))
    )

    expect(iconLinks.length).toBeGreaterThan(0)

    for (const link of iconLinks) {
      const url = new URL(link.href!, BASE_URL).toString()
      const res = await fetch(url)
      expect(res.status, `${link.rel} (${link.href}) returned ${res.status}`).toBe(200)

      // Validate MIME type matches declared type
      const contentType = res.headers.get('content-type') || ''
      if (link.type) {
        expect(contentType).toContain(link.type)
      }
      // ICO files should serve as image/x-icon or image/vnd.microsoft.icon
      if (link.href?.endsWith('.ico')) {
        expect(contentType).toMatch(/image\/(x-icon|vnd\.microsoft\.icon)/)
      }
      // SVG files should serve as image/svg+xml
      if (link.href?.endsWith('.svg')) {
        expect(contentType).toContain('image/svg+xml')
      }
    }
  })

  test('favicon.ico exists at site root', async () => {
    const res = await fetch(`${BASE_URL}/favicon.ico`)
    expect(res.status).toBe(200)
    const contentType = res.headers.get('content-type') || ''
    expect(contentType).toMatch(/image\/(x-icon|vnd\.microsoft\.icon)/)
  })

  test('apple-touch-icon is 180x180', async ({ page }) => {
    await page.goto(BASE_URL)
    const href = await page.$eval(
      'link[rel="apple-touch-icon"]',
      (el) => el.getAttribute('href')
    )
    expect(href).toBeTruthy()

    // Fetch the image and check dimensions
    const url = new URL(href!, BASE_URL).toString()
    const res = await fetch(url)
    const buffer = Buffer.from(await res.arrayBuffer())

    // PNG header contains dimensions at bytes 16-23
    const width = buffer.readUInt32BE(16)
    const height = buffer.readUInt32BE(18) // Note: readUInt32BE(18) for height
    // For a proper check, use sharp or jimp:
    // const metadata = await sharp(buffer).metadata()
    // expect(metadata.width).toBe(180)
    // expect(metadata.height).toBe(180)
  })

  test('web manifest icons are all reachable', async ({ page }) => {
    await page.goto(BASE_URL)
    const manifestHref = await page.$eval(
      'link[rel="manifest"]',
      (el) => el.getAttribute('href')
    ).catch(() => null)

    if (!manifestHref) return // No manifest, skip

    const manifestUrl = new URL(manifestHref, BASE_URL).toString()
    const manifest = await (await fetch(manifestUrl)).json()

    for (const icon of manifest.icons || []) {
      const iconUrl = new URL(icon.src, manifestUrl).toString()
      const res = await fetch(iconUrl)
      expect(res.status, `Manifest icon ${icon.src} returned ${res.status}`).toBe(200)
    }
  })
})

CI/CD Integration

Running favicon checks locally is useful. Running them automatically on every deploy is better. The following GitHub Actions workflow runs the Playwright favicon tests against your preview deployment. It catches regressions before they reach production — a broken path in a refactor, a missing file after a build tool change, or a MIME type misconfiguration from a CDN update.

.github/workflows/favicon-check.yml
name: Favicon Check

on:
  deployment_status:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  favicon-test:
    runs-on: ubuntu-latest
    if: github.event_name != 'deployment_status' || github.event.deployment_status.state == 'success'
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Build site
        run: npm run build

      - name: Start server
        run: npm start &
        env:
          PORT: 3000

      - name: Wait for server
        run: npx wait-on http://localhost:3000 --timeout 30000

      - name: Run favicon tests
        run: npx playwright test tests/favicon.spec.ts
        env:
          BASE_URL: http://localhost:3000

      - name: Upload test results
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: favicon-test-results
          path: test-results/

The Favicon Debugging Flowchart

When a favicon is not showing up, work through this decision tree. Most problems fall into one of five categories, and checking them in order saves time.

  1. Is the icon missing entirely? View source and look for <link rel="icon"> tags. If there are none, your HTML is not declaring a favicon. Add the appropriate link tags.
  2. Are the link tags present but the file returns a 404? Open DevTools Network tab, filter by the favicon filename. If you see a 404, the file path in the link tag does not match where the file actually lives. Common cause: the file is in /public/ but the link tag says /assets/favicon.ico.
  3. Does the file load but show the wrong icon? You are probably hitting a caching issue. Try an incognito window. If the correct icon shows in incognito, clear Chrome's favicon database (see the InfoBox above for the file path).
  4. Does the file load but the server returns the wrong MIME type? Some CDNs and static hosts serve .ico files as application/octet-stream instead of image/x-icon. Check the Content-Type response header in DevTools. Some browsers silently ignore favicons with incorrect MIME types.
  5. Does everything work on desktop but fail on mobile? iOS requires apple-touch-icon (a 180x180 PNG). Android uses the web manifest's icons array. Neither platform falls back to favicon.ico for home screen icons. Check that you have platform-specific declarations.

Common Failure Modes

Blank square on iOS

iOS ignores favicon.ico and .svg favicons entirely. It looks specifically for <link rel="apple-touch-icon"> pointing to a 180x180 PNG. If that tag or file is missing, iOS renders either a screenshot thumbnail of your page (which looks terrible) or a blank icon.

Apple is strict about this

The apple-touch-icon must be exactly 180x180 pixels. Apple will not use a 192x192 or 512x512 fallback. If you only serve one PNG size, make it 180x180 and let the Apple Touch icon link point to it.

Favicon not updating after deploy

Browsers cache favicons more aggressively than almost any other resource. Some browsers store favicons in a separate database that persists even after clearing the normal cache. Workarounds include:

  • Hard refresh (Ctrl+Shift+R or Cmd+Shift+R)
  • Incognito/private window
  • Append a cache-buster query string: favicon.ico?v=2
  • Clear the favicon cache specifically (Chrome stores it in a separate SQLite database)
  • Wait. Some browsers update favicon caches on a 24-hour cycle.

Wrong icon from a previous version

If you changed your favicon but the old one still appears, check that you did not leave stale files in your deployment. A public/favicon.ico from a previous version will override an app/favicon.ico in some frameworks. Make sure there is only one source of truth.

Pixelated on high-DPI screens

If your favicon looks pixelated on a Retina or high-DPI display, you are probably serving only a 16x16 icon. Modern high-DPI screens render browser tabs at 32x32 or even 48x48 pixels. Your ICO file needs to include these larger sizes, or you need explicit sizes attributes on your link tags pointing to appropriately sized PNGs.

Platform-Specific Debugging

Safari: SVG favicon quirks

Safari on macOS added SVG favicon support in version 15, but it comes with caveats that trip up developers. First, Safari requires the SVG to have a viewBox attribute — without it, the icon may not render at all. Second, Safari does not re-fetch SVG favicons when you navigate between pages on the same domain. If your SVG has an error, you will not see the fix until you fully quit and relaunch Safari, or clear the favicon cache at ~/Library/Safari/Favicon Cache/.

Safari on iOS does not support SVG favicons at all. It only uses the apple-touch-icon PNG. There is no workaround — you must provide a PNG alongside your SVG.

Safari also renders the pinned tab icon (the small monochrome icon in the tab bar when you have many tabs) from the mask-icon link tag. This icon must be a single-color SVG. If it has multiple colors, Safari will silently ignore it and show a generic globe. The color attribute on the link tag controls the fill color.

Safari mask-icon
<!-- Safari pinned tab icon — must be a single-color SVG -->
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#DA7756">

Firefox: aggressive caching

Firefox caches favicons in a Places database (places.sqlite in your profile folder) and this cache is notoriously sticky. Clearing browser history and cache does not always clear the favicon cache. During development, these methods force Firefox to re-fetch:

  • Navigate directly to yoursite.com/favicon.ico and hard-refresh that specific URL (Ctrl+Shift+R).
  • Open about:config and toggle browser.chrome.favicons to false, reload, then toggle it back to true.
  • Delete the favicons.sqlite file in your Firefox profile directory (with Firefox closed). Find your profile at about:support → Profile Directory.

Firefox also has a unique behavior with animated favicons: it is the only major browser that supports animated GIF and APNG favicons. The animation plays in the browser tab. This can be either a feature or a nuisance depending on your use case, but be aware that an animated GIF served as a favicon will animate in Firefox and show a static first frame in other browsers.

Edge: ICO parsing differences

Microsoft Edge (Chromium-based) generally handles favicons the same way as Chrome, but there are edge cases with ICO files. Edge is stricter about ICO file structure than Chrome. If your ICO file was created by simply renaming a PNG to .ico (a common shortcut), Chrome will accept it but Edge may not display it correctly. A proper ICO file is a container format that can hold multiple sizes — use a tool that generates real ICO files with embedded 16x16, 32x32, and 48x48 PNGs.

Edge also has a specific behavior with taskbar pinning on Windows. When a user pins your site to the taskbar, Edge uses the largest icon it can find — preferring the 512x512 from the web manifest. If that icon is missing, the pinned site shows a low-quality upscaled version of the 32x32 favicon. Always include a 512x512 icon in your web manifest for Windows taskbar quality.

Online Testing Tools

RealFaviconGenerator has a free favicon checker that tests your live URL against all major platforms and reports missing formats, incorrect sizes, and implementation issues. It catches things you would probably miss manually.

For social sharing previews, use the platform's own debugger tools. Facebook has the Sharing Debugger, Twitter has the Card Validator, and LinkedIn has the Post Inspector. Each one fetches your page and shows exactly what users will see when your URL is shared.

Test early, test often

Add favicon testing to your deployment checklist. It takes two minutes: open the site in Chrome and Safari, bookmark it, try Add to Home Screen on a phone, and paste the URL into Slack. If all four look correct, you are covered.

Ready to create your icon?

Generate a professional icon from a text prompt in seconds. Favicons, app icons, social media — all platforms.