Color Theory (Desktop)
Build semantic roles, not a swatch list
Don't start from "here are our 8 brand colors." Start from the roles the UI actually needs, then assign colors to roles:
| Role |
Purpose |
surface-base |
The window's base background |
surface-raised |
Cards, panels, sidebars — one step above base |
surface-overlay |
Modals, popovers, menus — floats above everything |
border-subtle / border-strong |
Hairline dividers vs. emphasized separators |
text-primary / text-secondary / text-tertiary |
Full-contrast body text, muted supporting text, disabled/placeholder text |
accent |
The single brand action color — used for primary buttons, active states, links |
success / warning / danger |
Status colors, used consistently everywhere they appear |
Every other color decision in the app should trace back to one of these roles. If you can't name which role a color is serving, it probably shouldn't be there.
The 60-30-10 balance
Roughly 60% of visible surface should be surface-base/neutral, 30% secondary surfaces and text, and only about 10% should be the accent color and status colors combined. Apps that break this — heavy accent color everywhere, saturated backgrounds — read as loud rather than confident. The accent color has more impact precisely because it's used sparingly.
Avoid the AI-purple trap
Purple/violet-to-pink gradients as a primary or accent color are the strongest single "this is an AI-generated app" visual signal that currently exists. This doesn't mean purple is banned — it means:
- If the brand color happens to be purple, use it as a flat, specific hue (not a gradient sweep) with a name and a hex value, not "purple-ish gradient."
- Don't reach for purple-to-pink by default when no brand color has been specified — that's the generator's laziest option, not a neutral choice.
- Prefer a color story that's actually tied to the product (an existing brand motif, logo color, or the mood of what the app does) over a generic "trustworthy tech" gradient.
Stacked / layered components — elevation via more than just shadow
When components sit on top of other components (a card inside a panel inside the window, a dropdown above a card, a modal above everything), each layer needs to read as clearly above or below the one beneath it. Use two or three of these together, not shadow alone:
- A one-step surface color shift (surface-base → surface-raised → surface-overlay), even a very subtle one (a few percent lightness difference is enough in most themes).
- A cheap box-shadow, sized to the elevation level from foundations (
raised gets a small tight shadow, floating/modal get a larger, softer one) — never rely on backdrop-blur alone for this, it's expensive and doesn't read as elevation on its own.
- A hairline border at low opacity, especially useful in dark mode where shadows read less clearly against dark backgrounds.
Never stack more than 3 visually distinct elevation levels on screen at once — beyond that, users lose track of what's "above" what, and it usually means the layout needs restructuring rather than a fourth shadow tier.
Active / selected / hover state color logic
Use a consistent, predictable transformation from rest → hover → active/selected across every component, rather than inventing a new treatment per component:
| State |
Typical treatment |
| Rest |
Base surface/text color for that role |
| Hover |
Small background tint shift toward the accent color (5-10% mix), or a subtle surface-level lightening |
| Active / pressed |
Slightly stronger tint than hover, plus the scale-down motion from desktop-ui-motion |
| Selected (persists after click — e.g. active sidebar item, chosen tab) |
A clear, ongoing marker: accent-colored background tint AND either a left-edge bar, an underline, or a filled icon state — never color alone, since color-only selected states are easy to miss and fail accessibility contrast checks for some users |
Dark mode is not "invert the colors"
Build dark mode as its own set of role assignments, not a CSS filter. In particular:
- Shadows read poorly on dark backgrounds — lean more on the surface-color-shift and border techniques above.
- Pure black (
#000) backgrounds create harsh contrast against white text and cause halation/glow on OLED and some LCD panels — prefer a very dark neutral gray (e.g. #0a0a0c–#121214 range) as surface-base.
- Saturated accent colors often need to be slightly desaturated or lightened in dark mode to avoid vibrating against the dark background.
Accessibility baseline
Body text against its background should meet at least a 4.5:1 contrast ratio; large text (18px+/bold 14px+) can go as low as 3:1. Never use color as the only signal for an important state (error, success, selected) — always pair it with an icon, text, or shape change.
Review format
| Before |
After |
Why |
| Purple-to-pink gradient as primary accent |
Flat, specific brand hue |
Gradients-as-accent are the top AI-generated-app tell |
| Selected sidebar item shown only via a lighter background |
Background tint + left-edge accent bar |
Color-only state markers are easy to miss and fail some contrast checks |
| Every card the exact same background as the window |
Cards one step up the surface scale (surface-raised) |
Without a surface shift, layering has to be carried entirely by shadow, which reads weaker |
1---2name: desktop-ui-color-system3description: Use whenever choosing a color palette, defining semantic colors, deciding how stacked/layered surfaces should read, styling active/selected/hover states, or reviewing an app for color inconsistency or the "AI purple gradient" look. Depends on desktop-ui-foundations. Trigger on "color palette," "dark mode," "this looks too purple/generic," "active state color," "elevation," or "stacked components."4---56# Color Theory (Desktop)78## Build semantic roles, not a swatch list910Don't start from "here are our 8 brand colors." Start from the roles the UI actually needs, then assign colors to roles:1112| Role | Purpose |13|---|---|14| `surface-base` | The window's base background |15| `surface-raised` | Cards, panels, sidebars — one step above base |16| `surface-overlay` | Modals, popovers, menus — floats above everything |17| `border-subtle` / `border-strong` | Hairline dividers vs. emphasized separators |18| `text-primary` / `text-secondary` / `text-tertiary` | Full-contrast body text, muted supporting text, disabled/placeholder text |19| `accent` | The single brand action color — used for primary buttons, active states, links |20| `success` / `warning` / `danger` | Status colors, used consistently everywhere they appear |2122Every other color decision in the app should trace back to one of these roles. If you can't name which role a color is serving, it probably shouldn't be there.2324## The 60-30-10 balance2526Roughly 60% of visible surface should be `surface-base`/neutral, 30% secondary surfaces and text, and only about 10% should be the `accent` color and status colors combined. Apps that break this — heavy accent color everywhere, saturated backgrounds — read as loud rather than confident. The accent color has more impact precisely because it's used sparingly.2728## Avoid the AI-purple trap2930Purple/violet-to-pink gradients as a primary or accent color are the strongest single "this is an AI-generated app" visual signal that currently exists. This doesn't mean purple is banned — it means:3132- If the brand color happens to be purple, use it as a **flat, specific hue** (not a gradient sweep) with a name and a hex value, not "purple-ish gradient."33- Don't reach for purple-to-pink by default when no brand color has been specified — that's the generator's laziest option, not a neutral choice.34- Prefer a color story that's actually tied to the product (an existing brand motif, logo color, or the mood of what the app does) over a generic "trustworthy tech" gradient.3536## Stacked / layered components — elevation via more than just shadow3738When components sit on top of other components (a card inside a panel inside the window, a dropdown above a card, a modal above everything), each layer needs to read as clearly above or below the one beneath it. Use two or three of these together, not shadow alone:39401. **A one-step surface color shift** (surface-base → surface-raised → surface-overlay), even a very subtle one (a few percent lightness difference is enough in most themes).412. **A cheap box-shadow**, sized to the elevation level from foundations (`raised` gets a small tight shadow, `floating`/`modal` get a larger, softer one) — never rely on `backdrop-blur` alone for this, it's expensive and doesn't read as elevation on its own.423. **A hairline border** at low opacity, especially useful in dark mode where shadows read less clearly against dark backgrounds.4344Never stack more than 3 visually distinct elevation levels on screen at once — beyond that, users lose track of what's "above" what, and it usually means the layout needs restructuring rather than a fourth shadow tier.4546## Active / selected / hover state color logic4748Use a consistent, predictable transformation from rest → hover → active/selected across every component, rather than inventing a new treatment per component:4950| State | Typical treatment |51|---|---|52| Rest | Base surface/text color for that role |53| Hover | Small background tint shift toward the accent color (5-10% mix), or a subtle surface-level lightening |54| Active / pressed | Slightly stronger tint than hover, plus the scale-down motion from `desktop-ui-motion` |55| Selected (persists after click — e.g. active sidebar item, chosen tab) | A clear, ongoing marker: accent-colored background tint AND either a left-edge bar, an underline, or a filled icon state — never color alone, since color-only selected states are easy to miss and fail accessibility contrast checks for some users |5657## Dark mode is not "invert the colors"5859Build dark mode as its own set of role assignments, not a CSS filter. In particular:6061- Shadows read poorly on dark backgrounds — lean more on the surface-color-shift and border techniques above.62- Pure black (`#000`) backgrounds create harsh contrast against white text and cause halation/glow on OLED and some LCD panels — prefer a very dark neutral gray (e.g. `#0a0a0c`–`#121214` range) as `surface-base`.63- Saturated accent colors often need to be slightly desaturated or lightened in dark mode to avoid vibrating against the dark background.6465## Accessibility baseline6667Body text against its background should meet at least a 4.5:1 contrast ratio; large text (18px+/bold 14px+) can go as low as 3:1. Never use color as the *only* signal for an important state (error, success, selected) — always pair it with an icon, text, or shape change.6869## Review format7071| Before | After | Why |72|---|---|---|73| Purple-to-pink gradient as primary accent | Flat, specific brand hue | Gradients-as-accent are the top AI-generated-app tell |74| Selected sidebar item shown only via a lighter background | Background tint + left-edge accent bar | Color-only state markers are easy to miss and fail some contrast checks |75| Every card the exact same background as the window | Cards one step up the surface scale (`surface-raised`) | Without a surface shift, layering has to be carried entirely by shadow, which reads weaker |