Designing with AI — From Prompt to Product — 7. Advanced Color: Palettes, Dark Mode and Total Accessibility

20 min read min de lecture
Chapter 07

Advanced Color: Palettes, Dark Mode and Total Accessibility

Chapter 7 of 10 · 70%

Chapter objectives

  • Extend the palette into coherent tint scales
  • Build a dark mode that isn't a simple negative
  • Guarantee accessibility beyond text: color blindness, non-text, states

When a palette meets the real world

Two pieces of feedback land the same morning at Studio Mango. The first comes from a Sereno beta tester: "I'm color-blind, and I can't tell the difference between your success and error messages." The second comes from the client: "My users meditate in the evening — we need a dark mode." Two different requests, one same lesson: the chapter 2 palette, perfect in ideal conditions, must now face the real diversity of eyes and usage contexts.

This chapter completes your color toolbox on four fronts: extending each color into a tint scale (because eight colors never suffice for long), reasoning in perceived lightness with OKLCH, building a dark mode worthy of the name, and pushing accessibility beyond text contrast — color blindness, non-text elements, semantic states. By the end, you'll no longer deliver a palette: you'll deliver a complete theme.

From colors to tint scales

Your --color-primary token is a single tint. But as soon as the interface grows, you need its neighbors: a very light version for a banner background, a dark version for hover, a near-black version for text on a light background. Rather than improvising these variants case by case, professional design systems define a scale of 9 to 11 shades per tint, numbered from 50 (the lightest) to 900 (the darkest).

Each number has a conventional role: 50-100 for subtle tinted backgrounds, 200-300 for borders and disabled states, 500 as the base color, 600-700 for hovers and active states, 800-900 for tinted text on a light background. This convention makes decisions fast and consistent: "the info banner uses sage-50 as background and sage-800 as text" is understood immediately, and the contrast is almost guaranteed by construction — the extremes of a well-built scale pass AA against each other.

css
:root {
  /* Sage scale — generated at regular perceived lightness */
  --sage-50:  #F2F6F4;   /* subtle tinted backgrounds */
  --sage-100: #E1EAE6;
  --sage-200: #C4D5CE;   /* borders, dividers */
  --sage-300: #A3BDB3;
  --sage-400: #7E9D91;
  --sage-500: #4A7C6F;   /* base: buttons, links */
  --sage-600: #3D685D;   /* hover */
  --sage-700: #32554C;   /* active */
  --sage-800: #27423B;   /* tinted text on light background */
  --sage-900: #1C302B;
}

/* Roles point to the scale — never the other way around */
:root {
  --color-primary: var(--sage-500);
  --color-primary-hover: var(--sage-600);
  --color-primary-surface: var(--sage-50);
}

Note the two-tier architecture of the code above: the raw scale (--sage-500) on one side, the functional roles (--color-primary) on the other, with roles pointing to the scale. This indirection is the key to the dark mode coming up below: you'll change what the roles point to, without touching either the scale or the components.

OKLCH: reasoning in perceived lightness

To generate these scales, the color format matters more than you'd think. Classic HSL has a known flaw: its "lightness" lies. A yellow and a blue at 50% HSL lightness don't have the same brightness perceived by the eye at all — the yellow looks dazzling, the blue dark. Consequence: a scale generated in HSL has irregular steps, and contrast ratios become unpredictable from one tint to another.

The modern OKLCH format fixes this: its L axis measures lightness as the eye perceives it. Two OKLCH colors with the same L genuinely look equally light, whatever the hue. For you, it's a practical superpower: ask for your scales "in OKLCH, with a regular lightness step", and all your tints (sage, peach, error red) will have aligned shades — the green's 600 and the red's 600 will offer the same contrast on a white background.

