Skip to content

Add WCAG contrast helpers: relativeLuminance() and contrastColor()#8

Merged
TDannhauer merged 2 commits into
horde:FRAMEWORK_6_0from
pierrefardel:feature/wcag-contrast-color
Jul 2, 2026
Merged

Add WCAG contrast helpers: relativeLuminance() and contrastColor()#8
TDannhauer merged 2 commits into
horde:FRAMEWORK_6_0from
pierrefardel:feature/wcag-contrast-color

Conversation

@pierrefardel

Copy link
Copy Markdown
Contributor

Problem

Horde_Image::brightness() uses the legacy YIQ formula. Callers that pick a
foreground color by thresholding it (brightness < 128 ? white : black) get
the lower-contrast option for some saturated mid-luminance backgrounds.

Example: bright magenta #fb00ec → brightness picks white, but white on
that magenta has a WCAG contrast ratio of only 3.33 (below the AA minimum
of 4.5) — hard to read. Black would give 6.31.

Change

Two new static helpers, no change to existing behavior:

  • relativeLuminance($color) — WCAG relative luminance (sRGB linearized),
    per https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
  • contrastColor($bg, $light = '#fff', $dark = '#000') — returns whichever
    candidate has the higher WCAG contrast ratio against $bg.

For #fb00ec, contrastColor() now correctly returns black.

Kronolith will use this to replace its brightness()-based foreground
selection for calendar colors (separate PR).

brightness() uses the legacy YIQ formula, which is a poor proxy for
perceived contrast. Thresholding it (brightness < 128 ? white : black)
picks the lower-contrast foreground for some saturated mid-luminance
colors — e.g. white text on bright magenta (#fb00ec) yields a WCAG
ratio of only 3.33, below the AA minimum of 4.5.

Add:
- relativeLuminance(): WCAG relative luminance (sRGB linearized).
- contrastColor($bg, $light, $dark): returns whichever of the two
  candidates has the higher WCAG contrast ratio against $bg.

For #fb00ec this now correctly picks black (ratio 6.31) over white.
Copilot AI review requested due to automatic review settings July 2, 2026 07:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds WCAG-based color contrast utilities to the legacy Horde_Image helper class so callers can choose the higher-contrast foreground color for a given background without relying on the legacy YIQ brightness heuristic.

Changes:

  • Add Horde_Image::relativeLuminance($color) implementing WCAG 2.1 relative luminance (sRGB linearized).
  • Add Horde_Image::contrastColor($bg, $light, $dark) to select the higher-contrast candidate via the WCAG contrast ratio formula.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

pierrefardel added a commit to pierrefardel/kronolith that referenced this pull request Jul 2, 2026
Per review: guard the three call sites so kronolith keeps working against an
older horde/image that doesn't yet provide contrastColor(). When the method
is present, use the WCAG ratio; otherwise fall back to the legacy brightness
threshold. Makes this change safe to merge independently of horde/Image#8.
Cover relativeLuminance() and contrastColor() on Horde_Image, including
the bright-magenta regression case and shorthand hex handling.
@TDannhauer TDannhauer merged commit 338e790 into horde:FRAMEWORK_6_0 Jul 2, 2026
TDannhauer pushed a commit to horde/kronolith that referenced this pull request Jul 2, 2026
Calendar text/icon foreground was chosen by thresholding
Horde_Image::brightness() (legacy YIQ), which picks the lower-contrast
option for some saturated colors — e.g. white on bright magenta (#fb00ec)
has a WCAG ratio of 3.33, below the AA minimum of 4.5.

Switch the three PHP call sites (Kronolith::foregroundColor,
Kronolith_Calendar::foreground, Kronolith_Event icon color) to
Horde_Image::contrastColor(), which compares the actual WCAG contrast ratio
of both candidates. Add a matching JS helper KronolithCore.contrastColor()
for the color preview in the calendar dialog.

Requires horde/Image#8 (adds contrastColor()/relativeLuminance()).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants