05

Same Hex, Different Ground

The surround changes what you see — pick and judge color in context.

The same hex is not the same color — the ground you put it on changes what you see, so pick and approve tokens in context, never on an empty artboard.

Pick a mid blue-gray chip. Put it on white, frost, warm sand, orange, teal, near-black, or a soft accent wash. The eyedropper still reports the same hex. Your eye does not. That is simultaneous contrast: the surround rewrites the chip.

Change the ground. Watch Actual stay fixed while Perceived shifts. The pair plate below makes the shift undeniable without tapping.

On light grounds the chip looks darker and cooler. On dark grounds it lifts and softens. On a warm ground it shifts cooler still — the eye pushes away from the surround. Perceived is pedagogical, not a colorimeter. What matters is that the same token is not a stable experience.

Scarce action color (#c0300f) is especially fragile. On a saturated hero it can disappear; on frost it can scream. Toggle the chip to Action in the demo and test both before you lock it.

Complements

Opposite hues on the wheel create tension. Use that tension for accents, not for equal area. A 50/50 primary–complement split is two loud colors with no job. A soft ground tinted from the primary plus one complementary chip as the CTA is product-useful.

The accent #c0300f stays scarce — one action job, never wallpaper. Optional triad (+120° / +240°) gives you two accent candidates; still pick one role, not a rainbow.

Pick in context

Never pick a brand blue on a blank Figma artboard and ship it on charcoal chrome. Put the live chip on every surface it actually sits on: page, card, frost panel, photo, saturated hero. Read the actual hex. Watch the relative-luminance contrast warning when the chip nearly vanishes.

If one hex fails on a ground, split the token by context (fg-on-light / fg-on-dark) — or change the ground — instead of hoping the number is "correct."

Basics to hacks

Checklist when choosing color for product UI:

  1. Role first — What job does this color own? Action, status, surface, text. Closed roles beat vibe swatches.
  2. Judge every ground — Approve the chip on white, frost, sand, dark, and any branded wash it ships on.
  3. OKLCH / HSL steps — Tune lightness and chroma with predictable steps; prefer OKLCH when equality across hues matters.
  4. Complements as accents — Opposite hue for tension on a CTA or badge — not 50/50 paint.
  5. Scarce action — The accent #c0300f appears where the user should act. Everywhere else, neutrals work.
  6. Neutrals work — Most of the UI should be #111 / white / frost. Color earns its keep.
  7. Contrast + simultaneous contrast — Pass relative luminance and trust your eye: the surround still shifts perception.
  8. Split tokens by context — If one hex fails on a ground, define roles per surface instead of one absolute value.

No purple sludge. No open "just one more blue." The ground is part of the color.

Usage

<!-- Same chip, two grounds — classic simultaneous contrast plate -->
<div class="grid grid-cols-2 overflow-hidden rounded-xl">
    <div class="grid place-items-center bg-[#F4F5F7] p-10">
        <span class="size-14 rounded-xl bg-[#6B7C93]"></span>
    </div>
    <div class="grid place-items-center bg-[#141414] p-10">
        <span class="size-14 rounded-xl bg-[#6B7C93]"></span>
    </div>
</div>

<!-- Scarce complement accent on a tinted ground — not 50/50 -->
<div class="flex items-end justify-end rounded-xl bg-[#E8F0FF] p-4">
    <button class="rounded-lg bg-[#F59E0B] px-3 py-1.5 text-xs font-medium text-white">
        Continue
    </button>
</div>
:root {
    --chip: #6B7C93;
    --action: #c0300f; /* scarce CTA only */
    --ground-frost: #F4F5F7;
    --ground-dark: #141414;
}

.pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
}

.ground-light { background: var(--ground-frost); }
.ground-dark { background: var(--ground-dark); }

.chip {
    width: 56px;
    height: 56px;
    border-radius: 12px;
    background: var(--chip);
}

/* Split by context when one hex fails */
.fg-on-light { color: #0A4FD0; }
.fg-on-dark { color: #3D7EFF; }

When a token must work on multiple grounds, define roles (fg on light, fg on dark) instead of one absolute hex. The ground is part of the color.