CColorWell
Get ColorWell

ColorWell/Guides

How to Build Brand Colors: A Step-by-Step Guide

Learn to build accessible brand color systems from a base hex using harmonized palettes, WCAG checks, and token exports for CSS and Figma.

November 1, 2026 · 5 min read

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:

  1. Text on Background: Brand color as text on white/black.
  2. UI Components: Icons or borders against the background.
  3. Reverse: White text on the brand color button.

Worked Example: Contrast Checks

Let’s check our key pairs from the scale above.

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."

Do it in ColorWell

Everything in this guide works in the browser — open the tool and try it on your own input.

Open ColorWell →

Questions people also ask

How many brand colors should I use?

Start with one primary brand hex and generate a full scale of 10-12 shades from it. This single base color, when expanded into a perceptual scale, provides all the necessary tints and shades for backgrounds, borders, and text without needing multiple unrelated hues.

What is the difference between AA and AAA contrast?

AA is the minimum standard requiring a 4.5:1 ratio for normal text and 3:1 for large text or UI components. AAA is the higher standard requiring 7:1 for normal text and 4.5:1 for large text, offering better readability for users with moderate visual impairments.

How do I export my palette to Tailwind CSS?

Map your generated hex values to semantic token names like 'primary' or 'surface' rather than color names, then define these as CSS variables or extend the Tailwind config. This ensures your design system remains flexible and allows you to update hex codes globally without rewriting component classes.

More guides