Skip to main content

Color & Accessibility

How to Create a Website Color Palette in 5 Steps (With CSS Variables)

Vibeus Moonscript

Updated 4 min read

How to Create a Website Color Palette in 5 Steps (With CSS Variables)
On this page

Most website palettes don’t fail at the color-picking stage. They fail later: when a hover state needs a slightly darker blue that doesn’t exist yet, when text on the brand color turns out to be unreadable, or when dark mode arrives and every hardcoded hex value has to be hunted down.

This is a repeatable process that goes from one brand color to production-ready CSS custom properties, with the accessibility checks built in. It’s deliberately practical. If you want to know why certain color combinations work, read Color Theory for Developers alongside it.

Step 1: Start from one color and describe it in HSL

Pick the single color that represents the brand. Everything else will be derived from it, so resist the urge to choose five colors up front.

Convert it to HSL (hue, saturation, lightness), because HSL makes relationships easy to reason about. The blue used throughout this post, #3366cc, is:

hsl(220 60% 50%)

Hue 220 on the color wheel, 60% saturation, 50% lightness. The Color Format Converter shows any color in HEX, RGB, HSL, and OKLCH.

Step 2: Choose a harmony for your accent colors

A harmony rule picks the other hues by their position on the color wheel relative to your base:

  • Complementary: the opposite hue (220 + 180 = 40, a warm orange). Strong contrast, good for a single call-to-action accent.
  • Analogous: neighbors (around 190 and 250). Calm and cohesive, good for supporting colors.
  • Triadic: three evenly spaced hues (220, 340, 100). Lively, harder to balance.

Put your base color into the Color Palette Generator, switch between schemes, and keep the one that suits the product. For most interfaces, one base color, one accent, and neutrals are plenty.

Step 3: Build a scale for each color

A single shade of each color isn’t enough for a real interface. You need light versions for backgrounds, mid-tones for borders, and dark versions for hover states and text. The conventional answer is an 11-step scale from 50 (near white) to 950 (near black), the naming Tailwind uses.

Generate scales for:

  1. The brand color: open the Tint & Shade Generator with #3366cc and export the CSS variables.
  2. The accent color.
  3. A neutral gray: don’t use pure gray. A gray with a little of the brand hue (for example hsl(220 15% 50%)) looks intentional next to your brand color instead of flat.

Step 4: Assign roles with two layers of CSS variables

This is the step that makes a palette maintainable. Use two layers:

  • Scale tokens hold raw values: --brand-600, --gray-200.
  • Role tokens say what a color is for: --color-text, --color-primary, --color-border. Components only ever use role tokens.
:root {
  /* Scale tokens (example values; paste your generated scales here) */
  --brand-50: hsl(220 60% 97%);
  --brand-500: hsl(220 60% 50%); /* #3366cc */
  --brand-700: hsl(220 60% 35%);
  --gray-50: hsl(220 15% 97%);
  --gray-200: hsl(220 15% 88%);
  --gray-600: hsl(220 10% 40%);
  --gray-900: hsl(220 20% 12%);
  --accent-700: #b45309;

  /* Role tokens: components use only these */
  --color-bg: var(--gray-50);
  --color-surface: #ffffff;
  --color-text: var(--gray-900);
  --color-text-muted: var(--gray-600);
  --color-border: var(--gray-200);
  --color-primary: var(--brand-500);
  --color-primary-hover: var(--brand-700);
  --color-accent: var(--accent-700);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: var(--gray-900);
    --color-surface: hsl(220 20% 16%);
    --color-text: var(--gray-50);
    --color-text-muted: hsl(220 15% 70%);
    --color-border: hsl(220 15% 26%);
    --color-primary: #8ab4f8;
  }
}

.button {
  background: var(--color-primary);
  color: var(--color-surface);
}

Dark mode now means remapping a handful of role tokens, not editing components. The same idea works in Tailwind: point theme colors at the variables, for example primary: "var(--color-primary)".

Add semantic roles too (success, warning, error). Keep their hues close to the conventions users already know, and don’t let your harmony rule turn “error” purple.

Step 5: Check contrast and color vision before you ship

Harmony rules say nothing about readability, so check every role pair that will carry text or icons:

  • 4.5:1 for normal text (WCAG AA)
  • 3:1 for large text and for UI parts such as icons, input borders, and focus rings

Real numbers from the palette above:

ForegroundBackgroundRatioNormal text
#3366ccwhite5.37:1Passes
#3366cc#111827 (dark background)3.31:1Fails
#8ab4f8#1118278.42:1Passes
#f59e0b (bright amber)white2.15:1Fails
#b45309 (darker amber)white5.02:1Passes

Two lessons show up immediately. The brand blue that works on white is too dark for a dark background, which is why the dark theme maps --color-primary to a lighter blue. And the bright accent that looks great as a badge fails as text, so the text role uses a darker step from the same scale. Check pairs like these in the Contrast Checker.

Finally, run the core colors through the Color Blindness Simulator. If success and error only differ by red versus green, add an icon or text label rather than relying on color alone.

Checklist

  1. One base color, described in HSL.
  2. A harmony rule for one accent (or two at most).
  3. 50–950 scales for brand, accent, and a tinted neutral.
  4. Scale tokens plus role tokens, with dark mode remapping role tokens only.
  5. Contrast checked for every text and UI pair, and color-blind-safe status colors.

Want the reasoning behind harmony rules, the 60-30-10 split, and why pure gray looks off? That’s all in Color Theory for Developers.

Try it free

Color Palette Generator

Create harmonious color palettes for your web projects with various color schemes.

Open Color Palette Generator

Written by

Vibeus Moonscript

Writes DevBottle's guides and builds the tools they cover. About DevBottle