EngineeringLive2025

OKLCH Palette Generator

A tool that anchors to one reference colour and generates full, perceptually uniform colour scales.

Adopted by Kinesis for primitive colour creation in the Nebula; shipped in days.

Live at oklch-palettes-generator.lovable.app
Role
Designer and engineer
Timeline
Feb - Mar 2025
Team
Solo
Platform
Web
  • Lovable
  • colorjs.io
  • OKLCH
  • Figma SupaPalette
  • Colour
  • Tooling
  • Design systems
The Palette Generator web app: an anchor colour of #017DE8 analysed as closest hue Indigo, closest tone Saturated, step 450; a brand colour named Midnight being added; and a palette preview of fifteen hue rows from Stone to Orange, each a ramp of swatches from near-white to near-black, under the headings Vibrant and Saturated

01 / What it is

A web tool that takes one reference colour and generates complete, perceptually even colour scales, with your brand colours kept intact.

02 / Why it existed

Mapping colour steps by hand takes weeks of trial and error. This does it in minutes, and you do not need to understand OKLCH to use it.

03 / What I achieved

Adopted by Kinesis for the Nebula's primitives, reused for ClubHost, and built in days with AI-assisted development.

palettes in a single generation
97
colours from one anchor
2,037

Overview

You give the generator one anchor colour and it produces full colour scales across configurable steps, hues and tones. It works in OKLCH, a colour space where equal steps look equal to the eye, which is what makes the scales usable for accessible UI. It places your anchor on the hue wheel, gives it a tone and a step, lets you add brand colours that keep their identity, and exports JSON that imports straight into Figma as variables.

The Configure Scale Steps panel over the generator: tabs for Hues, Brand, Monochrome and Transparent, a preset dropdown, the 21 hue steps listed from 25 upwards, and a greyscale preview of the distribution from 25 to 950
Every scale is configurable: the steps panel sets how many stops a palette has and where they fall, with the distribution previewed before it is applied

Why it existed

Building colour scales by hand in OKLCH is slow. A 21-step vibrant red means working out the lightness ramp, holding the chroma at the edge of what screens can show, and checking every step looks evenly spaced. Then the saturated, muted, pastel, tinted and shaded versions. Then fifteen hues. Without a tool it is weeks of spreadsheets and plugins.

I built it during the colour alignment project across the Kinesis web and app platforms, where the team was stuck on dark-mode palettes that kept the brand's vibrancy. Mixing black and white into a brand colour goes muddy. OKLCH does not, because its lightness is perceptual rather than additive.

My role

I designed and built it. I worked out the colour-space logic, the palette calculations and the gamut-mapping fix, and designed a UI that lets designers tune steps without breaking brand hue relationships. Built with AI-assisted development on Lovable.

Approach

Engineering

Engineering

The generator uses colorjs.io for the colour-space maths. The core algorithm takes an anchor colour, analyses it against the OKLCH hue wheel to find its closest hue, closest tone and step, then generates palettes across configurable steps (21 by default) and tones: Vibrant, Saturated, Muted, Pastel, Tinted, Shaded, Neutral and Transparent.

The tones are spaced by chroma, the colourfulness axis, in even gaps: Vibrant sits at the ceiling of what screens can display, and each tone below it steps down by the same amount, so the drop from Vibrant to Saturated looks the same as the drop from Saturated to Muted.

The hardest problem was gamut clipping. Asking for a vibrant red produced pink, because the naive way of forcing a colour onto a screen clamps each channel independently and shifts the perceived hue. The fix was perceptual gamut mapping: reduce the chroma until the colour fits, keeping the hue angle exactly where it was. The colour comes out slightly less saturated, but it is the right colour. I applied it across all five generators: hue and tone, monochrome, brand, tinted and shaded.

Brand colours are analysed and placed by their OKLCH position. A vibrant coral lands at step 350; a near-black midnight blue lands at 850. The brand colour keeps its identity in the palette rather than being snapped to the nearest generated step.

Design

Design

The UI exposes decisions, not maths. The anchor colour is a single input. The analysis is shown as read-only context. Brand colours are added by name and hex, and the tool handles the conversion and step assignment. Tones are toggled with a checkbox each; steps and hues have their own edit panels.

The "Include tints and shades" toggle adds two categories: Tinted, near-white surfaces with a trace of the hue for light-mode accented backgrounds, and Shaded, their dark-mode counterparts. These are the subtle hints that solve the muddy dark-mode problem. They are not full tones; they are backgrounds.

The palette preview is the output: a grid of swatches, click any to copy its code. The JSON export matches the SupaPalette plugin's format, so the palettes import directly into Figma as variables.

Key decisions

Decision

Perceptual gamut mapping vs naive channel clamping

  • Naive channel clamping (force each RGB channel into range)
  • Perceptual gamut mapping (reduce chroma until the colour fits)· chosen
Why
Clamping shifts the perceived hue: a vibrant red becomes pink because the red channel hits its ceiling before green and blue. Gamut mapping keeps the hue angle and gives up a little saturation instead.
What it cost
Slightly more computation per palette, and a dependency on colorjs.io.

Decision

Tinted and Shaded as separate tones vs modifying Pastel

  • Modify the existing Pastel tone's lightness range
  • Create new Tinted and Shaded tones with distinct purposes· chosen
Why
Pastel is a full tone. Tints and shades are hints: near-white and near-black surfaces with a trace of the hue. Folding them into Pastel would have conflated two different purposes.
What it cost
Two more palette categories to generate and maintain.

Decision

SupaPalette JSON export vs a custom Figma plugin

  • Build a custom Figma plugin to import the palettes
  • Export JSON in a format an existing plugin already imports· chosen
Why
SupaPalette already imports JSON into Figma as variables. A custom plugin would duplicate that work. The generator outputs the right format; the plugin does the import.
What it cost
Dependency on that JSON format staying stable.

Highlights

The Configure Hues panel: sixteen named hues from Monochrome and Stone through Amber, Yellow, Lime, Green and Teal, each with its hue angle and an optional custom chroma, above a hue spectrum preview strip
Hues are named, not numbered: each row is a hue angle and an optional chroma override, so the team edits colours in their own vocabulary
The Configure Tones panel: Vibrant, Saturated, Muted and Pastel, each with a one-line description, a chroma slider and a swatch, and a chroma comparison strip beneath
Tones are chroma presets with a purpose written next to each one, and a comparison strip shows the step-down between them

Outcome

palettes in one generation
97
colours from one anchor
2,037

Kinesis adopted it for primitive colour creation in the Nebula. The team stopped guessing colour steps, and a designer who had never heard of OKLCH could produce a consistent, accessible scale in minutes. It became the single source of truth for primitives, and I used it again for the ClubHost design system.

Learnings and what's next

AI-assisted development works best when I own the thinking and the AI owns the typing. I reasoned through the colour space, the tone spacing and the gamut fix; the AI built the UI and wired the generators. Without that split the tool would have been generic.

Next: a view showing which Nebula tokens reference each generated palette, closing the loop from generation to use.