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.swiftmodule - 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 phrase | Color implication |
|---|---|
| Meditation / sleep | Cool tones (blue, indigo, purple, deep teal) over warm |
| Adults 30–50 | Sophisticated palette, restrained; no neon |
| Overworked professionals | Premium feel; competes against Calm, Headspace |
| Winding down before bed | Dark 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:
| Pair | Required ratio | Result |
|---|---|---|
| Text on Surface (light) | ≥ 4.5:1 | check it |
| Text on Surface 2 (light) | ≥ 4.5:1 | check it |
| Text muted on Surface (light) | ≥ 4.5:1 | check it (most likely to fail — adjust if needed) |
| Primary on Surface (light) | ≥ 3:1 (UI element) | check it |
| Text on Surface (dark) | ≥ 4.5:1 | check it |
| All dark mode equivalents | same thresholds | check 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):
- New Color Set with that name
- Attributes Inspector → Appearances: Any, Dark
- Set Any to the light hex, Dark to the dark hex
- Confirm Xcode generates
Color.brandPrimarysymbol (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
- Run on iPhone 16 simulator
- Toggle dark mode (
Cmd+Shift+A) - Both screens should feel equally “right” — different but not jarring
- 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.swiftdefines spacing, fonts, radii enums - Two screens built using only tokens (no hex literals in view code)
-
.tint(.brandPrimary)applied at root -
PALETTE.mddocuments 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
#0F0F1Aor Apple’ssystemBackground. - 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.