Tutorials
9 min read

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.

Jordan Lee

Jordan Lee

Senior Developer Advocate

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 BrandThemeProvider React 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:

typescript
// 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

tsx
// 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:

tsx
// 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:

typescript
// 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:

tsx
// 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/month
Start free — no credit card
Start building today

Company 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

Tutorials
tutorial
brand colors
react
theming
css variables
personalization