DEV Community

Cover image for The Day I Stopped Writing Hover Colors Manually
Asmaa Almadhoun
Asmaa Almadhoun

Posted on

The Day I Stopped Writing Hover Colors Manually

Every frontend project starts the same way.

:root {
  --primary: #f59e0b;
  --primary-hover: #d97706;
}
Enter fullscreen mode Exit fullscreen mode

At first, it looks harmless.

Then the requirements grow.

:root {
  --primary: #f59e0b;
  --primary-hover: #d97706;
  --primary-active: #b45309;
  --primary-border: #ea9a00;
  --primary-shadow: rgba(245, 158, 11, 0.2);
}
Enter fullscreen mode Exit fullscreen mode

Somehow, one color became five.

And every time the design team changes the brand color, we repeat the same process:

  • Open the color picker
  • Make it darker for hover
  • Make it even darker for active
  • Adjust the shadow color
  • Hope everything still looks consistent

I never questioned this workflow.

Until I discovered OKLCH.


The Problem We Pretend Doesn't Exist

Imagine our primary color is:

--primary: #ffa000;
Enter fullscreen mode Exit fullscreen mode

Now we need a hover color.

What do we do?

Usually something like:

--primary-hover: #f29000;
Enter fullscreen mode Exit fullscreen mode

But how did we get that value?

Most of the time:

  • Trial and error
  • Design tool adjustments
  • Visual guessing

There is no real system.

We're manually creating color variations instead of describing how a color should change.


Then I Found This Weird Syntax

One day I came across this:

background: oklch(
  from var(--primary)
  calc(l - 0.05)
  c
  h
);
Enter fullscreen mode Exit fullscreen mode

My first reaction:

Wait...

I can generate the hover color from the original color?

No extra color variable?

  • Yes

What OKLCH Changed for Me

Instead of storing multiple versions of the same color:

--primary
--primary-hover
--primary-active
Enter fullscreen mode Exit fullscreen mode

I only keep the source color:

--primary: oklch(0.75 0.18 70);
Enter fullscreen mode Exit fullscreen mode

Then create variations on demand.

Hover:

background: oklch(
  from var(--primary)
  calc(l - 0.05)
  c
  h
);
Enter fullscreen mode Exit fullscreen mode

Active:

background: oklch(
  from var(--primary)
  calc(l - 0.1)
  c
  h
);
Enter fullscreen mode Exit fullscreen mode

Focus:

background: oklch(
  from var(--primary)
  calc(l + 0.05)
  c
  h
);
Enter fullscreen mode Exit fullscreen mode

The color stays the same.

Only its lightness changes.


The Moment It Clicked

Traditional CSS asks:

What should the hover color be?

OKLCH asks:

How should the base color change?

That small shift in thinking makes a huge difference.

Before:

:root {
  --success: #22c55e;
  --success-hover: #16a34a;

  --warning: #f59e0b;
  --warning-hover: #d97706;

  --error: #ef4444;
  --error-hover: #dc2626;
}
Enter fullscreen mode Exit fullscreen mode

After:

:root {
  --success: oklch(...);
  --warning: oklch(...);
  --error: oklch(...);
}
Enter fullscreen mode Exit fullscreen mode

And every hover color is generated automatically:

button:hover {
  background: oklch(
    from var(--button-color)
    calc(l - 0.05)
    c
    h
  );
}
Enter fullscreen mode Exit fullscreen mode

No duplicate color tokens & No maintenance burden.


Tailwind Is Already Doing This

Many developers still think in HEX and RGB.

The industry isn't moving toward OKLCH because the syntax is cool.

It's moving toward OKLCH because generating color variations mathematically is more reliable than manually picking dozens of related colors.


The Hidden Benefit During Rebranding

Imagine the design team changes the primary color.

Before:
All need review.

After:
Change one value.

Everything derived from it updates automatically.

Hover, Active, Focus, Shadows, Borders, and Glows.

All stay consistent.

Top comments (0)