Skip to the color picker

How to choose a color palette for a website or app

Start from one brand color, generate a 50 to 950 scale from it, add slightly tinted grays and four semantic colors, then test every text pair for contrast and color blindness. Choose steps by their OKLCH lightness, not by eye: on a white background, text passes WCAG AA once lightness drops to about 0.55, whatever the hue.

7 min read

Start from one brand color and generate a scale

One brand color is enough to build a whole interface. The example here is #2f6bdf, a medium blue. On its own it is one value. A UI needs light tints for backgrounds and badges, the color itself for buttons and links, and dark shades for hover states and text.

Generate eleven steps, from 50 to 950, the naming Tailwind and most design systems use. The scale below keeps the hue fixed, places each step at a set OKLCH lightness and lowers chroma toward the ends so the tints do not turn neon and the shades do not turn muddy. The brand color lands at step 600 because its lightness, 0.555, is closest to that slot.

StepHEXOKLCH LOn whiteOn blackText on this step
50#f1f5fc0.9691.09:119.21:1Black
100#e0eafa0.9341.21:117.32:1Black
200#c7d9f80.8821.43:114.71:1Black
300#a3c0f50.8051.84:111.42:1Black
400#6f9ef40.7032.67:17.87:1Black
500#4a83f00.6273.62:15.80:1Black
600#2f6bdf0.5554.90:14.29:1White
700#2158c30.4906.46:13.25:1White
800#1e4a9f0.4318.28:12.54:1White
900#20417e0.3869.91:12.12:1White
950#1225490.27015.15:11.39:1White

Read the table from the middle. Step 600 is the lightest step that works as text on white (4.90:1) and as a button with white text. Step 500 looks almost the same, yet it fails at 3.62:1. Everything from 50 to 500 needs dark text on top.

Pick steps by lightness, not by eye

Hue changes how light a color looks, so eyeballing a "medium" shade gives different contrast for every hue. OKLCH lightness removes the guesswork. The table shows the lightest color of each hue, at chroma up to 0.15, that still reaches 4.5:1 on white.

HueColorOKLCH LHSL lightnessOn white
Gray#7676760.56646%4.54:1
Red#c551490.58553%4.52:1
Orange#be5a0c0.58140%4.51:1
Yellow#8c75000.56727%4.51:1
Green#2988340.55335%4.50:1
Teal#00857b0.55526%4.52:1
Blue#3f75cf0.57153%4.51:1
Violet#8265c90.58159%4.51:1
Pink#bb51890.58753%4.52:1

HSL lightness at the pass line swings from 26% to 59%. OKLCH lightness stays in a narrow band from 0.553 to 0.587. The practical rule: a color with OKLCH L of 0.55 or lower passes AA as text on white, and a color with L of 0.61 or higher passes with black text. Colors in between need a check.

Build neutrals: slightly tinted grays

Most of the pixels in an interface are gray: backgrounds, borders, body text. Pure gray next to a saturated brand color can look cold and unrelated. Take the brand hue, 262 here, and give the grays a tiny chroma between 0.002 and 0.013. The shift is small enough that nobody sees a blue tint, but the grays feel like part of the same system.

Neutrals need a different lightness curve from the brand scale: very light steps for surfaces, then a quick drop to readable text colors.

StepHEXOKLCH LOn whiteTypical use
50#fafafa0.9851.04:1Page background
100#f4f5f60.9701.09:1Cards, table stripes
200#e4e5e70.9221.26:1Borders, dividers
300#d2d4d70.8691.49:1Input borders
400#9ca0a60.7042.63:1Disabled text, icons
500#6e72790.5514.83:1Muted text
600#4d51580.4347.97:1Secondary text
700#3c3f460.36810.54:1Strong secondary text
800#23262b0.26815.18:1Headings
900#15171b0.20417.94:1Body text
950#090a0c0.14519.80:1Maximum emphasis

Semantic colors: success, warning, danger, info

