Lab 3.3 — Palette from Brief

Duration: ~60 minutes Prereqs: Xcode 16+, Coolors.co account (free), Adobe Color (free), Contrast Mac app

The brief

Build a meditation and sleep tracking app for adults aged 30–50 who are overworked professionals trying to wind down before bed.

That’s it. One sentence. Your job: derive a complete, defensible color palette, define it in code with light/dark variants, build a 2-screen prototype, and verify every color choice with WCAG contrast checks.

This is the actual exercise you’ll do on day one of any new product. The brief is intentionally vague — real briefs always are.

Goal

By the end, you’ll have:

  • A fully defined palette: 1 brand primary, 1 brand accent, surface tokens, content tokens, state tokens
  • Light + dark variants in Asset Catalog
  • A DesignTokens.swift module
  • 2 SwiftUI screens (Home / Session) using only the palette
  • A documented WCAG audit showing every pair passes AA

Steps

Step 1 — Decode the brief (10 min)

Extract the constraints from the brief sentence-by-sentence:

Brief phraseColor implication
Meditation / sleepCool tones (blue, indigo, purple, deep teal) over warm
Adults 30–50Sophisticated palette, restrained; no neon
Overworked professionalsPremium feel; competes against Calm, Headspace
Winding down before bedDark mode is the primary mode, not the secondary

Research the category: open the App Store, search “meditation” and “sleep.” Screenshot the top 5 apps’ icons and onboarding screens. You’ll find a dominant pattern:

  • Calm: deep navy blue → indigo gradient, mountain photography
  • Headspace: warm orange (outlier, intentional for “friendly”)
  • Sleep Cycle: navy + light blue
  • Insight Timer: purple + magenta
  • Aura: dark navy + teal

Median: navy/indigo/deep blue as primary. Headspace’s orange is a deliberate differentiation but doesn’t fit “winding down” — they own “approachable” instead.

Decision: lean into the category convention. Pick a deep, cool primary.

Step 2 — Generate candidate palettes (10 min)

Go to Coolors.co. Spacebar regenerates palettes; press lock on colors you like and regenerate the rest.

Constraints to enforce:

  • One deep, desaturated primary (navy / indigo / deep blue)
  • One light/medium accent for highlights
  • Neutral surfaces (off-white, soft gray for light; near-black for dark)
  • One warm-ish accent allowed for “session complete” success states

Generate 3 candidate palettes. Save each as a Coolors URL.

Now go to Adobe Color → use “Color Wheel” → set Color Rule to “Analogous” or “Complementary” → pick a deep blue base and explore harmonies. Pick the harmony that visually feels closest to your brief.

Candidate I’ll work with for the lab template (pick your own; this is illustrative):

Primary:    #2D3561  (deep indigo)
Accent:     #8E9AAF  (muted blue-gray)
Surface:    #FAFAFA  (warm off-white)
Surface 2:  #F0F0F4  (slight cool tint)
Text:       #1A1A2E  (near-black with blue undertone)
Text muted: #6B7280  (cool gray)
Success:    #7FB069  (sage green — calm, not aggressive)
Warning:    #E07A5F  (terracotta — soft warm)
Error:      #D62828  (only for critical errors)

Notice: no saturated reds or hot yellows. The palette feels quiet.

Step 3 — Define dark mode pairs (10 min)

