What this guide covers

  • Extract a real palette, then map it to the full shadcn variable set.
  • Generate both :root and .dark blocks.
  • Check primary-foreground, muted-foreground and border first.
  • Paste the variables into the stylesheet the components already import.
  • Verify buttons, cards, forms, dialogs and destructive actions.

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.

What shadcn/ui actually gives you

shadcn/ui is a collection of components that you copy into your project. The components are built with Tailwind and read from CSS variables, but they do not impose a fixed visual identity. That is the point: you own the theme.

The theme is a contract between the components and your brand. Every button, input, card, dialog and dropdown expects the same variable names. If one variable is missing or badly chosen, the failure appears across the whole interface.

A photo can provide the raw material for that contract, but it cannot provide the contract itself. You still need to decide which extracted color becomes primary, which becomes the background and which becomes the muted surface.

Extract the palette

Choose a photo with a clear color story, upload it, and review the extracted colors. Remove the accidents: white backgrounds, black shadows, skin tones that belong only in photography, and edge colors that do not appear in a solid area.

You do not need eight colors. A practical shadcn theme can be built from one primary, one neutral family and two support colors. The rest of the variables are derived from those.

If the photo is very warm or very cool, decide whether that is part of the brand. A warm photo will produce warm neutrals, which may make the interface feel cozy or dated depending on the product. You can neutralize the surface colors while keeping the brand accent.

Map the palette to shadcn variables

Start with `--background` and `--foreground`. The background is the page; the foreground is the body text. Check 4.5:1 before moving on.

Then set `--card` and `--card-foreground`. The card should be distinguishable from the background without a heavy border. `--popover` and `--popover-foreground` follow the same logic for menus and dialogs.

Set `--primary` and `--primary-foreground`. This is the button pair, and it is where most themes fail. If the extracted primary cannot support readable text, choose a darker or lighter step. Keep the original color as `--accent` or a decorative token.

Then set `--secondary`, `--muted`, `--muted-foreground`, `--border`, `--input`, `--ring` and `--destructive`. The muted foreground is the helper text; the border is the edge; the ring is the focus indicator.

VariableRoleFirst check
--backgroundPageBody text contrast
--cardPanelSeparation from background
--primaryActionButton text contrast
--secondarySupportText contrast
--mutedQuiet surfaceMuted text contrast
--borderEdgeVisible but not harsh
--ringFocusVisible on all surfaces
--destructiveErrorDistinct from primary

Generate the .dark block separately

Do not invert the light values. Choose a dark background family, then rebuild the surface hierarchy. The card should be slightly lighter than the page, and the popover slightly lighter again or clearly bordered.

The primary color usually needs adjustment. A blue that looks strong on white may glow on near-black. Reduce saturation or adjust lightness, then re-check `--primary-foreground`.

The muted surface and border need special attention in dark mode. A border that is too dark disappears; a muted surface that is too close to the background makes cards look flat. Test both.

Paste the theme and verify the components

Paste the `:root` and `.dark` blocks into the stylesheet your shadcn setup already imports. Keep the variable names exactly as the components expect them.

Then inspect five components: a button, a card, an input, a dialog and a destructive action. These cover the primary, surface, border, foreground and destructive pairs. If all five look intentional in both themes, the theme is in good shape.

Check the focus ring on each surface. A ring that is visible on the page but invisible on a card is a real accessibility problem. The ring token should have enough contrast against both.

Common mistakes

The first mistake is using the brand color as `--primary` without checking `--primary-foreground`. The second is making `--border` the same value as `--card`, which makes every component look borderless.

The third is forgetting `--ring`. The fourth is using `--muted` as a text color instead of a surface. `--muted-foreground` is the text token.

The fifth is treating dark mode as an afterthought. A theme that only works in light mode will look broken the moment the user switches. Generate both blocks together.

A worked example

Suppose a photo yields a deep teal, a warm sand, a dark charcoal and a pale cream. The teal becomes primary, but white text on the teal fails 4.5:1. A darker teal step becomes the primary, while the original teal becomes the accent.

The cream becomes the background and the sand becomes the card surface. The charcoal becomes the foreground. The muted text is a mid-neutral that passes 4.5:1 on both cream and sand.

For dark mode, the charcoal becomes the background, a slightly lighter charcoal becomes the card, and a desaturated teal becomes the primary. The cream is replaced by an off-white foreground, and the border becomes a low-contrast light line.

The result is not a copy of the photo. It is a theme that feels like the photo while remaining readable, hierarchical and consistent across components.

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