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:
Color(hex: "#00B386").toLCH() // LCHColor(l: 64.93, c: 50.9, h: 166)Making an LCHColor#
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:
brand._600.toLCH().l // about 48.8
Color.proBlue._600.toLCH().l // about 48.8, the sameHow 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#
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#
- Color+ColorSpaces.swift, which defines
toLCH()onColor. - The LCH folder, which defines
LCHColor, its string parser and its stops. - W3C, CSS Color Module Level 4, which defines
lch()with the D50 white point.
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.