What You Will Build

A grid of 12 color swatches that respond to taps with a spring scale animation. The selected color displays a white checkmark and stroke overlay, while a large preview card above the grid shows the current selection with a gradient fill and shadow that matches the chosen color.

Why This Pattern Matters

Color pickers are needed in note-taking apps, drawing tools, theme customization screens, and profile editors. This pattern teaches grid-based selection with visual feedback, matching shadows to dynamic content, and spring animations on selection changes.

Step 1: The Preview Card

The preview card at the top uses the selected color for its fill, text overlay, and shadow. The shadow color changes with the selection:

RoundedRectangle(cornerRadius: 20)
    .fill(selectedColor.gradient)
    .frame(height: 200)
    .overlay {
        Text("Selected Color")
            .font(.title.bold())
            .foregroundStyle(.white)
    }
    .padding(.horizontal)
    .shadow(color: selectedColor.opacity(0.4), radius: 15, y: 8)
    .animation(.spring, value: selectedColor)

Step 2: The Color Grid with Selection State

@State private var selectedColor: Color = .blue
let colors: [Color] = [
    .red, .orange, .yellow, .green, .mint, .teal,
    .cyan, .blue, .indigo, .purple, .pink, .brown,
]

LazyVGrid(columns: Array(repeating: GridItem(.flexible(),
          spacing: 12), count: 6), spacing: 12) {
    ForEach(colors, id: \.self) { color in
        Circle()
            .fill(color.gradient)
            .frame(width: 44, height: 44)
            .overlay {
                if selectedColor == color {
                    Circle()
                        .stroke(.white, lineWidth: 3)
                        .shadow(radius: 3)
                    Image(systemName: "checkmark")
                        .font(.caption.bold())
                        .foregroundStyle(.white)
                }
            }
            .scaleEffect(selectedColor == color ? 1.15 : 1.0)
            .animation(.spring(response: 0.3, dampingFraction: 0.6),
                       value: selectedColor)
            .onTapGesture {
                selectedColor = color
            }
    }
}
.padding(.horizontal)

How the Selection Feedback Works

Three visual cues indicate the selected swatch:

  1. Scale: The selected circle scales to 1.15x, making it visibly larger than its neighbors.
  2. Checkmark: A white checkmark icon appears at the center.
  3. Stroke: A white border with a subtle shadow separates the selected circle from adjacent ones.

The spring animation with low dampingFraction (0.6) creates a slight bounce, giving the selection a playful, responsive feel.

Tips and Pitfalls

  • Color equality: SwiftUI Color conforms to Equatable, so selectedColor == color works directly. However, comparing colors created from custom RGB values may fail due to floating-point precision. Use an ID-based approach for custom colors.
  • .gradient modifier: Available in iOS 16+, this adds a subtle gradient to any solid color. For iOS 15, use LinearGradient explicitly.
  • Dynamic shadow: The .shadow(color: selectedColor.opacity(0.4)) on the preview card creates a colored glow effect that changes with each selection, adding perceived depth.
  • Haptic feedback: Add UIImpactFeedbackGenerator(style: .light).impactOccurred() inside the onTapGesture for tactile confirmation.
  • Custom colors: To support arbitrary colors, model them as identifiable structs with id, name, and Color properties instead of using raw Color values.

iOS Version: iOS 16+ (uses .gradient modifier)

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.