Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
How-to

Using ColorTokensKit in AppKit

Give AppKit views the palette's tokens as NSColors that follow the Mac's light and dark appearance.

Updated View as Markdown

Your Mac app's AppKit views get the palette and tokens too, and they follow the Mac's light and dark appearance. Convert a token with NSColor(_:), or build one from its two stops with the package's NSColor(light:dark:).

swift
import AppKit
import SwiftUI

let dates = NSTextField(labelWithString: "3 nights from October 12")
dates.font = .preferredFont(forTextStyle: .subheadline)
dates.textColor = NSColor(Color.proOrange.foregroundSecondary)   // _800 in light mode, _200 in dark
light mode
3 nights from October 12

On macOS 26.6, NSColor(_:) kept both stops of a token, and a color function's result for each appearance; we haven't tested earlier versions. It also keeps a Display P3 stop's full color, such as orange _200 here.

Build a token from its stops#

swift
import AppKit
import SwiftUI
import ColorTokensKit

extension ProTheme {
    /// foregroundSecondary for AppKit: _800 in light mode, _200 in dark mode.
    var nsForegroundSecondary: NSColor {
        NSColor(light: NSColor(_800.toColor()), dark: NSColor(_200.toColor()))
    }
}

let dates = NSTextField(labelWithString: "3 nights from October 12")
dates.textColor = Color.proOrange.nsForegroundSecondary
light mode
3 nights from October 12

NSColor(light:dark:) picks the dark stop whenever the appearance is closest to Dark Aqua, so it doesn't depend on how a macOS version bridges SwiftUI colors. Understanding semantic tokens lists every token's stops.

Update layer colors when the appearance changes#

swift
import AppKit

final class TripCardView: NSView {
    // outlineSecondary: gray _200 in light mode, _800 in dark mode
    private let outline = NSColor(
        light: NSColor(Color.proGray._200.toColor()),
        dark: NSColor(Color.proGray._800.toColor())
    )

    override var wantsUpdateLayer: Bool { true }

    override func updateLayer() {
        layer?.borderWidth = 1
        layer?.borderColor = outline.cgColor
    }
}

let tripCard = TripCardView(frame: NSRect(x: 0, y: 0, width: 120, height: 48))
tripCard.wantsLayer = true
light mode

A CGColor holds one value, so set layer colors in updateLayer(), which AppKit calls again when the appearance changes.

If something goes wrong#

An AppKit color looks less vivid than in SwiftUI#

The color was probably built with NSColor(srgbRed:green:blue:alpha:). That tags it plain sRGB, and AppKit clips it to sRGB when it converts or draws it, so a Display P3 stop loses the chroma beyond sRGB. Build it in the .extendedSRGB color space instead:

swift
import AppKit

let channels = Color.proOrange._200.toRGB()   // extended sRGB: red is past 1
let tagFill = NSColor(colorSpace: .extendedSRGB, components: [channels.r, channels.g, channels.b, channels.alpha], count: 4)

API#

swift
extension NSColor {
    convenience init(
        light lightModeColor: @escaping @autoclosure () -> NSColor,
        dark darkModeColor: @escaping @autoclosure () -> NSColor
    )
}

A dynamic NSColor that gives dark when the drawing appearance is closest to Dark Aqua, and light otherwise.

Sources#

See also

  • Managing dark mode Get dark mode without extra code: tokens switch stops for you, and your own colors can too.
  • Using ColorTokensKit in UIKit Give UIKit views the palette's tokens as UIColors that follow dark mode on every iOS version tested.
  • .toRGB() Read a color's red, green and blue channels, even past sRGB, to hand to other graphics code.
  • Fitting colors into Display P3 Stay inside Display P3 without losing contrast: chroma drops, lightness holds.