I recently launched my first new tab extension: Next Idea NewTab
The rightmost module of this extension needs to display logos from various third-party websites. While this might seem straightforward, it’s actually quite challenging: websites use different favicon formats, images fail to load, and timeouts occur.
Today, I’m sharing my robust React component that elegantly handles these logo display challenges.
Key Features
- Multiple fallback icon sources for maximum reliability
- Loading state indicator
- Timeout handling
- Graceful fallback (using domain’s first letter)
- Customizable size and styling
Implementation Details
1. Multiple Fallback Sources
First, we define an array of fallback icon sources, ordered by priority:
const fallbackSources = [
`https://${domain}/logo.svg`,
`https://${domain}/logo.png`,
`https://${domain}/apple-touch-icon.png`,
`https://${domain}/apple-touch-icon-precomposed.png`,
`https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
`https://${domain}/favicon.ico`
]
The thinking behind this order:
1. Most developers use logo.png or logo.svg files, which typically offer better resolution
2. Apple Touch Icons are the next best option, usually providing high-quality images
3. If those fail, we try Google and DuckDuckGo’s icon services
4. Finally, we fall back to the traditional favicon.ico
2. State Management
We define several crucial states to handle our logic:
const [imgSrc, setImgSrc] = useState(`https://${domain}/logo.svg`)
const [fallbackIndex, setFallbackIndex] = useState(0)
const [isLoading, setIsLoading] = useState(true)
const [hasError, setHasError] = useState(false)
3. Timeout Handling
To balance user experience and logo display quality, I implemented a timeout mechanism. I recommend setting it to 1 second:
useEffect(() => {
let timeoutId
if (isLoading) {
timeoutId = setTimeout(() => {
handleError()
}, timeout)
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}, [imgSrc, isLoading])
4. Graceful Fallback
When all image sources fail, we display the domain’s first letter as a last resort. This ensures the UI remains intact, and you’ll only see this fallback if there are network connectivity issues:
{hasError && (
<div
className="w-full h-full flex items-center justify-center bg-gray-100 rounded-md"
style={{ fontSize: `${size * 0.5}px` }}>
{domain.charAt(0).toUpperCase()}
</div>
)}
Full source code
"use client";
import { getDomain } from "@/lib/utils";
import { useEffect, useState } from "react";
interface IProps {
url: string;
size?: number;
className?: string;
timeout?: number;
}
const WebsiteLogo = ({
url,
size = 32,
className = "",
timeout = 1000, // 1 second
}: IProps) => {
const domain = getDomain(url);
const [imgSrc, setImgSrc] = useState(`https://${domain}/logo.svg`);
const [fallbackIndex, setFallbackIndex] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const fallbackSources = [
`https://${domain}/logo.svg`,
`https://${domain}/logo.png`,
`https://${domain}/apple-touch-icon.png`,
`https://${domain}/apple-touch-icon-precomposed.png`,
`https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
`https://${domain}/favicon.ico`,
];
useEffect(() => {
let timeoutId: any;
if (isLoading) {
timeoutId = setTimeout(() => {
handleError();
}, timeout);
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [imgSrc, isLoading]);
const handleError = () => {
const nextIndex = fallbackIndex + 1;
if (nextIndex < fallbackSources.length) {
setFallbackIndex(nextIndex);
setImgSrc(fallbackSources[nextIndex]);
setIsLoading(true);
} else {
setHasError(true);
setIsLoading(false);
}
};
const handleLoad = () => {
setIsLoading(false);
setHasError(false);
};
return (
<div
className={`relative inline-block ${className}`}
style={{ width: size, height: size }}
>
{/* placeholder */}
{isLoading && (
<div className="absolute inset-0 animate-pulse">
<div className="w-full h-full rounded-md bg-gray-200/60" />
</div>
)}
<img
src={imgSrc}
alt={`${domain} logo`}
width={size}
height={size}
onError={handleError}
onLoad={handleLoad}
className={`inline-block transition-opacity duration-300 ${
isLoading ? "opacity-0" : "opacity-100"
}`}
style={{
objectFit: "contain",
display: hasError ? "none" : "inline-block",
}}
/>
{/* Fallback: Display first letter of domain when all image sources fail */}
{hasError && (
<div
className="w-full h-full flex items-center justify-center bg-gray-100 rounded-md"
style={{ fontSize: `${size * 0.5}px` }}
>
{domain.charAt(0).toUpperCase()}
</div>
)}
</div>
);
};
export default WebsiteLogo;
Usage
// Basic usage
<WebsiteLogo url="https://nextidea.dev" />
// Custom size
<WebsiteLogo url="https://newtab.nextidea.dev" size={48} />
// Custom styling
<WebsiteLogo
url="ogimage.click"
className="shadow-lg rounded-full"
/>
// Custom timeout (milliseconds)
<WebsiteLogo
url="https://newtab.nextidea.dev"
timeout={2000}
/>
Conclusion
This component solves a common challenge in web development: reliably displaying website logos. With its multiple fallback options and graceful degradation, it ensures a smooth user experience. It’s particularly useful for developers building tool directories or resource navigation sites, as it significantly reduces manual effort.
I’ve open-sourced this component in my repo nextjs-15-starter. Feel free to use it in your projects!