Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
Function

.toOKLCH()

Read a color's lightness, chroma and hue the way the palette sees them, to compare or adjust it.

Updated View as Markdown

See a color the way the palette does with toOKLCH(): lightness, chroma and hue in OKLCH, the space every ramp is built in. It takes nothing and returns an OKLCHColor you can read, change and turn back into a Color:

swift
let brandColor = Color(hex: "#00B386")
let oklch = brandColor.toOKLCH()   // OKLCHColor(l: 0.680, c: 0.139, h: 166.99)
oklch.h                            // 166.99, the hue ProTheme(hex:) builds the brand's theme on

Type any hex value to get it in every color type, with the Swift that makes it and the nearest palette stop:

Try it / Convert a color

L* 52.3 · luminance 0.204 · nearest stop proBlue._550 (ΔEOK 0.019)

Hex#3c80c4Color(hex: "#3C80C4")
RGB0.235 0.502 0.769RGBColor(r: 0.235, g: 0.502, b: 0.769, alpha: 1)
HSL210° 53.5% 50.2%Color(h: 210, s: 0.535, l: 0.502)
OKLCH0.5879 0.1253 250.8OKLCHColor(l: 0.5879, c: 0.1253, h: 250.8)
OKLab0.5879 -0.0412 -0.1183OKLabColor(l: 0.5879, a: -0.0412, b: -0.1183)
LCH52.27 41.7 271.18LCHColor(l: 52.27, c: 41.7, h: 271.18)
LAB52.27 0.86 -41.69LABColor(l: 52.27, a: 0.86, b: -41.69, alpha: 1)
XYZ0.1954 0.2038 0.5512XYZColor(x: 0.1954, y: 0.2038, z: 0.5512, alpha: 1)

Choosing a color space#

  • OKLCH for how colors look: hue stays put as chroma changes, so the palette and the color functions use it.
  • CIELab L* for contrast: it depends only on luminance, as WCAG contrast does, so the palette holds it equal at each stop. .toLCH() and .toLAB() give it.
  • RGB for handing colors to other code, with .toRGB(), and hex for colors inside sRGB, with .getHexString().

Why OKLCH? shows the difference.

Making an OKLCHColor#

swift
OKLCHColor(l: 0.68, c: 0.14, h: 167)              // #00B386
OKLCHColor(hex: "#00B386")
OKLCHColor(oklchString: "oklch(0.68 0.14 167)")

Write oklchString with three plain numbers: no percent signs, no alpha, and 0.68 rather than .68. A string it can't read gives a default color, #E7729B, instead of an error. OKLCHColor matches CSS oklch() numbers to within rounding.

Changing one value#

getColor(l:c:h:alpha:) replaces the values you pass and returns a Color:

swift
let oklch = Color(hex: "#00B386").toOKLCH()
let quieter = oklch.getColor(c: 0.05)   // same lightness and hue, closer to gray

It doesn't fit the result into Display P3 as the color functions do, so for more chroma, use saturate(by:).

The ramp at this hue#

_50 to _1000, allStops and getColor(at:) read the ramp of the color's own hue, and the static getPrimaryColor(forHue:isGrayscale:) gives a hue's _450. They return OKLCHColor values; for a theme with every token, use ProTheme(hex:) or ProTheme.primary(forHue:).

How it behaves#

Conversions between the CIELab and OK types pass through extended sRGB without clipping, so a Display P3 color comes through unchanged. A token has two values, and toOKLCH() reads one: on iOS 17, macOS 14 and later, the light-mode value. To read dark mode, convert the stop the token uses there.

Hues are stored wrapped to 0 to 360 and rounded to 0.01°, so OKLCHColor(l: 0.5, c: 0.1, h: 370).h is 10. toColor() and getColor(l:c:h:alpha:) always give an opaque color.

From an OKLCHColor you can go to toRGB(), toLCH(), toOKLab() and toColor().

API#

swift
public extension Color {
    func toOKLCH() -> OKLCHColor
}

public struct OKLCHColor: Hashable, Sendable {
    public let l: CGFloat       // 0 to 1
    public let c: CGFloat       // 0 to 0.32 inside sRGB, more in Display P3
    public let h: CGFloat       // 0 to 360
    public let alpha: CGFloat   // 0 to 1

    public init(l: CGFloat = 0, c: CGFloat = 0, h: CGFloat = 0, alpha: CGFloat = 1)
    public init(color: Color)
    public init(hex: String)
    public init(oklchString: String)

    public func getColor(
        l: CGFloat? = nil, c: CGFloat? = nil, h: CGFloat? = nil, alpha: CGFloat? = nil
    ) -> Color
    public func getColor(at index: Int) -> OKLCHColor
    public var allStops: [OKLCHColor] { get }
    public var _50: OKLCHColor { get }   // and every stop to _1000
    public static func getPrimaryColor(forHue hue: Double, isGrayscale: Bool = false) -> OKLCHColor
    public func lerp(_ other: OKLCHColor, t: CGFloat) -> OKLCHColor

    public func toRGB() -> RGBColor
    public func toLCH() -> LCHColor
    public func toOKLab() -> OKLabColor
    public func toColor() -> Color
}

toOKLCH() takes no parameters. ProTheme has it too, as in brand._600.toOKLCH().

Sources#

See also

  • Why OKLCH? Every hue looks evenly matched at each stop, so you can swap families without rechecking contrast.
  • .toOKLab() Get a color in OKLab, where straight-line math between colors looks even, for your own blends.
  • Color(hex:) Turn a hex value from your design file into a SwiftUI color, with or without transparency.
  • ProTheme Make a theme from a hex, a hue or OKLCH numbers, and every stop and token keeps its contrast.
  • .normalizedHue and .rounded(to:) Wrap any angle into 0° to 360° and round numbers the way the library stores every hue.