OneMinuteBrandingOneMinuteBrandingGenerate Brand
  1. Home
  2. Blog
  3. Keeping Brand Consistency Across Multiple Repos
design tokensmonorepodevelopers

Keeping Brand Consistency Across Multiple Repos

Your marketing site, docs, dashboard, and mobile app all need the same brand. Here's how to share design tokens across repos without losing your mind.

March 15, 202611 min readBy Yann Lephay

You just shipped a new feature in your React app. You tab over to your Next.js marketing site to update the screenshots. That's when you notice it: the CTA button on the marketing site is a slightly duller blue than the primary button in the main app. You check the docs site built with Docusaurus. A third, entirely different blue. You check the React Native mobile codebase. Hardcoded hex codes from a PR merged in 2022. You maintain four repositories, and you have four completely different definitions of your brand identity.

The Copy-Paste Drift

Brand fragmentation happens gradually. You run npx create-next-app, paste your brand colors into tailwind.config.ts, and forget about it. Six months later, you spin up a documentation repo. You copy the config over. Everything matches.

Then reality hits. You tweak the primary-500 shade from #3B82F6 to #2563EB in the main app because the contrast ratio failed a Lighthouse accessibility audit. You update the border radius on your cards from 0.5rem to 0.75rem. You swap your sans-serif font from Inter to Geist. You commit the changes to the main app repository and tell yourself you'll update the marketing site later.

You never update the marketing site. Multiply this drift across fonts, spacing scales, border radii, and box shadows, and your product suite looks like a patchwork quilt. Copy-pasting configuration files guarantees technical debt. You need a single source of truth for your design tokens.

Here are the three ways to solve this, ranked from worst to best.

Option 1: The npm Package (The Versioned Approach)

The enterprise instinct is to extract your brand tokens into an npm package, publish it to a private registry or GitHub Packages, and install it across your repositories.

You create a package named @your-org/brand-tokens that exports your values as a JavaScript object:

Code
// packages/brand-tokens/index.js
export const colors = {
  primary: {
    50: '#eff6ff',
    100: '#dbeafe',
    500: '#3b82f6',
    900: '#1e3a8a',
    950: '#172554',
  },
  brandFont: '"Geist", sans-serif',
};
Then, in your isolated Next.js repo, you install the package and require it in your Tailwind config:
 
```typescript
// tailwind.config.ts
import { colors } from '@your-org/brand-tokens';
import type { Config } from 'tailwindcss';
 
export default {
  content: ['./app/**/*.{ts,tsx}'],
  theme: {
    extend: {
      colors: {
        primary: colors.primary,
      },
    },
  },
} satisfies Config;

This approach enforces strict semantic versioning. If you remove a color, you bump the major version. Repositories won't break until you explicitly upgrade the package version in their package.json files.

But this is massive overkill unless you manage a 50+ engineer organization. The developer experience for rapid iteration is miserable. If you want to test a new shade of blue, you have to run npm link, fight with your bundler's caching, or publish a beta tag to npm just to see how a button looks on the marketing site. You don't need semantic versioning for a hex code change.

Option 2: Shared CSS Variables via CDN (The Runtime Approach)

Instead of relying on build-time JavaScript packages, you can host a single theme.css file on Cloudflare, AWS S3, or Vercel Edge Network. Every repository simply links to this stylesheet in the document head.

Your hosted theme.css defines your brand tokens as CSS custom properties attached to the root pseudo-class:

Code
/* Hosted at https://cdn.your-org.com/theme.css */
:root {
  --color-primary-50: 239 246 255;
  --color-primary-500: 59 130 246;
  --color-primary-950: 23 37 84;
  --radius-button: 0.5rem;
  --font-brand: 'Geist', sans-serif;
}

Your Tailwind configs across your repos map these variables using the rgb() function to preserve opacity modifiers:

Code
// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: {
        primary: {
          50: 'rgb(var(--color-primary-50) / <alpha-value>)',
          500: 'rgb(var(--color-primary-500) / <alpha-value>)',
          950: 'rgb(var(--color-primary-950) / <alpha-value>)',
        }
      },
      borderRadius: {
        btn: 'var(--radius-button)',
      }
    }
  }
}

When you change the CSS file on the CDN, the colors update instantly across the marketing site, docs, and dashboard on the next page load. No rebuilds required.

This method is deeply flawed. Relying on network requests for core brand CSS introduces the risk of a Flash of Unstyled Content (FOUC). If the CDN takes 300ms to respond, your users see default browser styles before the brand colors snap into place. Worse, you lose all type safety and editor autocomplete. When a developer types text-primary-, their IDE has no idea what the underlying hex value is because it exists on a remote server, not in the local filesystem.

Option 3: Monorepos and Shared Packages (The Winner)

This is the correct way to build modern web applications. You consolidate your scattered repositories into a single monorepo using Turborepo or pnpm workspaces.

Instead of dealing with npm publishing or CDN latency, you create an internal, un-published package that holds your brand configuration. All your apps import this package locally.

Your monorepo structure looks like this:

Code
├── apps/
│   ├── web/               # Next.js App
│   ├── docs/              # Mintlify or Docusaurus
│   └── marketing/         # Astro site
├── packages/
│   ├── config-tailwind/   # The Single Source of Truth
│   └── ui/                # Shared React components
└── package.json

Inside packages/config-tailwind, you export a base Tailwind preset. This isn't just a color object; it's a complete Tailwind configuration that dictates exactly how the brand looks and behaves.

Code
// packages/config-tailwind/preset.ts
import type { Config } from "tailwindcss";
 
export const brandPreset = {
  theme: {
    extend: {
      colors: {
        brand: {
          50: "#f5f3ff",
          100: "#ede9fe",
          500: "#8b5cf6",
          900: "#4c1d95",
          950: "#2e1065",
        },
      },
      fontFamily: {
        sans: ["var(--font-geist-sans)"],
        mono: ["var(--font-geist-mono)"],
      },
      boxShadow: {
        'brand-soft': '0 4px 14px 0 rgba(139, 92, 246, 0.39)',
      }
    },
  },
  plugins: [require("@tailwindcss/typography")],
} satisfies Omit<Config, "content">;

Notice the Omit<Config, "content"> type. The preset defines everything except the content array, because the content array must point to the specific files within each individual app.

In your apps, you import this preset and add it to the presets array in your local tailwind.config.ts:

Code
// apps/marketing/tailwind.config.ts
import type { Config } from "tailwindcss";
import { brandPreset } from "@your-org/config-tailwind/preset";
 
export default {
  content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
  presets: [brandPreset],
} satisfies Config;

This architecture solves every problem. You get instant local updates without running npm link. You get full IDE autocomplete for all your custom classes. A single Pull Request updates the brand across the main app, the docs, and the marketing site simultaneously.

Tool Comparison

ArchitectureSetup TimeUpdate SpeedIDE AutocompleteVerdict
npm Package2 hoursSlow (requires publish)YesOverkill. Avoid unless 50+ devs.
CDN CSS30 minsInstant (no rebuild)NoFlawed. FOUC risk is too high.
Monorepo1 hourInstant (local dev)YesWinner. Use this.

Automating Token Distribution

Even with a monorepo, you still have to generate the tokens and map them correctly. Creating a cohesive brand identity from scratch takes hours of tweaking contrast ratios, calculating HSL steps for dark mode, and writing boilerplate configuration.

This is where OneMinuteBranding fits into a developer workflow. For a $49 one-time payment, you input your core preferences, wait 60 seconds, and get a production-ready brand package. The output includes exactly what you need for the packages/config-tailwind setup: a complete tailwind.config.ts preset, CSS variables mapped for 7 color shades (50-950), SVG logos, favicons, and a CLAUDE.md file.

You drop the generated Tailwind config directly into your monorepo's shared package. But to prevent future drift when you inevitably tweak the brand, you should automate the ingestion of these assets.

Write a simple Node.js script in your monorepo root that extracts the generated ZIP file and distributes the assets to the correct packages automatically.

Code
// scripts/sync-brand.mjs
import fs from 'fs/promises';
import path from 'path';
import { execSync } from 'child_process';
 
const BRAND_SOURCE_DIR = './.brand-temp';
const TAILWIND_DEST = './packages/config-tailwind/preset.ts';
const CSS_DEST = './packages/ui/src/global.css';
const CLAUDE_DEST = './CLAUDE.md';
 
async function syncBrand() {
  console.log('Syncing brand tokens across monorepo...');
 
  // 1. Copy Tailwind preset
  await fs.copyFile(
    path.join(BRAND_SOURCE_DIR, 'tailwind.config.ts'), 
    TAILWIND_DEST
  );
 
  // 2. Copy global CSS variables
  await fs.copyFile(
    path.join(BRAND_SOURCE_DIR, 'variables.css'), 
    CSS_DEST
  );
 
  // 3. Copy AI instructions
  await fs.copyFile(
    path.join(BRAND_SOURCE_DIR, 'CLAUDE.md'), 
    CLAUDE_DEST
  );
 
  console.log('Brand synced. Formatting files...');
  execSync('npx prettier --write "packages/**/*.{ts,css}"');
}
 
syncBrand();

The inclusion of the CLAUDE.md file is critical here. Brand consistency isn't just about hex codes; it's about implementation patterns. If you use Cursor or GitHub Copilot, placing a central CLAUDE.md file at the root of your monorepo ensures the AI knows exactly which Tailwind classes map to your brand. When you ask Claude to "build a pricing card," it will automatically use shadow-brand-soft and text-brand-900 instead of hallucinating random Tailwind utility classes.

Enforcing Consistency in CI

Setting up the monorepo is only half the battle. You have to actively prevent developers from bypassing the system. If an engineer hardcodes text-[#3B82F6] in a random component, your monorepo architecture is useless.

You must enforce token usage at the CI level. Add eslint-plugin-tailwindcss to your shared configuration and configure it to reject arbitrary values for properties that should be driven by your brand tokens.

Code
// packages/config-eslint/index.js
module.exports = {
  plugins: ["tailwindcss"],
  rules: {
    "tailwindcss/no-custom-classname": "warn",
    "tailwindcss/no-arbitrary-value": ["error", {
      "callees": ["classnames", "clsx", "ctl", "cva", "tv"]
    }],
    "no-restricted-syntax": [
      "error",
      {
        "selector": "Literal[value=/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/]",
        "message": "Do not use hardcoded hex colors. Use the Tailwind brand tokens."
      }
    ]
  }
};

This strict ESLint configuration guarantees that any Pull Request containing a hardcoded hex code will fail the CI check. The developer is forced to look up the correct semantic token from your shared package.

Handling Static Assets

Colors and fonts are text. They are easy to share via a .ts file. Logos, favicons, and Open Graph placeholder images are binary files. Sharing them across a monorepo requires a different strategy.

Do not duplicate your logo-dark.svg and logo-light.svg into the public folder of every Next.js app in your monorepo. When marketing updates the logo, they will inevitably miss one app.

Instead, serve your static brand assets from a dedicated package, and use a build script to copy them into the respective public directories right before deployment.

Create packages/brand-assets/ and place all your OneMinuteBranding generated logos and favicons inside. Then, in the package.json of your apps/web and apps/docs, add a prebuild step:

Code
{
  "name": "web",
  "scripts": {
    "dev": "next dev",
    "prebuild": "cp -r ../../packages/brand-assets/* ./public/",
    "build": "next build"
  }
}

This guarantees that every deployment of every application pulls the absolute latest static assets directly from the source of truth. Vercel or your CI pipeline will handle the file operations automatically before compiling the Next.js application.

FAQ

Should I use CSS variables or Tailwind utility classes for dynamic theming?

Use CSS variables mapped to Tailwind utility classes. Hardcoding hex values into tailwind.config.ts means your application has to ship duplicate CSS for dark mode (e.g., text-brand-900 dark:text-brand-50). By defining --color-brand-500 in your CSS and mapping it in the Tailwind preset, you can swap the entire color palette by simply toggling a data-theme="dark" attribute on the <html> tag. Tailwind handles the utility generation; CSS variables handle the runtime value swapping.

How do I handle dark mode across repos?

Force next-themes and a class strategy in your shared Tailwind preset. Add darkMode: 'class' to packages/config-tailwind/preset.ts. This ensures every repo respects the same dark mode trigger. Do not rely on media queries (darkMode: 'media') as your default, because it breaks SSR hydration in Next.js and removes the user's ability to manually override the theme on your marketing site.

What about non-JS repos (like a Go template or Python Django app)?

If you have a legacy Go or Python repository living outside your JavaScript monorepo, use a CI step to sync the tokens. Write a GitHub Action in your monorepo that triggers on pushes to the main branch. The action should compile your Tailwind configuration into a raw CSS file and push that compiled file directly into the static assets directory of the Go/Python repository via the GitHub API. You maintain the monorepo as the single source of truth, and treat the external repos as build targets.

How do I share custom fonts?

Do not load fonts via Google Fonts cdn.jsdelivr.net links in your _document.tsx files. This causes massive layout shifts and slows down your initial render. Download the .woff2 files (OneMinuteBranding provides the correct formats), place them in packages/brand-assets/fonts/, copy them during the prebuild step, and define the @font-face declarations in your shared packages/ui/src/global.css file.


Stop managing brand updates by searching for hex codes across four open VS Code windows. Move your configuration to a Turborepo, create a packages/config-tailwind preset, and automate the ingestion of your design tokens. The next time you need to update your primary color, it will take one Pull Request and exactly zero copy-pasting.

Y
Yann Lephay@YannBuilds

Vibe coder & Indie Hacker. Building tools to help devs ship faster. Creator of OneMinuteBranding.

Ready to create your brand?

Generate a complete brand system with Tailwind config in 60 seconds.

Generate your brand

Related articles

The Developer's Brand Kit Checklist: 23 Files You Need Before Launch

Before you launch, make sure you have these 23 files. From tailwind.config.ts to OG images to CLAUDE.md. Copy this checklist.

The Complete Guide to Favicons, OG Images, and App Icons in 2026

17 different icon sizes, OG images, Apple touch icons. Here's every size you need, where each one goes, and how to generate them all from one source.

Font Pairing for Developers: 7 Combinations That Always Work

Stop spending 2 hours on Google Fonts. These 7 font pairs work for any SaaS product. With next/font code snippets.

Explore more

Branding by RoleBranding by IndustryUse CasesFeaturesIntegrationsGlossaryFree Tools
BlogAboutTermsPrivacy