Skip to content
ChromaBrew
Color scalesBy Opeyemi Olorunfemi

Hover, active, disabled: deriving interactive states from a scale

Interactive states are lightness moves within a derived scale — not new hues. How to derive hover, active, focus and disabled from one primary scale, and keep them accessible.

States are the same hue, moved in lightness

A control in its default, hover, active and focus states should be unmistakably the same control. That reads best when the states are lightness moves within one scale: the default sits on the 600 step, hover steps to 700, and active (pressed) steps one further to 800. The hue never changes, so the eye tracks the element instead of reassessing it.

In dark mode the same logic inverts — surfaces move lighter, not darker, so hover shifts the primary toward a lighter 500 and active back toward 600. Deriving both directions from one scale is how paired light/dark themes stay siblings.

How a generator derives hover and active

In ChromaBrew's design system, hover and active variants are computed from the primary by lightness math in OKLCH, not by guessing percentages. The primary keeps its hue, the state token just relocates it on the lightness axis — so a primary of #6d28d9 yields a primary-hover and primary-active that are visibly more and more 'pressed' while staying the same color family.

The focus ring is a distinct consideration: it is a visibility cue, not a state change, and for default light themes it aliases to the primary color itself. Focus-visible styling is about location — a high-chroma ring around the element — so it intentionally reuses the strongest chroma in the scale rather than inventing a new one.

@layer base {
  .btn {
    background: var(--primary);
  }
  .btn:hover:not(:disabled) {
    background: var(--primary-hover);
  }
  .btn:active:not(:disabled) {
    background: var(--primary-active);
  }
  .btn:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
  }
}

Disabled is a design decision, not a sign-off

Disabled is the only state with a strict requirement — it must not look interactive — but no WCAG ratio at it. The convention is reduced contrast plus desaturation: pull the control toward a neutral gray with enough lightness separation from the background that it reads as 'not available', while never colliding with a variant that still means something. In practice that means using a low-chroma step of the scale rather than lowering opacity, because 50% opacity over content produces unpredictable composites.

Because ChromaBrew's token set ends at the interactive states, disabled is the one you author: take the button background, desaturate it in OKLCH (chroma toward zero), lighten it a step, and you get a safe default. Opacity-down-to-0.5 is the shortcut that ships the composite-color bug.

Tip

If a disabled element needs to keep readability — like a muted list item with text — test the pair in the contrast checker anyway; some 'grayed out' tokens drop below 3:1 and disappear for low-vision users.

The scale-first rule

Every state should be addressable as a named token (primary-hover, primary-active, focus-ring) that resolves to a step of the primary scale. When a token is a derived step instead of an eyeballed hex, global retheming works, dark mode sits for free, and a one-color seed produces a self-consistent interactive system.

Related free tools

Want to apply this right now? Open the color palette generator and try it with your own brand color — free, no sign-up.