Status colors need the same scale treatment as the brand. The seed you pick for a badge or an icon is rarely the step you can use for text. Generate a scale for each and pick two steps: one for text on white and one for a solid fill.

RoleSeedSeed on whiteText on whiteSolid fill and its text
Success#18a3493.29:1, fails#007630, 5.78:1White on #007630, 5.78:1
Warning#f59e0a2.15:1, fails#996000, 5.21:1Gray 950 on #f59e0a, 9.22:1
Danger#dc26274.83:1, passes#dc2627, 4.83:1White on #dc2627, 4.83:1
Info#0083e03.95:1, fails#0070c1, 5.14:1White on #0070c1, 5.14:1

Three of the four seeds fail as text. Warning is the extreme case: white text on amber reaches only 2.15:1, so amber buttons and banners always take dark text. Success at step 600 (#008c3b) also falls short with 4.36:1, which is why the table jumps to 700.

How many colors a UI needs and the 60-30-10 rule

A typical product UI needs one brand scale, one neutral scale and four semantic scales. That sounds like 66 colors, but a real interface uses about 15 of them: three or four neutrals for surfaces and borders, three for text, two or three brand steps and two steps per status color. A second brand color is optional and often makes things worse.

The 60-30-10 rule comes from interior design and works for interfaces too: about 60% of the screen is the dominant color, 30% the secondary one and 10% the accent. In a UI it maps like this.

  • 60%, neutral surfaces: page background (neutral 50), cards (white or neutral 100), large empty areas.
  • 30%, neutral structure and text: borders (neutral 200 and 300), body text (neutral 900), secondary text (neutral 600).
  • 10%, brand accent: primary buttons, links, focus rings, selected states (brand 600, hover 700, light tint 50 or 100 for selected rows).
  • Semantic colors sit outside the rule. They appear only when there is something to report.

If the brand color covers more than about a tenth of the screen, it stops pointing at anything. Keep it for the one action you want people to take.

Test contrast for every text pair

Check pairs, not colors. The same muted gray can pass on white and fail on a light gray card. These are the pairs a palette built from the scales above actually uses, with the WCAG 2 ratio and the APCA Lc value.

UseTextBackgroundWCAG 2APCA LcResult
Body text#090a0c#fafafa18.97:1102.9AAA
Secondary text#4d5158#ffffff7.97:187.7AAA
Muted text#6e7279#ffffff4.83:173.5AA
Muted text on card#6e7279#f4f5f64.43:167.5Fails AA
Placeholder#9ca0a6#ffffff2.63:151.2Fails AA
Link#2f6bdf#ffffff4.90:173.4AA
Primary button#ffffff#2f6bdf4.90:1-78.8AA
Button on step 500#ffffff#4a83f03.62:1-69.0Large text only
Brand badge#2158c3#e0eafa5.33:168.5AA
Error message#bf0a17#fdf2f15.85:173.1AA
Warning banner#090a0c#f59e0a9.22:162.0AAA

Two rows catch most palettes out. Muted text drops below 4.5:1 as soon as it sits on neutral 100, so use neutral 600 on cards. Placeholder text is still text under WCAG, so neutral 400 is fine for disabled labels and icons, but not for placeholders people need to read.

Test the palette for color blindness

Red and green are the classic problem, and scales make it easy to measure. The table gives the OKLCH distance (ΔE OK) between success and danger, normally and as simulated for protanopia, deuteranopia and full grayscale. Below about 0.05, two colors are easy to confuse.

PairNormalProtanopiaDeuteranopiaGrayscale
Success 600 and danger 6000.3270.1340.0540.024
Success 700 and danger 6000.3230.0640.1110.042

Taking success one step darker doubles the distance for deuteranopia but halves it for protanopia. No choice of steps keeps red and green apart for every viewer, so never let color carry the meaning alone: add an icon and a word such as "Error" or "Saved".

Also compare the brand with info. #2f6bdf and #0070c1 are only 0.054 apart with normal vision. In a blue-branded product, either use the brand scale for info messages or move info toward cyan.

Export the palette as CSS variables

Write the scales as custom properties in OKLCH. Lightness is visible in the first number, so anyone reading the code can see that brand 600 at 55.5% passes on white and brand 500 at 62.7% does not. Each value below converts back to exactly the HEX in the tables.

CSS

:root {
    --brand-50: oklch(96.9% 0.01 261.8);
    --brand-100: oklch(93.4% 0.024 259.8);
    --brand-200: oklch(88.2% 0.047 261.4);
    --brand-300: oklch(80.5% 0.082 262.3);
    --brand-400: oklch(70.3% 0.137 261.7);
    --brand-500: oklch(62.7% 0.175 261.9);
    --brand-600: oklch(55.5% 0.188 261.8);
    --brand-700: oklch(49% 0.177 261.9);
    --brand-800: oklch(43.1% 0.147 261.8);
    --brand-900: oklch(38.6% 0.11 261.4);
    --brand-950: oklch(27% 0.071 261.9);

    --gray-50: oklch(98.5% 0 0);
    --gray-100: oklch(97% 0.002 247.8);
    --gray-200: oklch(92.2% 0.003 264.5);
    --gray-300: oklch(86.9% 0.005 258.3);
    --gray-400: oklch(70.4% 0.01 258.3);
    --gray-500: oklch(55.1% 0.012 261.8);
    --gray-600: oklch(43.4% 0.013 261.8);
    --gray-700: oklch(36.8% 0.013 267.2);
    --gray-800: oklch(26.8% 0.01 260.7);
    --gray-900: oklch(20.4% 0.009 264.4);
    --gray-950: oklch(14.5% 0.005 263.8);

    --success: oklch(49.4% 0.14 148.9);
    --warning: oklch(76.9% 0.165 70);
    --warning-text: oklch(53.9% 0.117 69.7);
    --danger: oklch(57.7% 0.215 27.1);
    --info: oklch(53.7% 0.152 249.9);

    --background: var(--gray-50);
    --surface: #ffffff;
    --border: var(--gray-200);
    --text: var(--gray-900);
    --text-muted: var(--gray-500);
    --primary: var(--brand-600);
    --primary-hover: var(--brand-700);
}

Components should use only the role variables at the bottom. When the brand changes, you regenerate the scales and nothing else moves. The palette generator on PickShade builds this kind of scale from any color and checks the pairs for you.

Questions and answers

How many colors should a website palette have?

A website palette usually needs one brand color, one tinted gray and four semantic colors for success, warning, danger and info. Each gets a scale of shades, but a real interface uses only about 15 of them: a few grays for surfaces and text, two or three brand steps and two steps per status color.

What is the 60-30-10 rule in web design?

The 60-30-10 rule splits a screen into about 60% dominant color, 30% secondary color and 10% accent. In a website the 60% is neutral backgrounds, the 30% is borders and text, and the 10% is the brand color on buttons, links and selected states.

Which shade of my brand color can I use for text on white?

Use a shade with OKLCH lightness of about 0.55 or lower. At that lightness any hue reaches the WCAG AA ratio of 4.5:1 on white, while shades above about 0.61 fail whatever the hue and however dark they look. For the blue #2f6bdf, with lightness 0.555, the ratio is 4.90:1.

Should I use pure gray or tinted gray for a website?

Tinted gray usually looks better. Give the grays the hue of your brand color with a very small chroma, around 0.002 to 0.013 in OKLCH. The tint is too weak to notice as a color, but it makes the grays feel related to the brand instead of cold.

Why can’t I use white text on a yellow or amber button?

Yellow and amber are very light, so white text has almost no contrast on them. White on the amber #f59e0a gives only 2.15:1, far below the 4.5:1 WCAG AA needs, while near-black text on the same color gives 9.22:1. Use dark text on yellow, amber and light green.

Try it in PickShade

Related guides