Deriving a Ghost theme palette from one accent color

Ghost gives themes a single accent color, but you can still get a full palette out of it using CSS color-mix() and oklch for hover, border and background states.
Deriving a Ghost theme palette from one accent color

The constraint: Ghost gives you one color

Ghost themes get exactly one brand color from the admin: --ghost-accent-color. That value often needs to carry most of the UI: links, buttons, hovers, subtle backgrounds, borders.

You can add more theme settings, but it is possible to treat that single accent as the source of truth and derive everything else in CSS.

With one custom property in package.json, the theme can generate tints, hover states, borders and soft backgrounds using color-mix() and, where helpful, oklch(). No extra admin fields are required.

Starting point: normalize the accent color

Ghost exposes the accent color as --ghost-accent-color on :root. A simple first step is to mirror it into a small set of semantic tokens so the rest of the CSS never touches the raw accent directly.

:root {
  /* from Ghost admin */
  --ghost-accent-color: #ff1a75;

  /* base semantic tokens */
  --accent: var(--ghost-accent-color);
  --accent-text-on-solid: #fff;
}

--accent is the only color that is really “brand”. Everything else hangs off it with color-mix().

Using color-mix() for hover and focus states

A straightforward use is hover states. For buttons and links you can create a slightly stronger accent on hover without guessing a separate color.

:root {
  --accent-strong: color-mix(in srgb, var(--accent) 85%, black);
}

.button-primary {
  background: var(--accent);
  color: var(--accent-text-on-solid);
}

.button-primary:hover,
.button-primary:focus-visible {
  background: var(--accent-strong);
}

This works well for mid-range colors, but it can break down at the extremes. A very dark accent mixed toward black barely changes. A very light accent mixed toward black can jump too hard and fail contrast against white text.

Very light and very dark accents

Two cases tend to cause trouble:

  • Almost-white accents, like #fbe9ff or very light pastels.
  • Almost-black accents, like #050505 or deep navy.

With almost-white accents, Ghost often pairs them with dark text, not white. If you always use --accent-text-on-solid: #fff and darken the accent on hover, the background can still end up with too little contrast against white text.

With almost-black accents, mixing toward black on hover does very little visually. The button looks unchanged.

I wrote about this earlier in CSS Variables in the Age of AI: Letting Models Design Your Themes.

Instead of assuming the accent is a normal mid-tone, it helps to reason about it in oklch().

Using oklch to reason about lightness

oklch() exposes a lightness component that behaves more linearly than srgb. That makes it easier to decide two things:

  • Should text on the accent be light or dark?
  • Should hover move the accent lighter or darker?

First, convert the accent to oklch() in a custom property. Browsers can do this conversion implicitly:

:root {
  --accent-oklch: color-mix(in oklch, var(--accent) 100%, black 0%);
}

Then derive lighter and darker variants by nudging the lightness channel via mixes toward black or white.

:root {
  /* Slightly darker for hover, slightly lighter for subtle backgrounds */
  --accent-strong-oklch: color-mix(in oklch, var(--accent) 90%, black 10%);
  --accent-soft-oklch:   color-mix(in oklch, var(--accent) 90%, white 10%);
}

These are still oklch-space colors, but the browser returns them as usable CSS colors. For many accents this gives smoother steps than mixing in srgb, especially for saturated colors.

Choosing text color based on accent lightness

Ghost itself decides whether to use light or dark text on the accent in some places, but inside a theme it is useful to have a consistent rule. Accents above a certain lightness can be treated as “light backgrounds” and use dark text on them.

There is no native if() in CSS yet, so a practical approach is to define two modes in the theme, then switch modes based on a class that is set server-side or through a small script that inspects the computed accent color.

:root {
  --accent-text-on-solid: #fff;
}

.theme--accent-light {
  --accent-text-on-solid: #111;
}

A script can read getComputedStyle(document.documentElement).getPropertyValue('--accent'), convert it to oklch() using a helper, and toggle .theme--accent-light if the lightness is above a threshold. Extremely light accents then flip to dark text, and the hover and active colors derived from oklch stay on the right side of the contrast line.

Subtle backgrounds and borders from the same accent

For cards, badges and inline highlights you can use the accent to create a soft background and a slightly stronger border.

More on this in my article Using the Ghost forum to earn trust before selling themes.

:root {
  --accent-bg-soft: color-mix(in oklch, var(--accent) 12%, white);
  --accent-border-soft: color-mix(in oklch, var(--accent) 45%, white);
}

.badge {
  background: var(--accent-bg-soft);
  border: 1px solid var(--accent-border-soft);
  color: var(--accent);
}

For very dark accents, mixing toward white like this keeps the badge readable without turning it into a loud block. For very light accents, the badge is barely tinted; the accent still shows up in the border and text.

The same pattern works for focus outlines:

:root {
  --accent-outline: color-mix(in oklch, var(--accent) 65%, white);
}

:focus-visible {
  outline: 2px solid var(--accent-outline);
  outline-offset: 3px;
}

Checking contrast without designing per brand

Because the accent is user-controlled, you cannot guarantee perfect contrast for every possible color. You can only make it harder to get into trouble.

A practical approach is to test a small grid of accent colors locally:

  • Very light pastel
  • Mid-tone brand color
  • Deep saturated color
  • Near-black

For each, check a few key pairs in a contrast checker:

  • --accent vs --accent-text-on-solid
  • --accent-bg-soft vs body text
  • Focus outline vs page background

oklch-based mixes help keep these pairs within acceptable ranges for normal accents. Extremely low-contrast choices from the admin, such as an almost white accent on a white site background, will still be low contrast, but the theme does not make that worse by using the accent for long-form body text.

Fallbacks for browsers without color-mix()

color-mix() is not universal yet, so the theme should keep a basic experience for older browsers.

Read also Pricing Northline and Azimuth as a new Ghost studio.

One pattern is:

:root {
  /* basic fallback */
  --accent-strong: var(--accent);
  --accent-bg-soft: rgba(0, 0, 0, 0.03);
}

@supports (color-mix(in srgb, black, white)) {
  :root {
    --accent-strong: color-mix(in oklch, var(--accent) 85%, black);
    --accent-bg-soft: color-mix(in oklch, var(--accent) 12%, white);
  }
}

In browsers without @supports (color-mix), buttons still use the accent, they just do not get a more intense hover color. Soft backgrounds fall back to a neutral tint instead of an accent-tinted one.

Keeping everything wired to one variable

The main benefit of this approach is that the entire palette stays wired to --ghost-accent-color. When someone changes the accent in Ghost admin, links, buttons, cards, focus states and badges all follow without touching the theme settings.

The trade-off is that you have to be conservative: avoid using the accent for long-form text, keep strong backgrounds limited, and lean on oklch and color-mix() to keep hover and subtle states in a safe range.

Subscribe to my newsletter

Subscribe to my newsletter to get the latest updates and news

Member discussion