Building 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.
Imagine a CRM where every company profile automatically adapts its UI to match that company's brand colors. The sidebar accent, the header gradient, the button color — all pulled live from the company's actual brand palette. It sounds complex, but with LogoRouter's Colors API and CSS custom properties, you can build it in an afternoon.
This tutorial will take you from zero to a production-ready dynamic theming system that works with any company domain.
What We're Building
By the end of this tutorial you will have:
- A
BrandThemeProviderReact component that fetches and applies brand colors - Automatic contrast detection to ensure text stays readable
- A graceful fallback system for companies without detected colors
- Proper loading states so your UI does not flash unstyled content
Step 1: Fetching Brand Colors
LogoRouter's Colors API returns the full palette for any company domain:
// types/brand.ts
export interface BrandPalette {
primary: string; // Main brand color
secondary: string; // Supporting color
accent: string; // Highlight / CTA color
background: string; // Suggested background
text: string; // Suggested text color on primary bg
}
// lib/brand.ts
export async function getBrandColors(domain: string): Promise<BrandPalette | null> {
try {
const res = await fetch(
`https://api.logorouter.com/v3/${domain}/colors`,
{
headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_LOGOROUTER_KEY}` },
next: { revalidate: 86400 }, // Cache for 24h in Next.js
}
);
if (!res.ok) return null;
const { colors } = await res.json();
return colors;
} catch {
return null;
}
}Step 2: The BrandThemeProvider Component
// components/brand-theme-provider.tsx
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import type { BrandPalette } from '@/types/brand';
const BrandContext = createContext<BrandPalette | null>(null);
export const useBrand = () => useContext(BrandContext);
interface Props {
domain: string;
children: React.ReactNode;
fallback?: BrandPalette;
}
const DEFAULT_PALETTE: BrandPalette = {
primary: '#3b82f6',
secondary: '#1e3a5f',
accent: '#a3e635',
background: '#0f1117',
text: '#ffffff',
};
export function BrandThemeProvider({ domain, children, fallback = DEFAULT_PALETTE }: Props) {
const [palette, setPalette] = useState<BrandPalette>(fallback);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
setLoading(true);
const colors = await getBrandColors(domain);
setPalette(colors ?? fallback);
setLoading(false);
}
load();
}, [domain]);
const cssVars = {
'--brand-primary': palette.primary,
'--brand-secondary': palette.secondary,
'--brand-accent': palette.accent,
'--brand-background': palette.background,
'--brand-text': palette.text,
} as React.CSSProperties;
return (
<BrandContext.Provider value={palette}>
<div
style={cssVars}
data-brand-loading={loading}
className="transition-colors duration-300"
>
{children}
</div>
</BrandContext.Provider>
);
}Step 3: Using Brand Colors in Your Components
Once the provider is set up, every child component can use the CSS custom properties:
// components/company-card.tsx
function CompanyCard({ domain, name }: { domain: string; name: string }) {
return (
<BrandThemeProvider domain={domain}>
<div
className="rounded-xl p-6 border"
style={{
backgroundColor: 'var(--brand-background)',
borderColor: 'var(--brand-primary)',
}}
>
<div
className="inline-flex items-center gap-3 mb-4 px-3 py-1.5 rounded-full text-sm font-medium"
style={{
backgroundColor: 'var(--brand-primary)',
color: 'var(--brand-text)',
}}
>
<img
src={`https://img.logorouter.com/${domain}?size=24`}
alt=""
className="size-5 object-contain"
/>
{name}
</div>
{/* rest of card content */}
</div>
</BrandThemeProvider>
);
}Step 4: Contrast Safety
Brand colors are not always WCAG-compliant for text. Add an automatic contrast check:
// lib/contrast.ts
function hexToRgb(hex: string): [number, number, number] {
const result = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);
return result
? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
: [0, 0, 0];
}
function relativeLuminance([r, g, b]: [number, number, number]): number {
const [rs, gs, bs] = [r, g, b].map((c) => {
const s = c / 255;
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
export function contrastRatio(hex1: string, hex2: string): number {
const l1 = relativeLuminance(hexToRgb(hex1));
const l2 = relativeLuminance(hexToRgb(hex2));
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// Returns black or white, whichever has better contrast with bg
export function accessibleTextColor(bg: string): string {
return contrastRatio(bg, '#000000') >= contrastRatio(bg, '#ffffff')
? '#000000'
: '#ffffff';
}Step 5: Server-Side Rendering for No Flash
For SSR frameworks like Next.js, fetch colors server-side to avoid the flash of unstyled content:
// app/companies/[domain]/page.tsx
import { getBrandColors } from '@/lib/brand';
import { BrandThemeProvider } from '@/components/brand-theme-provider';
export default async function CompanyPage({ params }: { params: { domain: string } }) {
// Fetch colors server-side — no loading flash
const colors = await getBrandColors(params.domain);
return (
<BrandThemeProvider domain={params.domain} fallback={colors ?? undefined}>
<CompanyProfile domain={params.domain} />
</BrandThemeProvider>
);
}Real-World Results
Teams using dynamic brand theming consistently report higher engagement scores. The personalization makes the interface feel native to each company rather than generic — which is particularly impactful in B2B products where companies interact with many different customers.
Brand Colors API included on all paid plans
The Colors API is available on Startup ($29/mo) and above. Start with the free Community plan to explore.
Startup — from $29/monthCompany 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 allDisplay 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.
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.
Integrating a Company Logo API with Next.js 15 (App Router)
The complete guide to adding company logos to a Next.js 15 App Router app: server components, image optimization, caching, and the Next.js Image component configuration.