What this guide covers

  • Use layered dark neutrals, not pure black.
  • Give surface, card and elevated surfaces distinct steps.
  • Adjust accents for dark backgrounds instead of reusing light values.
  • Re-check every text pair in dark mode.
  • Test forms, dialogs and status messages, not just a hero screen.

Live theme

Preview a complete light and dark theme

Choose a sample palette, switch themes, and copy the CSS variables a real component library consumes.

Light
BrandHueDocsPricingGet started
Color system

A theme that holds up in both modes.

Surfaces, borders and text pairs are generated from the same palette. Switch the theme and compare the hierarchy.

Primary actionSecondary
Monthly reportUpdated today
+24.8%
Conversion8.4%
Activation62%

Semantic roles

Contrast checks

Light body text16.96:1AA
Light muted text16.96:1AA
Light primary button4.48:1Check
Dark body text17.85:1AA
Dark muted text7.77:1AA
Dark primary button4.48:1Check
Copy the theme
CSS variables
:root {
  --color-primary: #0B84D8;
  --color-primary-foreground: #111827;
  --color-secondary: #E0F2FE;
  --color-accent: #F59E0B;
  --color-neutral: #64748B;
  --color-background: #F8FAFC;
  --color-surface: #FCFDFE;
  --color-text: #111827;
  --color-muted-text: #111827;
  --color-border: #CFD4DC;
  --color-success: #22C55E;
  --color-warning: #F59E0B;
  --color-danger: #DC2626;
}

[data-theme="dark"] {
  --color-background: #0F172A;
  --color-surface: #0A1123;
  --color-text: #FFFFFF;
  --color-muted-text: #A2ACB9;
  --color-border: #344055;
  --color-primary: #0B84D8;
  --color-primary-foreground: #111827;
}
shadcn theme
:root {
  --background: #F8FAFC;
  --foreground: #111827;
  --card: #FCFDFE;
  --card-foreground: #111827;
  --primary: #0B84D8;
  --primary-foreground: #111827;
  --secondary: #E0F2FE;
  --secondary-foreground: #111827;
  --accent: #F59E0B;
  --accent-foreground: #111827;
  --muted: #FAFCFD;
  --muted-foreground: #111827;
  --border: #CFD4DC;
  --destructive: #DC2626;
  --destructive-foreground: #FFFFFF;
  --ring: #0B84D8;
}

.dark {
  --background: #0F172A;
  --foreground: #FFFFFF;
  --card: #0A1123;
  --card-foreground: #FFFFFF;
  --primary: #0B84D8;
  --primary-foreground: #111827;
  --secondary: #E0F2FE;
  --secondary-foreground: #111827;
  --accent: #F59E0B;
  --accent-foreground: #111827;
  --muted: #0D1427;
  --muted-foreground: #A2ACB9;
  --border: #344055;
  --destructive: #DC2626;
  --destructive-foreground: #FFFFFF;
  --ring: #0B84D8;
}
Figma tokens
{
  "color": {
    "primary": {
      "value": "#0B84D8",
      "type": "color"
    },
    "secondary": {
      "value": "#E0F2FE",
      "type": "color"
    },
    "accent": {
      "value": "#F59E0B",
      "type": "color"
    },
    "neutral": {
      "value": "#64748B",
      "type": "color"
    },
    "background": {
      "value": "#F8FAFC",
      "type": "color"
    },
    "surface": {
      "value": "#FCFDFE",
      "type": "color"
    },
    "text": {
      "value": "#111827",
      "type": "color"
    },
    "mutedText": {
      "value": "#111827",
      "type": "color"
    },
    "success": {
      "value": "#22C55E",
      "type": "color"
    },
    "warning": {
      "value": "#F59E0B",
      "type": "color"
    },
    "danger": {
      "value": "#DC2626",
      "type": "color"
    }
  },
  "dark": {
    "background": {
      "value": "#0F172A",
      "type": "color"
    },
    "surface": {
      "value": "#0A1123",
      "type": "color"
    },
    "text": {
      "value": "#FFFFFF",
      "type": "color"
    },
    "mutedText": {
      "value": "#A2ACB9",
      "type": "color"
    }
  }
}
OKLCH tokens
{
  "primary": "oklch(59.84% 0.1578 247.9)",
  "secondary": "oklch(95.14% 0.0250 236.8)",
  "accent": "oklch(76.86% 0.1647 70.1)",
  "neutral": "oklch(55.44% 0.0407 257.4)",
  "background": "oklch(98.42% 0.0034 247.9)",
  "foreground": "oklch(21.01% 0.0318 264.7)",
  "surface": "oklch(99.36% 0.0017 247.8)",
  "success": "oklch(72.27% 0.1920 149.6)",
  "warning": "oklch(76.86% 0.1647 70.1)",
  "danger": "oklch(57.71% 0.2152 27.3)"
}
Keep this palette

