Building a Production-Ready Company Logo Component in React
The definitive guide to a React logo component that handles loading states, error fallbacks, retina display, accessibility, and TypeScript — ready to drop into any codebase.
Most company logo implementations I have reviewed in production codebases share the same failure modes: broken images that never recover, layout shift from unsized containers, missing alt text, no retina support, and duplicate API calls for the same domain. This guide fixes all of them.
The Component Requirements
A production-grade logo component must:
- Show a skeleton placeholder while loading (prevents CLS)
- Recover gracefully from load errors (initials fallback)
- Support retina / HiDPI displays (2x images)
- Be accessible (correct alt text, ARIA attributes)
- Accept dark mode variant when needed
- Never fire duplicate requests for the same domain
- Support server-side rendering without hydration issues
The Full Implementation
// components/company-logo.tsx
'use client';
import { useState, useCallback, memo } from 'react';
import { cn } from '@/lib/utils';
type LogoSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
type LogoTheme = 'light' | 'dark' | 'auto';
type FallbackType = 'initials' | 'icon' | 'none';
interface CompanyLogoProps {
/** Company domain, e.g. "stripe.com" */
domain: string;
/** Display size preset */
size?: LogoSize;
/** Override display pixel size */
px?: number;
/** Logo color variant */
theme?: LogoTheme;
/** What to show on error */
fallback?: FallbackType;
/** Tailwind classes */
className?: string;
/** Accessible label — defaults to "${domain} logo" */
label?: string;
/** API token */
token?: string;
}
const SIZE_MAP: Record<LogoSize, { display: number; api: number }> = {
xs: { display: 16, api: 32 },
sm: { display: 24, api: 48 },
md: { display: 40, api: 80 },
lg: { display: 56, api: 112 },
xl: { display: 80, api: 160 },
'2xl': { display: 120, api: 240 },
};
export const CompanyLogo = memo(function CompanyLogo({
domain,
size = 'md',
px,
theme = 'auto',
fallback = 'initials',
className,
label,
token,
}: CompanyLogoProps) {
const [status, setStatus] = useState<'idle' | 'loading' | 'loaded' | 'error'>('loading');
const { display, api } = SIZE_MAP[size];
const displayPx = px ?? display;
const apiPx = px ? px * 2 : api;
const params = new URLSearchParams({
size: String(apiPx),
format: 'webp',
...(theme !== 'auto' && { theme }),
...(token && { token }),
});
const src = `https://img.logorouter.com/${domain}?${params}`;
const altText = label ?? `${domain} logo`;
const handleLoad = useCallback(() => setStatus('loaded'), []);
const handleError = useCallback(() => setStatus('error'), []);
return (
<div
role="img"
aria-label={altText}
className={cn(
'relative flex-shrink-0 overflow-hidden rounded-md bg-muted/50 flex items-center justify-center',
className
)}
style={{ width: displayPx, height: displayPx }}
>
{/* Skeleton */}
{status === 'loading' && (
<div
className="absolute inset-0 animate-pulse rounded-md bg-muted"
aria-hidden="true"
/>
)}
{/* Logo image */}
{status !== 'error' && (
<img
src={src}
alt={altText}
width={displayPx}
height={displayPx}
className={cn(
'absolute inset-0 w-full h-full object-contain transition-opacity duration-200',
status === 'loaded' ? 'opacity-100' : 'opacity-0'
)}
onLoad={handleLoad}
onError={handleError}
loading="lazy"
decoding="async"
crossOrigin="anonymous"
/>
)}
{/* Fallback */}
{status === 'error' && fallback === 'initials' && (
<InitialsFallback domain={domain} size={displayPx} />
)}
</div>
);
});
function InitialsFallback({ domain, size }: { domain: string; size: number }) {
// Use the second-level domain for initials: "stripe.com" → "ST"
const name = domain.split('.')[0] ?? domain;
const initials = name.slice(0, 2).toUpperCase();
const fontSize = Math.max(10, Math.round(size * 0.38));
return (
<span
className="font-semibold text-muted-foreground select-none"
style={{ fontSize }}
aria-hidden="true"
>
{initials}
</span>
);
}TypeScript Exports and Usage
// Usage examples
// Basic
<CompanyLogo domain="stripe.com" />
// Large with dark mode variant
<CompanyLogo domain="stripe.com" size="xl" theme="dark" />
// Custom pixel size with fallback disabled
<CompanyLogo domain="acme.io" px={48} fallback="none" />
// With API key for higher resolution
<CompanyLogo
domain="notion.so"
size="2xl"
token={process.env.NEXT_PUBLIC_LOGOROUTER_KEY}
/>Adding a Domain Normalization Utility
Email addresses, URLs, and bare domains all need to resolve to the same key:
// lib/domain.ts
export function normalizeDomain(input: string): string {
try {
// Handle full URLs
if (input.startsWith('http')) {
return new URL(input).hostname.replace(/^www\./, '');
}
// Handle email addresses
if (input.includes('@')) {
return input.split('@')[1].replace(/^www\./, '');
}
// Handle bare domains
return input.replace(/^www\./, '').split('/')[0].toLowerCase();
} catch {
return input.toLowerCase().trim();
}
}
// normalize('https://www.stripe.com/payments') → 'stripe.com'
// normalize('user@stripe.com') → 'stripe.com'
// normalize('www.stripe.com') → 'stripe.com'Free to start — 500K logos per month
The CompanyLogo component above works on the free tier with no API key. Add your key for higher resolution, dark mode, and brand colors.
Community — free foreverCompany logos and brand data, ready in 60 seconds
500,000 requests per month, completely free. No credit card. No contracts. Upgrade to a paid plan when you are ready to scale.
- 500K requests / month free
- 30M+ company logos
- Sub-50ms global CDN
- PNG, WebP & SVG formats
- No credit card required
Topics covered
Related articles
View allBuilding Dynamic UI Themes from Company Brand Colors in React
Step-by-step tutorial: automatically adapt your app's UI to match any company's brand colors using LogoRouter's Colors API and React CSS custom properties.
Display Sender Company Logos in Your Email App (Like Superhuman)
Tutorial: how to identify company domains from email addresses and display brand logos in your email client's inbox view — with error handling, caching, and fallbacks.
How to Optimize Logo API Performance: Caching, Lazy Loading & Core Web Vitals
Advanced techniques to make company logos load instantly: multi-tier caching, lazy loading, WebP/AVIF format selection, and how to measure impact on Core Web Vitals.