# .contrastRatio(to:method:)

Check WCAG 2 and APCA contrast in Swift, and keep your color pairs passing with a unit test.

Prove your text is readable in code: call `contrastRatio(to:)` on the background and pass the text. You get the [WCAG](https://colortokenskit.com/reference/glossary/index.md#wcag) 2 [contrast ratio](https://colortokenskit.com/reference/glossary/index.md#contrast-ratio) that accessibility audits check, or an [APCA](https://colortokenskit.com/reference/glossary/index.md#apca) score with `method: .apca`.

```swift
let brand = ProTheme(hex: "#00B386")
let background = brand._100
let text = brand._700

background.contrastRatio(to: text)                  // 5.53, passes WCAG AA for body text
background.contrastRatio(to: text, method: .apca)   // 69.6
```

Try your own pair: stops, tokens in either mode, or any hex color.

A contrast checker measures any text and background color with WCAG 2 and APCA. By default it shows `Color.proBlue._700` on `Color.proBlue._100`: 5.53:1, APCA Lc 70.1. [Try it on the live page](https://colortokenskit.com/api/contrast-ratio/).

## Meeting WCAG AA and AAA

Most apps aim for AA: 4.5:1 for body text, and 3:1 for large text, icons that carry meaning and the borders of controls.

AAA asks 7:1 for body text. Large text is at least 18 point, or 14 point bold, and text in a disabled control, such as a Sold out button, has no minimum.

## Checking a token in each mode

On a token, `contrastRatio(to:)` measures one mode: light mode on iOS 17, macOS 14 and later, whatever mode your app is in, and the current appearance before that. To check both modes, compare the token's stops:

```swift
brand.backgroundPrimary.contrastRatio(to: brand.foregroundSecondary)   // 8.72, light mode only

// The same pair from its stops: _800 on _50 in light mode, _200 on _1000 in dark
brand._50.contrastRatio(to: brand._800)     // 8.72
brand._1000.contrastRatio(to: brand._200)   // 11.48
```

[Understanding semantic tokens](https://colortokenskit.com/getting-started/semantic-tokens/index.md) lists the stops behind every token.

## Testing your pairs

A unit test keeps a theme change from lowering contrast unnoticed. This one checks secondary text on the page background in both modes, for each of the app's themes:

```swift
import ColorTokensKit
import SwiftUI
import Testing

struct ContrastTests {
    // The brand, then Lisbon, Kyoto and Reykjavík
    @Test(arguments: [ProTheme(hex: "#00B386"), Color.proOrange, Color.proPink, Color.proSky])
    func secondaryTextPassesAA(theme: ProTheme) {
        // foregroundSecondary on backgroundPrimary: _800 on _50 in light mode, _200 on _1000 in dark mode
        #expect(theme._50.contrastRatio(to: theme._800) >= 4.5)
        #expect(theme._1000.contrastRatio(to: theme._200) >= 4.5)
    }
}
```

All four pass, at 8.72:1 or more. If a pair of yours fails, move the text a stop further from its background.

## Choosing between WCAG 2 and APCA

Your colors have to pass WCAG 2, which audits check and accessibility laws refer to. But Color.js notes that it gives false positives and false negatives, particularly in dark mode. APCA, a candidate for WCAG 3, models how people read on screens, and here the two disagree:

```swift
// foregroundTertiary on backgroundPrimary
brand._50.contrastRatio(to: brand._700)                    // 6.10, light mode
brand._1000.contrastRatio(to: brand._300)                  // 9.00, dark mode
brand._50.contrastRatio(to: brand._700, method: .apca)     // 76.0
brand._1000.contrastRatio(to: brand._300, method: .apca)   // -65.0
```

WCAG 2 rates the dark pair as more readable. APCA rates it lower, under the Lc 75 its Bronze level asks for body text. We recommend passing WCAG 2 and using APCA as a second opinion, especially in dark mode.

## How it behaves

For WCAG 2 the order doesn't matter: you get the same ratio, from 1 to 21, either way round. For APCA it does: call the function on the background and pass the text. An Lc is positive for dark text on a light background, negative for light on dark, and 0 for a pair too close to read. The doc comment on `.apca` calls Lc 60 typical for body text, but APCA asks 75 there, and 60 for other text.

It measures a color beyond sRGB as it is, without clipping it into sRGB first, so a Display P3 stop gets its true contrast.

It ignores opacity, so measure a translucent token such as `surfacePrimary` over its background, as people see it.

`relativeLuminance` on `RGBColor` gives the number WCAG 2 builds its ratio from, from 0 for black to 1 for white: `color.toRGB().relativeLuminance`.

## API

`contrastRatio(to:method:)` is on `Color`, `ProTheme`, `RGBColor`, `OKLCHColor` and `LCHColor`, each taking a color of its own type.

```swift
public enum ContrastMethod {
    case wcag2   // WCAG 2.x ratio, 1 to 21
    case apca    // APCA Lc, signed
}

public extension Color {
    func contrastRatio(to other: Color, method: ContrastMethod = .wcag2) -> CGFloat
}

public extension ProTheme {
    func contrastRatio(to other: ProTheme, method: ContrastMethod = .wcag2) -> CGFloat
}

// RGBColor, OKLCHColor and LCHColor have the same function, taking their own type.

public extension RGBColor {
    var relativeLuminance: CGFloat { get }   // WCAG 2.x, 0 to 1
}
```

| Parameter | Type             | Default  | What other values do                                                                 |
| --------- | ---------------- | -------- | ------------------------------------------------------------------------------------ |
| `other`   | the same type    | none     | The text color. For WCAG 2 either color can come first.                              |
| `method`  | `ContrastMethod` | `.wcag2` | `.apca` returns an APCA Lc, signed by polarity, with the receiver as the background. |

It returns a `CGFloat`: a ratio from 1 to 21 for `.wcag2`, or an Lc for `.apca`.

## Sources

- W3C, [WCAG 2.2](https://www.w3.org/TR/WCAG22/): success criteria 1.4.3, 1.4.6 and 1.4.11, and the definitions of relative luminance and large text.
- Color.js, [Contrast](https://colorjs.io/docs/contrast), on the known problems with WCAG 2 contrast and the status of APCA.
- APCA, [Bronze simple mode](https://readtech.org/ARC/tests/bronze-simple-mode/), for the Lc levels.
- ColorTokensKit source, [ContrastRatio.swift](https://github.com/metasidd/ColorTokensKit-Swift/blob/main/Sources/ColorTokensKit/Accessibility/ContrastRatio.swift).

## See also

- [How accessible is it?](https://colortokenskit.com/basics/how-accessible/index.md): What the palette guarantees for contrast in every hue and mode, and what you still need to check.
- [High contrast modes](https://colortokenskit.com/advanced/high-contrast/index.md): Respect Increase Contrast with one rule for every family: text and borders two stops stronger.
- [Color theory](https://colortokenskit.com/advanced/color-theory/index.md): Pair stops that always read, and pick secondary hues that match your brand, in both modes.
- [.toRGB()](https://colortokenskit.com/api/to-rgb/index.md): Read a color's red, green and blue channels, even past sRGB, to hand to other graphics code.

---

From ColorTokensKit, by Penguin Design Ventures: https://colortokenskit.com/api/contrast-ratio/ (updated September 26, 2026).
