GenomeJS
Utilities

lockContrast()

Adjust a foreground hexadecimal color toward a requested contrast ratio.

lockContrast() adjusts a foreground color until it reaches, or gets as close as its current algorithm can get to, a requested contrast ratio against a background.

It is useful for derived foreground tokens whose source color may change at runtime.

Import

import { lockContrast } from "@genomejs/core";

Signature

lockContrast(
  foreground: string,
  background: string,
  minRatio?: number,
): string

Parameters

ParameterTypeDefaultDescription
foregroundstringForeground hexadecimal color to test and possibly adjust
backgroundstringBackground hexadecimal color
minRationumber4.5Requested minimum contrast ratio

Return value

string;

The function returns either:

  • The original foreground when it already satisfies the target
  • An adjusted six-digit hexadecimal foreground

Already-compliant colors

import { lockContrast } from "@genomejs/core";

const foreground = lockContrast("#000000", "#ffffff");

console.log(foreground);
// "#000000"

An already-compliant foreground is returned unchanged.

Adjust a low-contrast color

const foreground = lockContrast("#777777", "#888888", 4.5);

console.log(foreground);
// Adjusted hexadecimal color

Verify the result:

import { contrastRatio, lockContrast } from "@genomejs/core";

const adjusted = lockContrast("#777777", "#888888", 4.5);

const ratio = contrastRatio(adjusted, "#888888");

console.log(ratio);

The Core test suite verifies that this example reaches at least 4.5.

Use in a Genome token

import { Genome, lockContrast } from "@genomejs/core";

const genome = new Genome({
  primitives: {
    lightSurface: "#ffffff",
    darkSurface: "#121620",
    preferredForeground: "#7c6cff",
  },

  tokens: {
    surface: (dna, context) =>
      context.mode === "dark" ? dna.darkSurface : dna.lightSurface,

    foreground: (dna, context) =>
      lockContrast(
        String(dna.preferredForeground),
        String(dna.surface),
        context.contrast === "high" ? 7 : 4.5,
      ),
  },
});

genome.mutate({
  mode: "dark",
  contrast: "standard",
});

GenomeJS discovers that foreground depends on:

preferredForeground
surface

When surface changes after a context mutation, the foreground is calculated again.

Standard and high-contrast modes

const tokens = {
  foreground: (dna, context) => {
    const target = context.contrast === "high" ? 7 : 4.5;

    return lockContrast(
      String(dna.preferredForeground),
      String(dna.surface),
      target,
    );
  },
};
genome.mutate({
  contrast: "high",
});

The meaning of each target remains an application decision. The utility only attempts to meet the numeric ratio supplied to it.

How adjustment works

The current implementation:

  1. Checks the original contrast ratio
  2. Compares black and white against the background
  3. Chooses the more promising direction
  4. Shifts the foreground toward black or white
  5. Rechecks contrast
  6. Repeats for at most 20 iterations

Each iteration shifts RGB channels by approximately ten percent of the full 0–255 channel range.

The function does not search every possible color or preserve perceptual hue relationships precisely.

The result may change hue or saturation

The current adjustment works by adding or subtracting the same amount from each RGB channel and clamping each channel between 0 and 255.

This can change:

  • Lightness
  • Saturation
  • Perceived hue near channel limits

Treat the output as a practical contrast adjustment, not a full perceptual color-space optimizer.

Validate the returned ratio

For ordinary thresholds and valid input, the algorithm generally moves toward the highest-contrast black or white direction.

When the target is unusually high or impossible, validate the returned value:

const adjusted = lockContrast(foreground, background, target);

const achieved = contrastRatio(adjusted, background);

if (achieved < target) {
  console.warn("Requested contrast was not reached.");
}

The function does not throw when the requested target remains unmet after its iteration limit.

Input format

Use standard six-digit RGB hexadecimal colors:

lockContrast("#7c6cff", "#121620");

The current implementation does not explicitly support:

  • Three-digit shorthand
  • Eight-digit alpha hex
  • rgb() or hsl()
  • CSS named colors
  • Transparency composition

User-provided colors

Validate external values before use:

function assertHex(value: string): void {
  if (!/^#[0-9a-f]{6}$/i.test(value)) {
    throw new Error(`Invalid color: ${value}`);
  }
}
assertHex(foreground);
assertHex(background);

const adjusted = lockContrast(foreground, background);

Use the result as CSS

const foreground = lockContrast("#7c6cff", "#ffffff", 4.5);

document.documentElement.style.setProperty(
  "--accessible-foreground",
  foreground,
);

Inside GenomeJS, the resolved token is expressed automatically:

--g-foreground: #...;

Error behavior

lockContrast() does not currently throw a dedicated error when:

  • A color format is invalid
  • The requested ratio is impossible
  • The iteration limit is reached
  • minRatio is negative or otherwise nonsensical

Validate externally supplied colors and ratio values where necessary.

Notes

  • The default requested ratio is 4.5.
  • Compliant foregrounds are returned unchanged.
  • The foreground is the only color adjusted.
  • The adjustment moves toward black or white.
  • The algorithm performs at most 20 shifts.
  • It does not use a perceptual color space.
  • It can be used without constructing a Genome.
  • Verify the result when targets are unusually strict.

On this page