-
Notifications
You must be signed in to change notification settings - Fork 991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API Proposal: Calculate Contrast Color for Improved Accessibility #12588
Comments
@JeremyKuhne this one is just something I feel like a lot of devs might need at some point and a built in function would be immensely helpful. |
I did a quick check with chatGPT, it suggests the following changes: /// <summary>
/// Calculates a contrasting color (black or white) based on the luminance of the current color.
/// </summary>
/// <param name="color">The base color.</param>
/// <returns>Black for bright colors, white for dark colors.</returns>
public static Color ContrastColor(this Color color)
{
// If fully transparent, return black immediately
if (color.A == 0)
return Color.Black;
// Use integer math for luma calculation to avoid floating-point overhead
int luma = (299 * color.R + 587 * color.G + 114 * color.B) / 1000;
// Return black for bright colors, white for dark colors
return luma > 128 ? Color.Black : Color.White;
} And then even faster (maybe) public static Color ContrastColor(this Color color)
{
// Approximate luma calculation with bitwise shifts for integer math
int luma = (color.R * 77 + color.G * 150 + color.B * 29) >> 8; // Dividing by 256
// Return black for bright colors, white for dark colors
return luma > 128 ? Color.Black : Color.White;
} even faster (maybe)? public static Color ContrastColor(this Color color)
{
// Get ARGB value once
int argb = color.ToArgb();
// Extract components and calculate luminance
int luma = ((argb >> 16 & 0xFF) * 77 + // Red component
(argb >> 8 & 0xFF) * 150 + // Green component
(argb & 0xFF) * 29) >> 8; // Blue component
// Return black for bright colors, white for dark colors
return luma > 128 ? Color.Black : Color.White;
} |
Benchmark Results:
|
@elachlan it would be good to reference specific requirements. @merriemcgaw or @Tanya-Solyanik might have some relevant specifications to add as well. |
internal void GetRgbValues(out int r, out int g, out int b) is the one that extracts the three colors without multiple lookups. We should probably recommend that we make this public in another API proposal. |
The current WCAG 2.2 requirements can be found here: https://www.w3.org/TR/WCAG22/#contrast-minimum Love this idea! |
The current API proposal returns either black or white depending on the luminence. I don't think its WCAG 2.0 compliant because it doesn't return other colors. An update to the function might be something like this: public static Color WCAGCompliantContrastColor(this Color color)
{
// Calculate luminance
double r = color.R / 255.0;
double g = color.G / 255.0;
double b = color.B / 255.0;
r = (r <= 0.03928) ? r / 12.92 : Math.Pow((r + 0.055) / 1.055, 2.4);
g = (g <= 0.03928) ? g / 12.92 : Math.Pow((g + 0.055) / 1.055, 2.4);
b = (b <= 0.03928) ? b / 12.92 : Math.Pow((b + 0.055) / 1.055, 2.4);
double luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
// Use a threshold to decide contrast color
return luminance > 0.5 ? Color.Black : Color.White;
} But that still returns black or white. Edit:
|
Hello everyone, I’ve been following this issue and wanted to share some experimental results. I tested an approach that uses a small LUT (lookup table) for gamma correction, removing the need for repetitive MathF.Pow calls. This significantly improves performance while still meeting WCAG requirements. For example, here are some benchmark results:
This approach requires only a tiny, static LUT, yielding O(1) performance with no allocations. If it’s of interest, I’m happy to share more details. Thanks for the ongoing work on accessibility! |
I recommend adding an /// <summary>
/// Determines whether the specified foreground color meets or exceeds
/// the given WCAG contrast ratio when drawn over the specified background color.
/// </summary>
/// <param name="background">The background color to test against.</param>
/// <param name="foreground">The foreground color to verify.</param>
/// <param name="requiredRatio">
/// The required WCAG contrast ratio. Defaults to 4.5, which is the standard for normal text.
/// </param>
/// <returns>
/// <c>true</c> if the contrast ratio between <paramref name="background"/> and <paramref name="foreground"/>
/// is greater than or equal to <paramref name="requiredRatio"/>; otherwise <c>false</c>.
/// </returns>
public static bool IsCompliant(Color background, Color foreground, double requiredRatio = RequiredRatio) Usage// Suppose you have a background color:
var background = Color.FromArgb(255, 0, 0); // Bright red
// Get a compliant foreground color:
Color foreground = background.ContrastColor();
// Check compliance explicitly if needed:
bool isAccessible = WcagContrastColor.IsCompliant(background, foreground); |
I've updated the issue description. with I am unsure on the LUT, I feel the overhead of populating it might be excessive. My use case doesn't require calling it within a render loop, I am setting the I could see a timer or async function invoking an update on a controls value, which might change the |
Background and motivation
When designing user interfaces, it is essential to ensure text or elements displayed over a background color have sufficient contrast for readability. This is particularly relevant for accessibility compliance (e.g., WCAG). Calculating a contrasting color, such as black or white, based on the background color's luminance, is a common requirement.
Currently, .NET's
Color
struct does not provide an in-built way to compute a contrast color. This proposal adds aContrastColor
method directly to the Color struct, allowing developers to easily determine the optimal contrasting color (black or white) for any given color.I currently use a version of this in my applications to help get contrasting text color for different labels/controls where a user can configure its background color (such as the status bar).
Reference: https://stackoverflow.com/questions/1855884/determine-font-color-based-on-background-color
API Proposal
API Usage
Alternative Designs
Standalone Helper Function
Instead of an extension method, a static utility function could be added in a helper class. However, attaching the method directly to Color improves discoverability and API integration.
Configurable Threshold
An additional overload could allow developers to specify a custom luminance threshold, but this would add complexity without significant value for most use cases.
Risks
Perceived Simplicity
While the luminance formula used is standard, it assumes consistent behavior across platforms. Deviations in rendering systems or gamma settings might lead to slight visual discrepancies.
Edge Cases
Fully transparent colors (Color.A = 0) are handled by returning Color.Black as a fallback, which may not align with all design requirements.
Will this feature affect UI controls?
N/A
The text was updated successfully, but these errors were encountered: