Injecting Dynamic Brand Colors Across a Next.js CRM with a Central BrandingInjector
TL;DR: Added a BrandingInjector component that pulls the client’s primary color from the API and injects it as a CSS variable on :root. This single change replaced dozens of hard‑coded color values, making the brand theme truly global and future‑proof.
The Problem
Our CRM (PlayaMXCRM) displayed the client’s primary brand color only on the login screen. All other pages used a hard‑coded fallback (#3b82f6) baked into individual component styles. When the client asked for a full‑brand rollout, the UI team faced two issues:
- Inconsistent UI – every component that needed the brand color had to be patched manually.
- Maintenance nightmare – any future brand change required hunting down every occurrence of the color string.
The symptom was obvious in the UI, but the underlying error we saw in the console was a missing CSS variable:
Uncaught TypeError: Cannot read property '--primary-brand' of undefined
Because we never set that variable, components that tried to read it fell back to the hard‑coded color.
What I Tried First
My first instinct was to sprinkle the primary color into each component’s style prop:
// Example in apps/web/src/app/_components/ui.tsx
<div style={{ backgroundColor: brandColor || "#3b82f6" }}>
…
</div>
I added a useBrand() hook that fetched /api/branding_settings and returned brandColor. This worked for a few components, but quickly hit two blockers:
-
Duplication – Every file needed the same
useEffect/fetch logic. - Render flicker – The UI rendered with the fallback color, then re‑rendered once the async request completed, causing a noticeable flash.
It was clear we needed a single source of truth that applied the brand color before any component rendered.
The Implementation
1. Create a client‑side BrandingInjector
I added apps/web/src/app/_components/BrandingInjector.tsx. The component runs once on mount, fetches the branding settings, and writes a CSS custom property to document.documentElement.
// apps/web/src/app/_components/BrandingInjector.tsx
"use client";
import { useEffect } from "react";
import { getApiBase } from "../../lib/apiBase";
/**
* Fetches the client’s primary brand color and injects it as a CSS variable.
* The variable is named --primary-brand and lives on :root.
*/
export default function BrandingInjector() {
useEffect(() => {
async function setBrandColor() {
try {
const res = await fetch(`${getApiBase()}/branding_settings`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { primaryColor } = await res.json();
// Fallback to a safe default if the API returns an empty string
const color = primaryColor?.trim() || "#3b82f6";
// Inject the variable on the root element
document.documentElement.style.setProperty(
"--primary-brand",
color
);
} catch (err) {
console.error("[BrandingInjector] failed:", err);
// Keep the default color defined in globals.css
}
}
setBrandColor();
}, []);
// This component renders nothing – it only has a side‑effect
return null;
}
Key decisions:
-
use clientdirective – ensures the component runs only in the browser, wheredocumentexists. -
Graceful fallback – if the API fails, we keep the default defined in
globals.css. -
No UI output – the component returns
null; it’s purely a side‑effect injector.
2. Wire the injector into the layout
The global layout (apps/web/src/app/layout.tsx) now imports and renders BrandingInjector at the top level so the variable is set before any page component mounts.
// apps/web/src/app/layout.tsx (excerpt)
import BrandingInjector from "./_components/BrandingInjector";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<head>{/* … */}</head>
<body>
<BrandingInjector />
{/* The rest of the app */}
{children}
</body>
</html>
);
}
Because RootLayout is rendered on every route, the brand color is guaranteed to be present for all downstream components.
3. Refactor components to use the CSS variable
With the variable in place, I replaced inline color strings with a reference to var(--primary-brand). For example, CrmShell.tsx now looks like this:
// apps/web/src/app/_components/CrmShell.tsx (excerpt)
const NAV_VENTAS_GROUPS: NavGroup[] = [
{
label: "Ventas",
icon: <SalesIcon color="var(--primary-brand)" />, // <-- changed
items: [/* … */],
},
];
Similarly, the MobileCardList component in ui.tsx got updated:
// apps/web/src/app/_components/ui.tsx (excerpt)
export function MobileCardList({ children, empty = "Sin registros." }: {
children: React.ReactNode;
empty?: string;
}) {
const count = React.Children.count(children);
return (
<div style={{
display: "flex",
flexDirection: "column",
gap: 10,
borderTop: "2px solid var(--primary-brand)" // <-- new
}}>
{count === 0 ? empty : children}
</div>
);
}
All components that previously referenced the hard‑coded #3b82f6 now use the CSS variable, eliminating duplication.
4. Design‑tokens refactor (Phase 1)
The same commit also introduced the first phase of moving inline styles to a shared token file (design-tokens.ts). While not directly related to branding, it set the stage for future theming:
// apps/web/src/app/_components/design-tokens.ts
export const COLORS = {
primary: "var(--primary-brand)",
secondary: "#6b7280",
// …
};
Components can now import COLORS instead of writing raw CSS values, further decoupling UI from hard‑coded literals.
5. Verify with a quick sanity test
Running npm run dev and opening any page now shows the brand’s exact hue (e.g., #ff5722) everywhere, without a flash. The console no longer prints the missing‑variable error.
Key Takeaway
Inject global design tokens (like brand colors) as CSS custom properties from a single, client‑side entry point. This eliminates per‑component fetches, avoids UI flicker, and makes future brand updates a one‑line change in the API or a CSS variable.
What’s Next
-
Server‑side pre‑rendering of the brand variable – generate a small
<style>tag in the HTML head during SSR so the first paint already has the correct color, removing the tiny flash for users with slow connections. -
Expand the token system – move typography, spacing, and shadow values into
design-tokens.tsand expose them as CSS variables for full theming support. -
Add a brand‑editor UI – let admins update
primaryColordirectly in
Part of my Build in Public series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.
Repo: zaerohell/VS · 2026-09-05
#playadev #buildinpublic
Top comments (0)