Skip to content
ColorTokensKitby Penguin Design Ventures
Contents
Function

.harmony(_:)

Build the harmony someone picks at runtime, from a picker or a setting, in one call.

Updated View as Markdown

Build a harmony chosen at runtime, such as from a picker in your theme settings, with .harmony(_:). You pass a ColorHarmony and get its colors back in a fixed order, your own color included.

swift
struct HarmonySwatches: View {
    let harmony: ColorHarmony

    var body: some View {
        let families = brand.harmony(harmony)
        HStack(spacing: 4) {
            ForEach(families.indices, id: \.self) { index in
                Rectangle()
                    .fill(families[index].backgroundTertiary)   // _200 in light mode, _700 in dark
                    .frame(width: 32, height: 32)
            }
        }
    }
}

VStack(alignment: .leading) {
    HarmonySwatches(harmony: .triad)                                   // 167°, 287° and 47°
    HarmonySwatches(harmony: .splitComplement(spread: .degrees(20)))   // 167°, 327° and 7°
}
light mode

Pick a harmony and a color below; the Swift under the result updates as you go.

Try it / Harmonies
Color

Every OKLCH hue at this color's lightness

0°90°180°270°360°
light mode
#63a7fdproBlue._400
#ed8295proRuby._400
#8ab24dproLime._400
dark mode
#63a7fdproBlue._400
#ed8295proRuby._400
#8ab24dproLime._400
swift
let color = Color.proBlue._400.toColor()
color.triad

Letting people choose#

ColorHarmony is Hashable, so it can be a picker's selection. Include your own spreads and offsets as separate options:

swift
struct HarmonyPicker: View {
    @State private var harmony: ColorHarmony = .triad

    var body: some View {
        Picker("Harmony", selection: $harmony) {
            Text("Triad").tag(ColorHarmony.triad)
            Text("Square").tag(ColorHarmony.square)
            Text("Split complement").tag(ColorHarmony.splitComplement())
        }
    }
}

The harmonies you can pass#

Each case lists its hues as offsets around the wheel, in degrees from yours, in the order the colors come back:

CasehueOffsetsThe same as
.complement0, 180complement, with your color first
.triad0, 120, 240triad
.square0, 90, 180, 270square
.tetrad(offset:)0, offset, 180, 180 + offsettetrad(offset:)
.splitComplement(spread:)0, 180 − spread, 180 + spreadsplitComplement(spread:)
.analogous(count:spread:)count offsets spread apart, centered on 0analogous(count:spread:)

Read the offsets in code when you need them, such as to label a hue wheel:

swift
let offsets = ColorHarmony.splitComplement(spread: .degrees(20)).hueOffsets   // [0, 160, 200]

How it behaves#

An offset of 0 returns your color exactly as it was. Every other offset turns it with rotateHue(by:), so a palette color lands on the same stop at its new hue, with the same contrast. On a ProTheme, you get whole families at the stop you started from. Gray has no hue, so every harmony of a gray is gray.

Color and ProTheme read the same ColorHarmony, so they always agree: the complement of brand at _600 is the same color whichever route you take. The one difference from the named properties is .complement: the property returns only the turned color, while harmony(.complement) returns both. Color theory covers which harmony suits which job.

API#

swift
public extension Color {
    func harmony(_ harmony: ColorHarmony) -> [Color]
}

public extension ProTheme {
    func harmony(_ harmony: ColorHarmony) -> [ProTheme]
}

public enum ColorHarmony: Hashable {
    case complement
    case triad
    case tetrad(offset: Angle = .degrees(60))
    case square
    case splitComplement(spread: Angle = .degrees(30))
    case analogous(count: Int = 3, spread: Angle = .degrees(30))

    public var hueOffsets: [Double] { get }
}
ParameterTypeDefaultWhat other values do
harmonyColorHarmonynoneWhich harmony to build. Each case's own defaults match the named functions.

It returns one color or family per entry in hueOffsets, in that order.

Sources#

See also

  • Color theory Pair stops that always read, and pick secondary hues that match your brand, in both modes.
  • .triad Secondary and tertiary colors for tags and categories, at the same contrast as your brand.
  • .splitComplement(spread:) Two accents that stand apart from your brand, softer than its opposite, at the same contrast.
  • .rotateHue(by:) Make an accent or a whole second theme from your brand color, at the same lightness.
  • Setting up themes Recolor a view, a screen or your whole app from one value, with the same contrast in every hue.