Start with one brand hex, generate a harmonized palette, and verify every text-and-background pair against WCAG thresholds before you name the tokens. This prevents the most common failure mode: picking a pretty color that fails contrast checks or looks muddy when you try to make it lighter or darker.
Why You Need a Systematic Brand Color Approach
A brand color is not just a hex code; it is a functional system that must survive being used on white backgrounds, dark modes, buttons, and disabled states. If you pick a color visually and then manually try to create tints and shades, you will likely drift the hue. The color might turn purple when you lighten it, or brown when you darken it, breaking brand consistency.
A systematic approach often uses a perceptual color space (like Oklch) rather than simple RGB or HSL. While standard color relationships work for basic harmony, perceptual spaces ensure that when you adjust lightness or saturation, the perceived hue stays true. You also need to validate accessibility early. A brand color that fails WCAG AA contrast against white text is not necessarily useless; it simply means you cannot use white text on that specific shade. You must either darken the background, lighten the text, or use a different shade from your scale for that specific use case.
Step 1: Selecting and Validating Your Base Color
Begin with the single hex value that defines your brand. For this guide, we will use #4F46E5, a vibrant indigo. Your first task is to determine its role. Is it a primary action color? A background? A text color?
If you are using it for text on a white background (#FFFFFF), you must check contrast immediately. If it fails, you have two options: darken the base color or change the background. Do not just add a black border to "fix" it. Adjust the lightness in a perceptual space until it passes.
For our example, #4F46E5 has a contrast ratio of approximately 6.3:1 against white. This passes WCAG AA for normal body text (which requires 4.5:1) but fails AAA (which requires 7:1). It is a strong candidate for primary text, but you may want a darker variant if you need to meet stricter AAA standards or if the text is very small.
Step 2: Generating Harmonized Palettes
Once you have your base, generate variations. You need a scale (light to dark) and complementary colors for accents. Manual calculation is error-prone. ColorWell generates analogous, complementary, triadic, and monochromatic schemes from a single hex, keeping the hue stable in Oklch space.
For #4F46E5, we want a monochromatic scale for UI states (hover, active, disabled) and a complementary color for success/error states.
Worked Example: Generating the Scale
Input: #4F46E5 (Indigo)
The system generates a scale by adjusting lightness and saturation while locking hue. A typical output for a primary brand scale looks like this:
/* Generated from #4F46E5 */
:root {
--brand-50: #EEF2FF; /* Lightest, for backgrounds */
--brand-100: #E0E7FF;
--brand-200: #C7D2FE;
--brand-300: #A5B4FC;
--brand-400: #818CF8;
--brand-500: #6366F1; /* Close to original, but adjusted for perceptual evenness */
--brand-600: #4F46E5; /* Our Base Color */
--brand-700: #4338CA; /* Darkened for stronger text contrast */
--brand-800: #3730A3;
--brand-900: #312E81; /* Darkest, for strong contrast */
--brand-950: #1E1B4B;
}
Notice that --brand-600 is our input. The system provided --brand-700 and --brand-800 as darker variants suitable for text where higher contrast is needed. It also provided --brand-50 for subtle backgrounds. This saves you from guessing hex codes for hover states.
Step 3: Verifying WCAG Contrast for Accessibility
You cannot ship a brand system without checking contrast ratios. You need to verify three scenarios:
- Text on Background: Brand color as text on white/black.
- UI Components: Icons or borders against the background.
- Reverse: White text on the brand color button.
Worked Example: Contrast Checks
Let’s check our key pairs from the scale above.
- Pair 1: White text (#FFFFFF) on Brand Base (#4F46E5)
- Ratio: ~6.3:1 (Passes AA for normal text, fails AAA)
- Verdict: Usable for standard buttons and links. It meets the 4.5:1 requirement for normal text, so it is safe for most body copy and button labels.
- Pair 2: Brand Dark (#3730A3) on White (#FFFFFF)
- Ratio: ~9.9:1 (Passes AAA)
- Verdict: Perfect for body text and links where maximum readability is required.
- Pair 3: Brand Light (#A5B4FC) on White (#FFFFFF)
- Ratio: ~2.1:1 (Fails AA)
- Verdict: Do not use for text. Use only for decorative elements or large icons where contrast requirements are lower (3:1 for UI components).
When you use a tool that runs these checks instantly, you can see which shades in your generated scale are safe for text and which are safe for backgrounds. If --brand-500 fails against white for your specific text size, you automatically know to use --brand-700 for text.
Step 4: Naming Tokens and Exporting to Code
Hex codes are unreadable in code. #4F46E5 means nothing to a developer; --brand-primary means something. You need semantic names.
Do not name tokens by color (blue-500). Name them by role (primary, surface, on-surface). This allows you to change the actual hex values later without rewriting your CSS.
Worked Example: Exporting to CSS Variables
After generating the palette and verifying contrast, export the tokens. Here is what the output looks like when you map the generated scale to semantic names:
:root {
/* Primary Brand Scale */
--color-brand-primary: #4F46E5;
--color-brand-primary-hover: #4338CA;
--color-brand-primary-active: #3730A3;
/* Text Colors */
--color-text-primary: #3730A3; /* Accessible on white */
--color-text-on-brand: #FFFFFF; /* Accessible on #4F46E5 */
/* Surfaces */
--color-surface-subtle: #EEF2FF; /* Brand-50 */
--color-surface-border: #C7D2FE; /* Brand-200 */
/* Complementary Accent (Generated) */
--color-accent-success: #10B981;
--color-accent-error: #EF4444;
}
You can also export this as a Tailwind config or Figma styles. The key is that the hex values are locked in by the contrast checks, so the developer knows --color-text-primary is safe to use.
Common Mistakes to Avoid When Building Brand Colors
1. Ignoring the "Disabled" State Developers will ask for a disabled button color. If you didn’t generate a low-contrast version of your brand color, they will guess. Generate a desaturated, lightened version (like --brand-200) and explicitly define it as --color-brand-disabled.
2. Using Pure Black or White Pure black (#000000) on pure white (#FFFFFF) can cause eye strain. Use your darkest brand shade (e.g., #1E1B4B) for text on white, and your lightest brand shade (e.g., #EEF2FF) for backgrounds. This keeps the system cohesive.
3. Forgetting Dark Mode Your brand color might look great on white but vibrate on black. You need a separate set of tokens for dark mode. Usually, you invert the scale: the --brand-900 from light mode becomes the background, and --brand-100 becomes the text. Verify contrast again in this new context.
4. Skipping the UI Component Check WCAG AA requires 3:1 contrast for UI components (like input borders or icons), not just text. A brand color that fails for text might still pass for a border. Check both. For example, a lighter brand shade might fail the 4.5:1 text requirement but easily pass the 3:1 border requirement, making it a valid "border color" even if it’s not a "text color."