For sleep/meditation, dark mode is primary. Each light color needs a dark equivalent that:

  • Has near-black background (#0F0F1A or similar, never pure #000)
  • Desaturates accents ~15-20% (saturated colors feel harsh on dark)
  • Keeps text high-contrast (off-white #F2F2F7)
                 LIGHT            DARK
Primary:         #2D3561    →    #6B7BC4   (desaturated, lighter for visibility on dark)
Accent:          #8E9AAF    →    #B8C2D6
Surface:         #FAFAFA    →    #0F0F1A
Surface 2:       #F0F0F4    →    #1A1A2E
Text:            #1A1A2E    →    #F2F2F7
Text muted:      #6B7280    →    #A0A8B5
Success:         #7FB069    →    #9CC97D
Warning:         #E07A5F    →    #E89B85
Error:           #D62828    →    #FF5C5C

Step 4 — Verify WCAG contrast (5 min)

Open the Contrast app. For every text-on-surface pair, verify:

PairRequired ratioResult
Text on Surface (light)≥ 4.5:1check it
Text on Surface 2 (light)≥ 4.5:1check it
Text muted on Surface (light)≥ 4.5:1check it (most likely to fail — adjust if needed)
Primary on Surface (light)≥ 3:1 (UI element)check it
Text on Surface (dark)≥ 4.5:1check it
All dark mode equivalentssame thresholdscheck all

If any pair fails, darken/lighten the offender by 5% increments and re-check. Document the final hex values.

Step 5 — Define in Asset Catalog (10 min)

Create a new SwiftUI Xcode project: WindDown.

In Assets.xcassets, for each token name (brandPrimary, brandAccent, surface, surface2, textPrimary, textSecondary, success, warning, error):

  1. New Color Set with that name
  2. Attributes Inspector → Appearances: Any, Dark
  3. Set Any to the light hex, Dark to the dark hex
  4. Confirm Xcode generates Color.brandPrimary symbol (project settings → Build Settings → “Generate Asset Symbols” → Yes; default in Xcode 15+)

Create DesignTokens.swift:

import SwiftUI

enum Spacing {
    static let xs: CGFloat = 4
    static let sm: CGFloat = 8
    static let md: CGFloat = 16
    static let lg: CGFloat = 24
    static let xl: CGFloat = 32
}

enum AppFont {
    static let heroTitle = Font.system(size: 34, weight: .light, design: .serif)
    static let title = Font.system(size: 22, weight: .regular)
    static let body = Font.system(size: 17)
    static let caption = Font.system(size: 13, weight: .medium)
}

enum Radius {
    static let sm: CGFloat = 8
    static let md: CGFloat = 16
    static let lg: CGFloat = 24
}

Note the typography choice: serif at light weight for the hero title (Calm and Headspace both use elegant typography to signal “premium meditation”). Stick to system fonts for v1 — NewYork (SwiftUI’s .serif design) is included free.

Step 6 — Build the two screens (15 min)

Home screen

struct HomeView: View {
    var body: some View {
        ZStack {
            Color.surface.ignoresSafeArea()

            VStack(alignment: .leading, spacing: Spacing.lg) {
                Text("Good evening")
                    .font(AppFont.heroTitle)
                    .foregroundStyle(Color.textPrimary)
                Text("Ready to unwind?")
                    .font(AppFont.body)
                    .foregroundStyle(Color.textSecondary)

                ForEach(["10 min · Sleep", "20 min · Deep Rest", "5 min · Breath"], id: \.self) { item in
                    HStack {
                        Image(systemName: "moon.stars")
                            .foregroundStyle(Color.brandPrimary)
                        Text(item)
                            .font(AppFont.body)
                            .foregroundStyle(Color.textPrimary)
                        Spacer()
                        Image(systemName: "chevron.right")
                            .foregroundStyle(Color.textSecondary)
                    }
                    .padding(Spacing.md)
                    .background(Color.surface2)
                    .clipShape(RoundedRectangle(cornerRadius: Radius.md))
                }
                Spacer()
            }
            .padding(Spacing.lg)
        }
    }
}

Session screen

struct SessionView: View {
    @State private var progress = 0.6
    var body: some View {
        ZStack {
            Color.surface.ignoresSafeArea()

            VStack(spacing: Spacing.xl) {
                Text("Deep Rest")
                    .font(AppFont.heroTitle)
                    .foregroundStyle(Color.textPrimary)

                Circle()
                    .trim(from: 0, to: progress)
                    .stroke(Color.brandPrimary, style: StrokeStyle(lineWidth: 8, lineCap: .round))
                    .frame(width: 240, height: 240)
                    .rotationEffect(.degrees(-90))
                    .overlay {
                        Text("8:32")
                            .font(.system(size: 48, weight: .light))
                            .foregroundStyle(Color.textPrimary)
                    }

                Button(action: { }) {
                    Image(systemName: "pause.fill")
                        .font(.title)
                        .foregroundStyle(.white)
                        .frame(width: 64, height: 64)
                        .background(Color.brandPrimary)
                        .clipShape(Circle())
                }
            }
            .padding()
        }
    }
}

Apply the global accent at the App root:

@main
struct WindDownApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .tint(.brandPrimary)
        }
    }
}

