Skip to content

TomyCesaille/react-site-icon

Repository files navigation

react-site-icon

Display any site's favicon effortlessly. One prop in, the right icon out. Zero dependencies. < 1KB.

npm bundle size TypeScript license CI

import { SiteIcon } from 'react-site-icon';

<SiteIcon domain="github.com" />
<SiteIcon
  domain="example.com"
  fallback={<span>?</span>}
/>

Live demo | Try it on StackBlitz

Why

Favicons sound easy. They aren't. Different CDNs, missing icons, blurry upscales, layout shift, SSR caching, default-globe placeholders that look like real favicons — most "just give me a favicon" libraries punt on these.

react-site-icon handles them for you:

  • Reliable fallback detection. Distinguishes real favicons from Google's default globe via a naturalWidth check (see How it works below) — no other React favicon library does this.
  • Sharp at every size. Only exposes sizes Google's CDN actually serves (12 | 16 | 24 | 28 | 32 | 40 | 48 | 50 | 64 | 96 | 128) — no blurry upscaling.
  • Forgiving domain input. "github.com", "https://github.com", "https://github.com/user/repo" — all work. Internally normalized to hostname.
  • Three render strategies. Pick lazy (fallback during load), eager (fastest paint), or hidden (zero layout shift).
  • SSR + hydration aware. Renders fallback on the server. Post-mount, checks img.complete via a ref callback so browser-cached / SSR-prefetched images don't get stuck on the fallback.
  • Stale-prop safe. Change domain mid-load and you'll never see the previous domain's icon flash through.
  • One CDN request. No canvas pixel-comparison, no crossOrigin tainting, no second fetch to the target domain.

How it works

Google's faviconV2 CDN returns a favicon for any domain. When the domain has no favicon, it returns a default globe icon that is always 16x16 pixels, regardless of the size you requested. This size mismatch is the detection mechanism:

                    react-site-icon
                         |
           Google faviconV2 CDN request
                  (single request)
                         |
                   +-----+-----+
                   |           |
              Real favicon   Default globe
              (64x64 px)    (always 16x16)
                   |           |
           naturalWidth > 16  naturalWidth = 16
                   |           |
              Show <img>   Show fallback

After the image loads, we check naturalWidth on the <img> element. A real favicon comes back at the requested size (e.g. 64x64). The default globe is always 16x16. One comparison, zero CORS issues, single network request.

Note: When size={16}, the component internally requests 24px from the CDN (and renders at 16x16) so detection still works — otherwise both real favicons and the globe would be 16px, making them indistinguishable.

Why other approaches fail:

  • Direct fetch() to Google CDN -- blocked by CORS. Google doesn't send Access-Control-Allow-Origin.
  • Canvas pixel comparison -- requires crossOrigin attribute, which Google's CDN rejects. Canvas is tainted.
  • Fetching from target domain -- unreliable. Many sites don't serve /favicon.ico, return redirects, or have restrictive CORS. Adds latency from a second request.

Install

npm install react-site-icon

API

Prop Type Default Description
domain string required Domain to fetch the favicon for (e.g. "github.com" or "https://github.com/user/repo")
size SiteIconSize 32 Requested favicon size in pixels (12 | 16 | 24 | 28 | 32 | 40 | 48 | 50 | 64 | 96 | 128)
fallback ReactNode null Content to render when no favicon is available
strategy 'lazy' | 'eager' | 'hidden' 'lazy' Detection strategy (see Strategies)
onResolved (found: boolean) => void -- Called when detection completes. true = found, false = globe/error

All standard <img> props (className, style, alt, loading, decoding, data-*, aria-*) are spread onto the underlying element. src, width, height, onLoad, onError are reserved.

Strategies

lazy (default)

Shows fallback during detection, swaps to favicon when found.

<SiteIcon domain="github.com" fallback={<Spinner />} />

eager

Shows the <img> immediately. May flash Google's default globe briefly before detection completes.

<SiteIcon domain="github.com" strategy="eager" />

hidden

Renders a sized empty placeholder during detection. Prevents layout shift.

<SiteIcon domain="github.com" strategy="hidden" />

When to use which

Strategy During detection Best for
lazy Shows fallback Lists with loading states
eager Shows <img> directly Above-the-fold content, fastest paint
hidden Sized empty placeholder Preventing layout shift

Advanced

SSR

On the server, SiteIcon renders the fallback (for lazy) or a placeholder (for hidden). Detection runs on the client after hydration. No window or document access during SSR.

Ref forwarding

const imgRef = useRef<HTMLImageElement>(null);

<SiteIcon ref={imgRef} domain="github.com" />

The ref points to the <img> element when a favicon is found. When the fallback renders, the ref is null.

onResolved callback

Track favicon availability:

function FaviconWithStatus({ domain }: { domain: string }) {
  const [found, setFound] = useState<boolean | null>(null);

  return (
    <div>
      <SiteIcon domain={domain} onResolved={setFound} />
      {found === false && <span>No favicon available</span>}
    </div>
  );
}

Memoize onResolved if you don't want re-fires on re-render.

Compare

Feature react-site-icon favicon-stealer DIY Google CDN DIY domain fetch Proxy services
Bundle size < 1KB ~3.5KB 0 0 0
Dependencies 0 2 0 0 0
Fallback detection Yes No No No No
Network requests 1 1-2 1 1+ (may fail) 1
React versions 17, 18, 19 19 only Any Any Any
SSR compatible Yes Unknown Manual Manual Manual
TypeScript Full Full Manual Manual Manual

Contributing

git clone https://github.com/TomyCesaille/react-site-icon.git
cd react-site-icon
npm install
npm test

PRs welcome.

License

MIT

About

A React component to display any website's favicon

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors