
How to Use Brand Colors in CSS and Tailwind
Quick Answer
Define your named brand colors (primary, secondary, accent, neutral, text) as CSS custom properties in :root, then point your Tailwind config at those same variables instead of hardcoding hex codes twice. In Tailwind v4, putting the variables in an @theme block auto-generates utilities like bg-primary and text-primary. Override the variables in a dark-mode media query to swap palettes without touching component markup, and recheck contrast (4.5:1 for body text, 3:1 for large text) any time you substitute a shade.
You've got a palette with five roles: primary, secondary, accent, neutral, text. Maybe you built it by hand, maybe the brand color palette generator pulled it from your logo. Either way, hex codes on a PDF don't do anything until they're in the codebase, and this is where a lot of brands quietly drift. The button on the marketing site is one blue. The button in the app is a slightly different blue because someone copied the hex from a screenshot instead of the source file.
The fix is boring on purpose: put the colors in one place, then have CSS and Tailwind both read from it.
Start from the roles, not the swatches
If you already have named roles, primary, secondary, accent, neutral, text, you're most of the way there. If you don't, back up and read building a 5-color brand palette or creating a brand board first. Both cover how to pick the roles and check contrast, and I'm not going to re-explain that here. This post picks up right after you have hex codes with jobs attached to them.
Say the palette looks like this:
Primary: #1B4B8F
Secondary: #E8B14C
Accent: #C3361B
Neutral: #FAF8F3
Text: #14171A
Turn the palette into CSS custom properties
Put these in :root once, in whatever global stylesheet loads on every page:
:root {
--color-primary: #1B4B8F;
--color-secondary: #E8B14C;
--color-accent: #C3361B;
--color-neutral: #FAF8F3;
--color-text: #14171A;
}
Every component now references var(--color-primary) instead of the literal hex. Change the brand blue six months from now, and it's a one-line edit instead of a find-and-replace across forty files that inevitably misses two of them.
Name the variables after the role, not the color. --color-primary, not --color-blue. A rebrand that swaps blue for teal shouldn't force you to rename every reference in your codebase.
Wire the same tokens into Tailwind
The mistake I see most often: a team sets up the CSS variables above, then hardcodes the same hex codes a second time in tailwind.config.js. Now there are two sources of truth, and they will eventually disagree.
If you're on Tailwind v4, skip the JS config file for this and define the tokens directly in your CSS using @theme:
@import "tailwindcss";
@theme {
--color-primary: #1B4B8F;
--color-secondary: #E8B14C;
--color-accent: #C3361B;
--color-neutral: #FAF8F3;
--color-text: #14171A;
}
Tailwind reads variables prefixed with --color- inside @theme and generates the matching utilities automatically: bg-primary, text-primary, border-primary, ring-accent, and so on. You don't need a separate config object, and the variables are the same ones your plain CSS can reference too.
If you're still on Tailwind v3, you don't get that automatic behavior, but you can still avoid duplicating hex codes by pointing the config at the CSS variables instead of literal values:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
secondary: 'var(--color-secondary)',
accent: 'var(--color-accent)',
neutral: 'var(--color-neutral)',
text: 'var(--color-text)',
},
},
},
};
Either way, a developer writes bg-primary or text-accent in markup and never thinks about the hex code again. That's the whole point: one number to maintain, everywhere else just references a name.
Recheck contrast before you substitute a shade
Named tokens make it easy to swap a color without touching a hundred files, which is also exactly how a palette quietly breaks. Someone darkens the primary blue for a redesign, drops it into --color-primary, and doesn't notice that text-primary on a white background used to pass contrast and now doesn't.
The threshold hasn't changed from what's already covered in the palette and brand board posts: WCAG AA wants at least 4.5:1 for normal body text and 3:1 for large text (18pt and up, or 14pt bold and up). Run any new shade of --color-primary or --color-text through a contrast checker against the neutral it's most likely to sit on before you ship it. A token system makes it fast to change a color everywhere; it does nothing to stop that color from being unreadable everywhere too.
Dark mode without a second palette
You don't need a whole second brand palette for dark mode, just overrides for the tokens that actually need to flip. A media query works if you're following the OS setting:
:root {
--color-primary: #1B4B8F;
--color-neutral: #FAF8F3;
--color-text: #14171A;
}
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #5B8DD9;
--color-neutral: #14171A;
--color-text: #F5F5F0;
}
}
If your app has a manual theme toggle instead of following the system setting, swap the media query for an attribute selector and let JavaScript flip the attribute:
:root[data-theme='dark'] {
--color-primary: #5B8DD9;
--color-neutral: #14171A;
--color-text: #F5F5F0;
}
Notice the primary color itself changed, not just neutral and text. A dark navy that read as strong on a white background can go muddy and low-contrast on a near-black one. Treat the dark-mode primary as its own decision, check it against the dark neutral with a contrast tool, and don't assume the light-mode value will carry over cleanly.
Secondary and accent colors often survive the switch unchanged, especially if they're already mid-brightness. Check them anyway. It takes thirty seconds and saves you from shipping an accent badge nobody can read at night.
Keep the token file as the single source
Once the variables exist, resist the urge to let any component define its own one-off color "just this once." That one-off is how a five-color system turns into fourteen slightly different blues over a year. If a design genuinely needs a sixth token, add it to the root list with a name and a reason, the same way you'd add a new role to the palette itself.
The brand board generator is a reasonable place to keep the canonical hex codes next to the rest of the brand kit, so whoever edits the CSS file six months from now is copying from the same source everyone else did.
Ready to create your logo?
Generate a professional SVG + PNG logo in under 30 seconds.