Step 7 — Test both modes

  1. Run on iPhone 16 simulator
  2. Toggle dark mode (Cmd+Shift+A)
  3. Both screens should feel equally “right” — different but not jarring
  4. Take screenshots of all 4 combinations (Home/Session × Light/Dark) for the writeup

Step 8 — Document

Create PALETTE.md in the project root with:

  • The brief
  • The category research (5 competitor primary colors)
  • The chosen palette (hex values, light and dark)
  • WCAG contrast results table
  • Screenshots of both screens in both modes
  • One paragraph defending each color choice

Stretch goals

  • Increased Contrast variant: add a third appearance variant in Asset Catalog (Any, Dark, High Contrast Light, High Contrast Dark) with darker text and bolder accents. Test by enabling Settings → Accessibility → Display → Increase Contrast.
  • Animated background gradient: add a slow-shifting linear gradient (60-second loop) using two of your palette colors. Gate on accessibilityReduceMotion.
  • App icon: design a simple app icon using your palette (1024×1024). Use Figma free tier or Sketch. Provide both tinted and dark variants per iOS 26 Liquid Glass guidelines.
  • Onboarding screen: design a 3-card paginated onboarding that uses your full palette. Verify every screen passes WCAG.

Acceptance criteria

  • Palette defined: brand primary, brand accent, surface (1-2), text (2), state colors (3)
  • All colors live in Asset Catalog with Any + Dark variants
  • WCAG AA verified for every text-on-surface pair (light + dark)
  • DesignTokens.swift defines spacing, fonts, radii enums
  • Two screens built using only tokens (no hex literals in view code)
  • .tint(.brandPrimary) applied at root
  • PALETTE.md documents brief, research, palette, contrast, screenshots
  • App works in light and dark mode without visual bugs

Common pitfalls

  • Picking favorite colors over category-fit colors: if your meditation app uses neon green and hot pink, you’ve ignored the brief.
  • Saturated brand color in 60% of the surface: brand color is 10%. Most of the screen should be neutral.
  • Pure black dark mode background: causes OLED smear. Use #0F0F1A or Apple’s systemBackground.
  • Skipping the WCAG check: trendy palettes often fail body-text contrast. Verify every pair.
  • Forgetting .tint(): without it, every Button, Toggle, Slider falls back to system blue, ignoring your brand.

What you’ve learned

You can now take a vague product brief and produce a defensible, accessible, mode-adaptive color system in under an hour. This is a senior skill — most engineers offload this to designers and can’t articulate why a palette works. You can.

The palette work you do once will outlive 90% of the code you write. Spend the hour.


Phase 3 complete. You now have the design literacy to:

  • Read Apple’s HIG and apply it consistently
  • Translate Figma frames to SwiftUI without losing fidelity
  • Define and maintain a token-based color and type system
  • Use SF Symbols with the rendering modes appropriate to each context
  • Build apps that adapt to dark mode, Dynamic Type, and accessibility settings without drama
  • Audit any iOS app for HIG and accessibility violations
  • Design Mac apps that feel Mac-native, not iPhone-ported
  • Derive a palette from a brief, verify it, and ship it

Phase 4 — Swift Language Fundamentals — comes next.