Skip to content

Translucent colours turn opaque in dark mode and are measured as opaque #696

Description

@rubenvdlinde

A token set colour with transparency loses it in the generated dark palette. The contrast audit also treats it as opaque, so faint text can pass a check it would fail as rendered.

Where, at development fd992ea:

  • lib/Service/ContrastService.php:245 matches only #rgb and #rrggbb, so #rgba and #rrggbbaa are "not a colour".
  • lib/Service/ContrastService.php:258-263 matches the alpha of rgba() and throws it away.
  • lib/Service/ContrastService.php:91-92 the contrast audit measures pairs through that parser.
  • lib/Service/DarkPaletteService.php:314-321 derives each dark token from the same parser, skipping what it cannot read.
  • lib/Service/DarkPaletteService.php:381-408 deriveColorToken() writes the dark value as an opaque hex.
    public function parseColor(string $value): ?array {
    $value = trim($value);
    if (preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $value, $match) === 1) {
    $hex = $match[1];
    if (strlen($hex) === 3) {
    $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
    }
    return [
    hexdec(substr($hex, 0, 2)),
    hexdec(substr($hex, 2, 2)),
    hexdec(substr($hex, 4, 2)),
    ];
    }
    if (preg_match('/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*[\d.]+\s*)?\)$/', $value, $match) === 1) {
    $red = min(255, (int)$match[1]);
    $green = min(255, (int)$match[2]);
    $blue = min(255, (int)$match[3]);
    return [$red, $green, $blue];
    }
    return null;
    }//end parseColor()

    $rgb = $this->contrast->parseColor(value: $literal);
    if ($rgb === null) {
    // Unparseable (gradient, keyword, size, font stack, url(), an
    // alias chain with no literal at the end) — skip.
    continue;
    }
    $dark[$token] = $this->deriveColorToken(token: $token, rgb: $rgb, lightDeclarations: $lightDeclarations);

So an overlay like rgba(0, 0, 0, 0.5) becomes solid in dark mode and hides what it covers. An 8-digit hex token is never darkened at all. The audit compares the opaque colour, not the blend a user sees.

Specified fix: openspec/changes/authoring-token-value-types (requirements "Translucent colours are measured as they render" and "Derived dark values keep the light value's alpha", tasks 3.1 and 3.2).

Found by the OpenSpec pass on 27 Sep 2026 and re-read at fd992ea on 28 Sep.

Live check: add --nldesign-color-overlay: rgba(0, 0, 0, 0.5) to a token set, run occ nldesign:generate-dark-variants --force, and read an opaque hex in its dark file.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingtriageAwaiting triage

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions