Lora

Designing an organic color system

01 Jan 2026 · 7 min read

Color Design System

For a long time, the visual identity of this website was defined by a color system I built myself with a strong focus on accessibility.

My first attempt started with a simple goal: a scheme containing the main colors of the spectrum, where each hue had a well-specified contrast.

I used Leonardo to generate this initial palette, and for a while, it worked. It allowed me to build templates paying attention to accessibility standards.

But I ran into a problem.

As I deepened my understanding of color theory, I realized that my previous system —based on HSL and limited to only 7 swatches— was too rigid. It often forced me to choose between a color I liked and a color that was accessible. I lacked the nuance to fine-tune visual hierarchy, and the inconsistencies of the HSL color space meant that manipulating hues often broke the perceived lightness.

As noted in the W3C CSS Color Module Level 4 Editor’s Draft:

A disadvantage of HSL over OkLCh is that hue manipulation changes the visual lightness, and that hues are not evenly spaced apart.

I needed to move away from these inconsistencies. I didn’t just want an “accessible” palette; I wanted something organic, precise, and robust.

Lora

I found my aesthetic north star in Flexoki by Steph Ango. It’s an inky, analog-inspired theme that feels like writing on high-quality paper. It shifted my perspective on what a digital color scheme could feel like.

However, I didn’t just want to copy Flexoki’s values. I wanted to rebuild that organic feeling using a robust mathematical approach.

I wanted a name that reflected the system’s architecture, and I landed on Lora. It sounds organic and is pronounced /lo.ʁa/ —echoing the French pronunciation of “Laura”, my wife, to whom I dedicate this color palette.

It stands for:

To quickly test the two themes —light and dark— just press ‘D’ on this page to switch the theme.

The implementation

To build a truly cohesive system, I turned to OkLCh. It separates Lightness (L), Chroma (C), and Hue (h) in a way that aligns with how our eyes actually perceive color.

Interactive resources such as Huetone, oklch.fyi and color x color have become my daily playground for understanding this new space.

1. The hues

Value copied!

Is color subjective or objective? From a purely physical point of view, color is electromagnetic radiation, existing independently of the observer. The shade I chose as RED , for example, is objectively defined by specific coordinates.

However, perception is subjective. Each of us —or rather, our brains— interprets color with different sensitivity, influenced by biological, cultural, and historical contexts.

This means the colors in my palette were chosen according to my own logic and personal taste —which others may or may not like. Yet, utilizing the OkLCh model allows me to translate these subjective choices into a perceptually uniform system, ensuring the palette works harmoniously.

These are the definitions I chose for the color wheel:

/* --- colors definitions --- */
--hue-magenta: 0;
--chroma-magenta: 0.2;
--hue-red: 30;
--chroma-red: 0.2;
--hue-orange: 45;
--chroma-orange: 0.2;
--hue-yellow: 95;
--chroma-yellow: 0.2;
--hue-green: 135;
--chroma-green: 0.2;
--hue-teal: 180;
--chroma-teal: 0.2;
--hue-cyan: 225;
--chroma-cyan: 0.2;
--hue-blue: 270;
--chroma-blue: 0.2;
--hue-purple: 315;
--chroma-purple: 0.2;
Select
Hue
Hue --

The gray, which I consider to be a neutral base, is obtained simply by setting chroma to 0:

--hue-base: 0;
--chroma-base: 0;

2. The lightness curve

My previous palette had only seven steps. This was a significant constraint: I often ran out of options when trying to find combinations that met contrast requirements while still looking cohesive.

With Lora, I expanded the scale to thirteen steps.

This larger canvas gives me the freedom to dial in the exact contrast needed for readability without losing the character of the color.

The philosophy here is one of capacity rather than obligation; just because the system offers thirteen steps does not mean they must all be used, but having them available ensures I always have the right tool to solve the accessibility puzzle.

I divided the range from 0.15 (a “less white” white) to 0.95 (a “less black” black) into these values:

/* --- lightness curve (l) --- */
--L-100: 0.95;
--L-200: 0.9;
--L-300: 0.85;
--L-400: 0.8;
--L-500: 0.7;
--L-600: 0.6;
--L-700: 0.55;
--L-800: 0.5;
--L-900: 0.45;
--L-1000: 0.4;
--L-1100: 0.3;
--L-1200: 0.2;
--L-1300: 0.15;

These steps create a very linear trend:

base Lightness Curve
100
200
300
400
500
600
700
800
900
1000
1100
1200
1300
Value copied!

3. The chroma curve

After defining the lightness, I experimented with Chroma (saturation). Here is where the math gets interesting.

In the Lora system, I separate the curve shape from the color intensity:

The final value is simply the math of this relationship:

This allows me to tweak the saturation curve for the entire design system in one place, without having to manually rewrite the chroma values for every single color swatch.

/* --- chroma curve (c - multiplier) --- */
--C-scale-100: 0.1;
--C-scale-200: 0.2;
--C-scale-300: 0.3;
--C-scale-400: 0.45;
--C-scale-500: 0.6;
--C-scale-600: 0.8;
--C-scale-700: 0.95;
--C-scale-800: 1;
--C-scale-900: 0.95;
--C-scale-1000: 0.8;
--C-scale-1100: 0.6;
--C-scale-1200: 0.45;
--C-scale-1300: 0.35;

With these values, the curve for magenta looks like this:

magenta Chroma Curve
100
200
300
400
500
600
700
800
900
1000
1100
1200
1300
Value copied!

The result

The idea of how to implement this at the CSS level came from reading Easy theming with OKLCH colors by Manuel Strehl.

His work helped me understand how to make the system parametric, using CSS variables to generate the palette dynamically.

Here is the final result.

Lora color palette

100
200
300
400
500
600
700
800
900
1000
1100
1200
1300
Base
Magenta
Red
Orange
Yellow
Green
Teal
Cyan
Blue
Purple

Tokens for the current Hugo theme

ColorValue (OKLCH)ContrastLight TokenDark Token
base-100
calc...Aa
base-200
calc...Aa
base-300
calc...Aa
base-400
calc...Aa
base-500
calc...Aa
base-600
calc...Aa
base-700
calc...Aa
base-800
calc...Aa
base-900
calc...Aa
base-1000
calc...Aa
base-1100
calc...Aa
base-1200
calc...Aa
base-1300
calc...Aa
Copied!
ColorValue (OKLCH)ContrastLight TokenDark Token
cyan-100
calc...Aa
cyan-200
calc...Aa
cyan-300
calc...Aa
cyan-400
calc...Aa
cyan-500
calc...Aa
cyan-600
calc...Aa
cyan-700
calc...Aa
cyan-800
calc...Aa
cyan-900
calc...Aa
cyan-1000
calc...Aa
cyan-1100
calc...Aa
cyan-1200
calc...Aa
cyan-1300
calc...Aa
Copied!

TokenValue (OKLCH)ContrastLight MapDark Map
--tx-re // Error
calc...Aa......
--tx-or // Warning
calc...Aa......
--tx-ye // Yellow
calc...Aa......
--tx-gr // Success
calc...Aa......
--tx-cy // Accent
calc...Aa......
--tx-bl // Blue
calc...Aa......
--tx-pu // Purple
calc...Aa......
--tx-ma // Magenta
calc...Aa......
Copied!

Color contrast matrix

Base

TXT \ BG1002003004005006007008009001000110012001300
100
200
300
400
500
600
700
800
900
1000
1100
1200
1300

Accent

TXT \ BG1002003004005006007008009001000110012001300
100
200
300
400
500
600
700
800
900
1000
1100
1200
1300