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:).
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 darkOn 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#
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.nsForegroundSecondaryNSColor(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#
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 = trueA 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:
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#
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#
- The library's NSColor+Dynamic.swift, which defines
NSColor(light:dark:), and NSColor+OKLCH.swift, which builds colors in extended sRGB. - Apple, updateLayer(), which AppKit calls when a layer-backed view needs redrawing, including after an appearance change.
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.