Email a private link to reopen it and its CSS, Tailwind, shadcn, Figma and OKLCH exports.

No account. We store the palette colors and your email, never the image.

Dark mode is a second system

A light theme and a dark theme can share a brand, but they cannot share every value. The background, surface, border, text and accent colors all interact differently on a dark canvas.

The naive approach is to invert the light theme. That produces pure white text on pure black, glowing accents and surfaces that disappear into the background. It may be technically dark, but it is not a designed interface.

A real dark theme starts with the same information hierarchy and rebuilds the color layers for a dark environment.

Build layered dark surfaces

Most dark interfaces need at least three neutral steps: the page background, the card surface and the elevated surface for menus and dialogs. The steps should be close enough to feel like one family and far enough apart to be distinguishable.

Pure black is usually too harsh. A very dark neutral with a slight tint from the brand hue can feel more intentional and gives the surfaces room to separate.

Borders in dark mode often work better as a low-contrast light line. A dark border on a dark surface is invisible; a light border defines the edge without shouting.

LayerExample roleWhat to check
BackgroundPageDarkest neutral, body text contrast
SurfaceCardVisible separation from background
ElevatedDialog or menuLighter again, or a stronger border
BorderEdgesVisible but not harsh
Muted surfaceSecondary panelDistinct from card and page

Text needs its own contrast pass

Pure white text on a dark background can be too harsh, especially for long reading. An off-white or very light neutral often reads better. But the softer color still needs to pass 4.5:1.

Muted text is the usual failure. A gray that looks subtle on a light background may disappear on a dark surface. Measure it against the actual dark surface, not the page background.

Placeholder text, helper text and disabled text are also content. If they carry information, they need to be readable. If they are purely decorative, they can be quieter.

Adjust accents for dark backgrounds

A saturated brand color that looks confident on white may vibrate on near-black. Reducing saturation and adjusting lightness often produces a more comfortable accent.

The primary button is the critical pair. If the dark-mode primary cannot support readable text, choose a different step. The original brand color can remain as a glow, a border or a decorative accent.

Do not assume the light-mode accent is accessible in dark mode. The background changed, so the ratio changed. Re-measure.

Status colors and non-color cues

Success, warning and danger colors need to stay distinguishable on dark surfaces. A deep red may disappear; a bright amber may glow. Adjust lightness separately from the brand accents.

Pair status with an icon or a label. A red border alone is not enough, especially for users with color-vision deficiency. A red border plus an error icon plus the word 'Error' is clear.

Check the status colors against the surface they appear on, not just the page background. A toast, an inline alert and a form error may use different surfaces.

Test the components that break

Forms are the best dark-mode test. Inputs, placeholders, borders, focus rings, helper text and error messages all use different pairs. If the form is readable, the theme is in good shape.

Dialogs and menus are the second test. Elevated surfaces need to separate from the page, and the overlay behind them needs to create focus without making the dialog disappear.

Charts and data visualizations are the third. Lines, labels and gridlines need enough contrast without turning the chart into a high-contrast maze.

  • Forms: input, placeholder, helper, error, focus.
  • Dialogs: overlay, surface, border, text.
  • Menus: selected state, hover, divider.
  • Charts: line, label, gridline, tooltip.
  • Status: icon, text, background.

A practical dark-mode workflow

Start from the light theme's roles, not its values. Choose a dark background family, then rebuild the surface hierarchy and the text pairs. Adjust the accents and status colors separately.

Measure every pair in the dark theme. Compare the screenshots side by side with the light theme. The hierarchy should feel the same even though the values are different.

Finally, test on a real device in a dark room. A theme that looks fine on a bright monitor may be too harsh or too dim in the environment where dark mode is actually used.

Keep a small dark-mode preview page in the design system with a heading, a paragraph, a form, a card, a dialog and a status message. When a token changes, open that page first. It catches the majority of regressions in a few minutes and gives designers and engineers a shared reference instead of an argument about whether a surface looks right.

A preview page is not a substitute for testing with real users, but it makes the common failures visible before they reach them.

PhotoColors Team

We build browser-based color tools and write about the unglamorous parts of color systems: contrast, tokens, handoff and the difference between a pretty palette and a usable one.

About PhotoColors