Fix color-contrast()
function for WCAG 2.1 compliance
#41585
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Closes #41543
Supersedes #41551
Description
The
color-contrast
function inscss/_functions.scss
currently uses a strict greater than comparison (>
) when checking if a contrast ratio meets the minimum requirement:This excludes colors that produce exactly the minimum contrast ratio (4.5:1), which violates the WCAG 2.1 Success Criterion 1.4.3 Contrast (Minimum). The standard requires "at least 4.5:1" (>= 4.5), not "strictly greater than 4.5:1" (> 4.5).
To comply with the standard, the comparison operator should be changed from
>
to>=
:This would ensure that colors producing exactly a 4.5:1 contrast ratio are accepted, correctly implementing the WCAG requirement.
Investigation results
After extensive testing, it appears that no specific colors in Bootstrap's current implementation produce exactly a 4.5:1 contrast ratio. This is due to Bootstrap’s use of a precomputed luminance list (
$_luminance-list
) that maps integer RGB values (0–255) to discrete luminance values.I tested various colors near the 4.5:1 threshold against
#fff
and found:#777777
(rgb(119, 119, 119)) → 4.4776:1#767676
(rgb(118, 118, 118)) → 4.5415:1So, there's no integer RGB color that yields exactly 4.5:1 in Bootstrap's current luminance calculation.
Testing colors like
#777777
,#767676
,#787878
,#757575
gives the same results with or without the fix, matching results from tooks like WebAIM: Contrast Checker.Testing approach
I created a Sass test suite (
scss/tests/mixins/_color-contrast.test.scss
) that tests the boundary condition using a custom minimum contrast ratio that matches actual color contrast values. This clearly demonstrates the behavioral difference between>
and>=
and validates edge cases around the 4.5:1 threshold.The test suite documents specific contrast ratios for real-world color examples and ensures regression prevention for future updates. I included several test cases from my own manuel tests covering various scenarios including colors below, at, and above the 4.5:1 threshold, edge cases with very light and very dark backgrounds, custom minimum contrast ratios, RGBA color handling, and luminance and contrast ratio calculation accuracy.
Reflection on practical impact
It's worth reflecting on whether this change is actually useful given the investigation results. The reality is that no real-world colors in Bootstrap's current implementation are affected by this fix - the luminance calculation uses discrete integer RGB values, so no color produces exactly 4.5:1 contrast ratio.
I don't have a strong opinion regarding the merge of this PR.
I'd say this change could still be reasonable to respect the standards compliance (the "at least 4.5:1") and for future-proofing if Bootstrap ever changes its luminance calculation, or if PRs with new Sass implementations such as https://github.com/twbs/bootstrap/pull/41512/files#diff-de6111a37eb67d027961043ce738e80a3ac4ad61a6e7704d35cb3168369cba01 produce slightly different colors.
It remains a kind of theoretical fix, more of a preventive fix that aligns Bootstrap's implementation with accessibility standards and provides peace of mind that the code correctly handles edge cases, even if they don't currently exist in practice.
Warning
While I'm not an accessibility expert, I've done my best to approach this carefully and welcome feedback from others more experienced in this area. Pinging @patrickhlauke, @ffoodd, @mdo for double-check, and most of all, opinions :)