Breaking
highestRatedContrastLevel,aaaContrastLevel,aaContrastLevel,aaLargeContrastLevelnow returnColor?. They used to silently return.whitewhen nothing in the candidate set actually met the requested level. Now they returnnil— check it and pick a fallback deliberately.ContrastLevel.ratio(internal(min, max)tuple) removed. Replaced byContrastLevel.minimumRatio: Double. There's no upper bound to migrate to — see next point.- Contrast levels are floors, not bands.
AAno longer rejects a candidate for being too readable (previously capped at6.99), andAA Largeis no longer capped at4.49. Meeting AAA now also means meeting AA and AA Large, per WCAG.aaContrastLevel/aaLargeContrastLevelmay now return a different (higher-contrast, more correct) result than before. - The lightness ramp (
level(_:for:),level(_:), shorthand properties) now derives from a true RGB→HSL conversion, not HSB read viagetHue(_:saturation:brightness:). Any saturated color's generated shades will differ slightly from previous versions — this was a real bug (HSB brightness ≠ HSL lightness), not a style change. Grayscale colors are unaffected. - Luminance and contrast now convert to sRGB before computing anything. Display P3 asset colors — Xcode's default — previously had their raw components read directly and pushed through the sRGB-only WCAG formula, silently producing wrong numbers that grew with saturation. Every ratio and derived color involving a wide-gamut color will change to the correct value.
Added
Color.relativeLuminance: Double— WCAG 2.1 relative luminance,0...1.Color.contrastRatio(with:) -> Double— the actual ratio between two colors,1...21. This is the one thing the package couldn't do before: answer "what is the contrast ratio," not just "pick me a color."Color.meets(_:against:) -> Bool— direct pass/fail check against aContrastLevel.Color.flattened(over:) -> Color— alpha-composites a translucent color over an opaque backdrop. WCAG contrast is only defined for opaque colors; use this before measuring contrast on anything with alpha < 1.Color.mostReadableForeground(from:meeting:) -> Color?— picks the best-contrasting color from an arbitrary candidate list that still meets a given level, instead of only ever choosing from[.white, .black]+ the built-in ramp.- Trait-aware overloads for all of the above —
compatibleWith: UITraitCollection(UIKit) /compatibleWith: NSAppearance(AppKit) — so contrast can be measured against a specific appearance (light/dark, and Increase Contrast viaaccessibilityContrast) instead of whatever's ambient at the call site. Ambient resolution is unpredictable in tests and wrong outside the screen the call happens to run on. ContrastLevel.minimumRatio: Double.
Fixed
- Alpha was previously ignored entirely when computing luminance — a translucent color returned a confidently wrong number.
relativeLuminance/contrastRatio/flattened(over:)now handle it:asserts (debug-only) if you call luminance/contrast on a non-opaque color without compositing it first. - The WCAG ratio formula was implemented three times across the codebase (production code, a DEBUG-only preview helper, and effectively duplicated in the test suite) — and the tests were exercising the DEBUG-only copy, not the one that ships in Release. There is now exactly one implementation, and it's the one both Release builds and tests use.
- Dead
@available(iOS 14.0, macOS 11.0, …)annotations removed — the package has required iOS 17 / macOS 14 since26.2.20; these were unreachable and misleading.
Improved
ColorLevelStylenow conforms toSendable.- Reindented
ContrastLevel.swiftandPreviewTesting.swiftto match the rest of the codebase (4-space). - Test suite expanded from 3 tests (exercising only internal, DEBUG-only code) to 19 tests covering the full public API, including a P3-vs-sRGB regression test verified against Apple's own
CGColorconversion, and trait/appearance-aware resolution verified against dynamic color fixtures.
See the README migration guide for before/after code and full detail on every change above.