Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
Function

.toLCH()

Read a color as CIELab lightness, chroma and hue, to match LCH numbers or check contrast.

Updated View as Markdown

Match LCH numbers from a design tool, or read the lightness that decides contrast, with toLCH(). It takes nothing and returns an LCHColor: CIELab lightness from 0 to 100, chroma, and hue in degrees:

swift
Color(hex: "#00B386").toLCH()   // LCHColor(l: 64.93, c: 50.9, h: 166)

Making an LCHColor#

swift
LCHColor(l: 65, c: 51, h: 166)          // #00B386
LCHColor(hex: "#00B386")
LCHColor(lchString: "lch(65% 51 166)")

Write lchString with a percent sign on the lightness. A string it can't read gives a default color, #DF97AC, instead of an error, and lightness and chroma are kept within 0 to 100 and 0 to 128.

Lightness that decides contrast#

l is CIELab L*, which follows luminance, the thing WCAG contrast measures. The palette gives every hue the same L* at each stop, so a stop has the same contrast in every theme:

swift
brand._600.toLCH().l            // about 48.8
Color.proBlue._600.toLCH().l    // about 48.8, the same

How the ramps are built lists the L* of every stop.

Numbers from other tools#

LCHColor uses the D65 white point. CSS Color 4's lch() uses D50, so LCH numbers from a tool built on it give a slightly different color here. For numbers that match exactly, use OKLCH: OKLCHColor matches oklch() to within rounding.

How it behaves#

Conversions pass through extended sRGB without clipping, so a Display P3 color comes through unchanged, with more chroma than sRGB can hold. Hues are stored wrapped to 0 to 360 and rounded to 0.01°. A token gives its light-mode value on iOS 17, macOS 14 and later, and toColor() and getColor(l:c:h:alpha:) give an opaque color.

_50 to _1000, allStops and getColor(at:) read the ramp at the color's OKLCH hue, and LCHColor.getPrimaryColor(forHue:isGrayscale:) gives a hue's _450. From an LCHColor you can go to toRGB(), toXYZ(), toLAB() and toColor().

API#

swift
public extension Color {
    func toLCH() -> LCHColor
}

public struct LCHColor: Hashable, Sendable {
    public let l: CGFloat       // 0 to 100, CIELab L*
    public let c: CGFloat       // 0 to 134 inside sRGB
    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(lchString: String)

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

    public func toRGB() -> RGBColor
    public func toXYZ() -> XYZColor
    public func toLAB() -> LABColor
    public func toColor() -> Color
}

toLCH() takes no parameters. ProTheme has it too, as in brand._600.toLCH(). For a quick line to log, .getLCHString() gives the same numbers rounded down.

Sources#

See also

  • .toLAB() Change a color's lightness alone in CIELab, the lightness that decides contrast.
  • .getLCHString() Log a color's lightness, chroma and hue in one short line, to check a color at a glance.
  • .toOKLCH() Read a color's lightness, chroma and hue the way the palette sees them, to compare or adjust it.
  • How the ramps are built The lightness and chroma behind every stop, so any hue you pick gives the same contrast.