OKLCH is supported by all modern browsers. For very old browsers, ask the AI to provide the hexadecimal fallback for each value — two lines per token, the hex version first, the oklch version second.
PROMPT
Extend my Sereno design system into complete tint scales:
- for each base color (sage #4A7C6F, peach #E8A87C, error red #B5544D), generate a scale of 10 shades (50 to 900) in OKLCH with a regular perceived-lightness step
- keep the hue and adjust mostly lightness and chroma: light shades slightly desaturated, dark ones slightly more saturated
- give for each shade: the oklch() value, the hex fallback, and the contrast ratio on white AND on #1A1F1D
- end with a table of guaranteed AA combinations: which text number on which background number
Format: commented CSS :root block.

Dark mode is not a negative

First intuition to kill: dark mode is not the inversion of the light palette. Inverting produces a pure black background (#000) that makes white text vibrate, saturated colors that turn fluorescent, and invisible shadows. A good dark theme rests on four principles. One: a very dark tinted gray background (for Sereno, a deep gray-green like #141816), never pure black. Two: desaturated colors — on a dark background, the same saturation looks louder, so you bring the chroma down a notch.

Three: elevation through lightness. In light mode, a card stands out by its shadow; in dark mode, shadows no longer show — it's the surface itself that lightens slightly (a card is a bit lighter than the background, a modal a bit lighter still). Four: off-white text rather than pure white (#E8ECEA rather than #FFFFFF), to reduce vibration during long nighttime reading — precisely Sereno's usage context.

And here is where your two-tier architecture pays off: to create the dark theme, you touch neither the components nor the scale — you re-declare only the roles in a prefers-color-scheme: dark block. --color-surface now points to the deep gray, --color-primary to a lighter sage-400 (because on a dark background, it's the light version of the tint that contrasts). Thirty lines of CSS, and the whole interface switches over.

css
/* Dark mode: re-declare the roles, nothing else */
@media (prefers-color-scheme: dark) {
  :root {
    --color-surface: #141816;          /* deep gray-green, no pure black */
    --color-surface-raised: #1E2421;   /* elevation through lightness */
    --color-text: #E8ECEA;             /* off-white, no pure white */
    --color-text-muted: #9DABA5;       /* re-checked: 5.2:1 on surface */
    --color-primary: var(--sage-400);  /* light version of the tint */
    --color-primary-hover: var(--sage-300);
    --color-primary-surface: #20302B;
    --shadow-soft: none;               /* elevation replaces the shadow */
    --shadow-raised: none;
  }
}
PROMPT
Generate Sereno's complete dark theme from my light tokens (below):
[paste your :root block + your scales here]
Rules:
- re-declare ONLY the roles in @media (prefers-color-scheme: dark), without touching the scales or the components
- deep gray-green background (no pure black), off-white text (no #FFF)
- slightly desaturate the accents, use the scales' light shades for primary
- replace shadows with elevation: surface-raised lighter than surface
- give the contrast ratio of EVERY text/background combination in the dark theme
- flag any combination under 4.5:1 and propose the fix
Dark mode resets every contrast counter to zero: a combination that is AA in light can fail in dark. The classic trap is --color-text-muted, already borderline in light, becoming unreadable on a dark background. Demand the numerical ratios for both themes.

Color blindness: never encode information by color alone

Back to the beta tester. About 8% of men and 0.5% of women perceive certain colors poorly — most often the red/green distinction, exactly the pair Sereno uses for error/success. The accessibility rule is absolute: color must never be the only channel carrying a piece of information. A red error message must also be signaled by an icon, a text prefix ("Error:"), or a distinct shape. An invalid field must have a thicker border or an icon, not just a red border.

The verification reflex: ask the AI to simulate. "Describe this interface as a deuteranope person would see it; list every place where information is carried only by color." You can also do the mental grayscale test: if the whole page went black and white, would the states remain distinguishable? If yes, your design is robust; if no, you know what to double up with an icon or a label.

WCAG beyond text: the 3:1 of interface elements

Chapter 2 covered text (4.5:1). But the WCAG also impose a minimum of 3:1 for meaningful non-text elements: a form field's border (otherwise the field is invisible), an icon that acts as a button, the focus ring, a checkbox's checked state, a chart's strokes. It's the blind spot of almost every pastel palette — that elegant light gray border in your mockup probably measures 1.8:1, and a visually impaired user simply can't find where to click.

Also think of semantic states as complete couples: each status color (success, error, warning, information) exists in a text version, a background version and a border version — and each combination passes its thresholds in both themes. This is the moment your system leaves the "pretty palette" stage to become a true production theme.

flowchart TD
  P["Base palette: roles and tints"] --> E["Scales 50 to 900 in OKLCH"]
  E --> T1["Text ratios: 4.5 to 1 minimum"]
  E --> T2["Non-text ratios: 3 to 1 minimum"]
  T1 --> D["Color blindness simulation: info never by color alone"]
  T2 --> D
  D --> S["Dark theme: roles re-declared and re-tested"]
  S --> V["Validated theme: light + dark documented"]
The color validation pipeline: from palette to complete theme, every step has its test.

Delivering a theme, not colors

Let's recap the final deliverable for Sereno's developer: the tint scales (the raw material), the functional roles in two declarations (light and dark), the complete semantic state couples, and a short usage document — which scale number for which use, which combinations are guaranteed, how to add a tint without breaking the system. Add respect for the user's choice: the theme follows prefers-color-scheme by default, but a manual switch must be able to force it, because meditating at night in forced light mode isn't a premium experience.

The color-blind beta tester will receive an interface where every state is doubled with an icon; the client will get a dark mode worthy of a nighttime meditation app. And you have learned the lesson that goes beyond color: a design system isn't judged in ideal conditions, but on the real diversity of eyes, screens and contexts it will meet.

🛠️ Your turn

Context

The client expects the dark mode for the next release, and the color-blind beta tester's feedback must be handled in the same pass. You have your chapter 2 tokens and half a day. The deliverable: a complete light + dark theme, verified beyond text, with its usage documentation for the developer.

Instructions

  1. Have the 10-shade scales (50-900) generated in OKLCH for your three main tints, with hex fallbacks and contrast ratios.
  2. Restructure your tokens into two tiers: raw scales on one side, functional roles on the other.
  3. Generate the dark theme by re-declaring only the roles: deep tinted background, desaturated accents, elevation through lightness.
  4. Demand the numerical ratios for every combination in both themes and have everything below 4.5:1 (text) or 3:1 (non-text) fixed.
  5. Run the color blindness audit: ask for the list of information carried only by color, and double each with an icon or a label.
  6. Test the full landing in both themes (including the form and its error states) and write the 10 lines of usage documentation.
Hint — Start with the two-tier architecture (scales → roles): it's what makes dark mode nearly free. If you have to modify a component for the dark theme, it's the sign that a hard-coded value escaped the roles.

In summary

  • A base color becomes a scale of 9-11 shades (50-900), each number having a conventional role.
  • The two-tier architecture — raw scales, functional roles pointing at them — makes themes interchangeable.
  • OKLCH measures perceived lightness: regular scales and predictable contrasts from one tint to another.
  • Dark mode is not a negative: deep tinted background, desaturated accents, elevation through lightness, off-white.
  • All contrasts must be re-tested in dark — muted text is the classic trap.
  • Information must never be carried by color alone: icon, label or shape as a second channel (8% of men are color-blind).
  • The WCAG also impose 3:1 on non-text elements: field borders, active icons, focus, states.

Quiz — check your understanding

1. What is a tint scale (50 to 900) for?

Each number has a conventional role (50 = subtle background, 500 = base, 600 = hover, 800 = tinted text): decisions become fast and contrast predictable.

2. What is OKLCH's advantage over HSL?

In HSL, a yellow and a blue at "50%" don't have the same perceived brightness. OKLCH's L axis is perceptual: same L = same felt lightness, hence aligned contrasts across tints.

3. How do you build a good dark mode?

Inversion produces fluorescent colors and invisible shadows. You re-declare only the roles: deep tinted gray, off-white, the scales' light shades for the accents.

4. What should you do for a color-blind user who confuses error and success messages?

Color must never be the only channel of a piece of information. The mental test: in black and white, the states must remain distinguishable.

5. What minimum contrast ratio applies to meaningful non-text elements (field borders, active icons)?

The WCAG require 3:1 for interface components: a field border at 1.8:1 makes the form unfindable for a visually impaired user.

6. Why re-test all contrasts after creating the dark theme?

Each role points to new values: the ratios are entirely recalculated. Muted text, already borderline in light, is the first to fall under the threshold.

Auteur(s)

R

REHOUMA Haythem

Haythem Rehouma est un ingénieur et architecte IA et cloud, formateur et enseignant technique, avec un profil orienté IA médicale, AWS, MLOps, LLM/RAG et vision par ordinateur.