skills/swiftui-atomic-design/SKILL.md
Guide for building SwiftUI components using Brad Frost's Atomic Design methodology — organizing views into Atoms, Molecules, Organisms, Templates, and Pages with Design Tokens for theming. Use this skill whenever building new SwiftUI UI components, refactoring existing views into reusable pieces, creating a design system, or organizing a component library. Also trigger when the user mentions "atomic design", "design system", "component hierarchy", "reusable components", "atoms and molecules", "design tokens", or wants to decompose a complex SwiftUI view into smaller, composable parts, or needs to implement theming/customization across a SwiftUI app.
npx skillsauth add tddworks/claude-skills swiftui-atomic-designInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Atomic Design is Brad Frost's methodology for building UI systems from small, composable pieces. In SwiftUI, this maps naturally to the framework's declarative, compositional view architecture. The hierarchy flows from simple to complex:
Design Tokens → Atoms → Molecules → Organisms → Templates → Pages
Each level composes the one below it. Design Tokens provide the visual foundation that all levels reference. The discipline is knowing where a component belongs — that's what makes the system scalable, consistent, and maintainable.
Design tokens are the centralized source of truth for all visual properties: colors, typography, spacing. They sit beneath atoms — every component references tokens rather than hardcoding values. This ensures consistency across the entire design system and makes sweeping visual changes (rebranding, dark mode, accessibility) a single-point update.
Why tokens matter:
// Design Tokens: Centralized visual constants
struct DesignTokens {
struct Colors {
static let primary = Color("PrimaryColor")
static let secondary = Color("SecondaryColor")
static let background = Color("BackgroundColor")
static let surface = Color(.secondarySystemGroupedBackground)
static let textPrimary = Color.primary
static let textSecondary = Color.secondary
}
struct Typography {
static let largeTitle = Font.largeTitle.weight(.bold)
static let title = Font.title2.weight(.semibold)
static let headline = Font.headline.weight(.medium)
static let body = Font.body
static let caption = Font.caption
static let captionBold = Font.caption2.weight(.medium)
}
struct 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
}
struct Radius {
static let sm: CGFloat = 8
static let md: CGFloat = 12
static let lg: CGFloat = 16
}
}
Common token categories: colors, typography (font families, sizes, weights), spacing (margins, padding), corner radii, shadows/elevations, animation durations, opacity values.
Tokens become powerful when combined with SwiftUI's Environment system for runtime theming. Define theme variants and inject them through the environment so every component adapts automatically.
// Theme definition using tokens
struct AppTheme {
let primaryColor: Color
let secondaryColor: Color
let backgroundColor: Color
let textColor: Color
static let light = AppTheme(
primaryColor: .blue,
secondaryColor: .gray,
backgroundColor: .white,
textColor: .black
)
static let dark = AppTheme(
primaryColor: .cyan,
secondaryColor: .gray,
backgroundColor: .black,
textColor: .white
)
}
// Environment integration
struct AppThemeKey: EnvironmentKey {
static let defaultValue: AppTheme = .light
}
extension EnvironmentValues {
var appTheme: AppTheme {
get { self[AppThemeKey.self] }
set { self[AppThemeKey.self] = newValue }
}
}
// Apply theme to a view hierarchy
struct ThemedModifier: ViewModifier {
let theme: AppTheme
func body(content: Content) -> some View {
content.environment(\.appTheme, theme)
}
}
extension View {
func themed(_ theme: AppTheme) -> some View {
modifier(ThemedModifier(theme: theme))
}
}
Components read from @Environment(\.appTheme) instead of hardcoding colors, making the entire UI theme-switchable.
An atom is the smallest meaningful UI element. It does one thing and has no awareness of its context. Atoms reference Design Tokens for all visual properties — they never hardcode colors, fonts, or spacing.
What qualifies as an atom:
SwiftUI patterns for atoms:
bodysize * 0.5) so atoms scale naturally// Atom: Colored SF Symbol in a rounded square
struct IconBadge: View {
let icon: String
let color: Color
var size: CGFloat = 28
var body: some View {
Image(systemName: icon)
.font(.system(size: size * 0.5, weight: .medium))
.foregroundStyle(color)
.frame(width: size, height: size)
.background(color.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: size * 0.21))
}
}
// Atom: Capsule-shaped status indicator
struct StatePill: View {
let text: String
let color: Color
var body: some View {
Text(text)
.font(DesignTokens.Typography.captionBold)
.foregroundStyle(color)
.padding(.horizontal, DesignTokens.Spacing.sm)
.padding(.vertical, 3)
.background(color.opacity(0.12))
.clipShape(Capsule())
}
}
// Atom: Reusable primary button
struct PrimaryButton: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(DesignTokens.Typography.headline)
.foregroundColor(.white)
.padding(.horizontal, DesignTokens.Spacing.lg)
.padding(.vertical, DesignTokens.Spacing.md)
.background(DesignTokens.Colors.primary)
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm))
}
}
}
// Atom: Styled text with token-based typography
struct TitleText: View {
let text: String
var body: some View {
Text(text)
.font(DesignTokens.Typography.title)
.foregroundStyle(DesignTokens.Colors.textPrimary)
}
}
Common atoms: badges, pills, dividers, icon wrappers, character counters, section headers (text-only), card backgrounds, simple labels, primary/secondary buttons, text field wrappers.
A molecule combines 2–3 atoms (or atom-level elements) into a unit that serves a single user purpose. Molecules are the backbone of the design system — they create the reusable patterns that appear throughout the app. The key test: does this combination appear in multiple places?
What qualifies as a molecule:
@Binding for two-way data flow (e.g., text fields)// Molecule: Label-value pair for detail screens
struct InfoRow: View {
let label: String
let value: String
var valueColor: Color = .primary
var body: some View {
HStack {
Text(label)
.font(DesignTokens.Typography.body)
.foregroundStyle(DesignTokens.Colors.textSecondary)
Spacer()
Text(value)
.font(DesignTokens.Typography.body)
.foregroundStyle(valueColor)
}
.padding(DesignTokens.Spacing.md)
}
}
// Molecule: Tappable card with icon, title, and subtitle
struct QuickActionCard: View {
let title: String
let subtitle: String
let icon: String
let color: Color
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(alignment: .leading, spacing: DesignTokens.Spacing.sm) {
Image(systemName: icon)
.font(.system(size: 24))
.foregroundStyle(color)
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(DesignTokens.Typography.headline)
.foregroundStyle(DesignTokens.Colors.textPrimary)
Text(subtitle)
.font(DesignTokens.Typography.caption)
.foregroundStyle(DesignTokens.Colors.textSecondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(DesignTokens.Spacing.md)
.background(DesignTokens.Colors.surface)
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md))
}
.buttonStyle(.plain)
}
}
// Molecule: Search form combining label, text field, and button atoms
struct SearchBar: View {
@Binding var query: String
var placeholder: String = "Search..."
var onSearch: () -> Void
var body: some View {
HStack(spacing: DesignTokens.Spacing.sm) {
Image(systemName: "magnifyingglass")
.foregroundStyle(DesignTokens.Colors.textSecondary)
TextField(placeholder, text: $query)
.font(DesignTokens.Typography.body)
if !query.isEmpty {
Button { query = "" } label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(DesignTokens.Colors.textSecondary)
}
}
}
.padding(DesignTokens.Spacing.sm)
.background(DesignTokens.Colors.surface)
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.sm))
.onSubmit(onSearch)
}
}
Common molecules: info rows, action buttons with labels, search bars, form fields with validation, stat cards, option/toggle rows, tip views, labeled input fields.
An organism is a distinct section of the interface that could stand alone. It composes multiple molecules (and atoms) into a cohesive unit. Organisms form the significant parts of your UI — login forms, navigation bars, metric dashboards. This is where you introduce @ViewBuilder content slots and section-level structure.
What qualifies as an organism:
@ViewBuilder for content injection@Environment for contextual data@State for expand/collapse, selection, etc.)// Organism: Collapsible content section with header
struct ContentSection<Content: View>: View {
let title: String
let subtitle: String?
let icon: String
let iconColor: Color
@ViewBuilder let content: Content
@State private var isExpanded = true
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Button { withAnimation { isExpanded.toggle() } } label: {
HStack(spacing: 10) {
IconBadge(icon: icon, color: iconColor)
VStack(alignment: .leading, spacing: 2) {
Text(title).font(DesignTokens.Typography.headline)
if let subtitle {
Text(subtitle).font(DesignTokens.Typography.caption)
.foregroundStyle(DesignTokens.Colors.textSecondary)
}
}
Spacer()
Image(systemName: "chevron.right")
.rotationEffect(.degrees(isExpanded ? 90 : 0))
.foregroundStyle(DesignTokens.Colors.textSecondary)
}
}
.buttonStyle(.plain)
.padding(DesignTokens.Spacing.md)
if isExpanded {
content
.padding(.horizontal, DesignTokens.Spacing.md)
.padding(.bottom, DesignTokens.Spacing.md)
}
}
.background(DesignTokens.Colors.surface)
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md))
}
}
// Organism: Login form composing labeled input molecules
struct LoginForm: View {
@State private var username = ""
@State private var password = ""
var onLogin: (String, String) -> Void
var body: some View {
VStack(spacing: DesignTokens.Spacing.lg) {
LabeledInputField(label: "Username", text: $username)
LabeledInputField(label: "Password", text: $password, isSecure: true)
PrimaryButton(title: "Login") {
onLogin(username, password)
}
}
.padding(DesignTokens.Spacing.lg)
.background(DesignTokens.Colors.surface)
.clipShape(RoundedRectangle(cornerRadius: DesignTokens.Radius.md))
}
}
Common organisms: content sections, form groups, navigation bars, card lists, detail panels, metric dashboards, grouped settings, login forms.
A template defines the spatial arrangement of organisms on a screen. It's the skeleton — it knows where things go but not what specific data fills them. Templates use generics and @ViewBuilder heavily. They handle page-level concerns like loading states, empty states, and error states.
What qualifies as a template:
// Template: Standard detail page layout with header, content, and actions
struct DetailPageTemplate<
Header: View,
Content: View,
Actions: View
>: View {
@ViewBuilder let header: Header
@ViewBuilder let content: Content
@ViewBuilder let actions: Actions
let isLoading: Bool
var body: some View {
ScrollView {
VStack(spacing: DesignTokens.Spacing.md) {
header
if isLoading {
ProgressView()
.frame(maxWidth: .infinity, minHeight: 200)
} else {
content
}
}
.padding(DesignTokens.Spacing.md)
}
.background(DesignTokens.Colors.background)
.safeAreaInset(edge: .bottom) {
actions
.padding(.horizontal, DesignTokens.Spacing.md)
.padding(.bottom, DesignTokens.Spacing.sm)
}
}
}
// Template: Main layout with navigation bar and content area
struct MainLayout<Content: View>: View {
let title: String
@ViewBuilder let content: Content
var body: some View {
VStack(spacing: 0) {
// Navigation bar (organism)
HStack {
Image(systemName: "arrow.left")
Spacer()
Text(title).font(DesignTokens.Typography.headline)
Spacer()
Image(systemName: "gear")
}
.padding(DesignTokens.Spacing.md)
.background(DesignTokens.Colors.primary)
content
.padding(DesignTokens.Spacing.md)
Spacer()
}
.background(DesignTokens.Colors.background)
}
}
A page is a specific instance of a template, wired to real data and business logic. This is where @Observable objects, @Environment, and navigation live. Pages are what users actually interact with — they assemble templates, inject data, and handle user actions.
// Page: App detail screen using the DetailPageTemplate
struct AppDetailPage: View {
let app: AppEntity
@Environment(\.appRepository) private var repository
@State private var isLoading = true
@State private var metrics: AppMetrics?
var body: some View {
DetailPageTemplate(
header: { AppHeaderOrganism(app: app) },
content: {
if let metrics {
MetricsDashboardOrganism(metrics: metrics)
RecentReviewsOrganism(appId: app.id)
}
},
actions: {
QuickActionCard(
title: "View in App Store",
subtitle: "Open external link",
icon: "arrow.up.forward.app",
color: .blue
) { /* open URL */ }
},
isLoading: isLoading
)
.task {
metrics = try? await repository.fetchMetrics(for: app.id)
isLoading = false
}
}
}
// Page: Login page using MainLayout template
struct LoginPage: View {
var body: some View {
MainLayout(title: "Welcome") {
LoginForm { username, password in
// Handle authentication
}
}
}
}
Organize components by atomic level within each module:
Sources/
├── DesignTokens/ # Colors, Typography, Spacing, Radius
├── Components/
│ ├── Atoms/ # IconBadge, StatePill, PrimaryButton, TitleText
│ ├── Molecules/ # InfoRow, QuickActionCard, SearchBar, ActionRow
│ └── Organisms/ # ContentSection, LoginForm, MetricsDashboard
├── Templates/ # DetailPageTemplate, MainLayout, ListPageTemplate
└── Pages/ # AppDetailPage, LoginPage, SettingsPage
For a shared design system module used across targets:
DesignSystemModule/
├── Sources/
│ ├── Tokens/ # DesignTokens, AppTheme
│ ├── Atoms/
│ ├── Molecules/
│ ├── Organisms/
│ └── Modifiers/ # ViewModifiers that cut across levels
Ask these questions in order:
Gray areas:
@State)When a visual treatment applies across levels (card styling, glass effects), extract it as a ViewModifier rather than duplicating styling. This keeps atoms clean and ensures visual consistency:
struct CardBackgroundModifier: ViewModifier {
var cornerRadius: CGFloat = DesignTokens.Radius.md
func body(content: Content) -> some View {
content
.padding(DesignTokens.Spacing.md)
.background(DesignTokens.Colors.surface)
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
}
}
extension View {
func cardBackground(cornerRadius: CGFloat = DesignTokens.Radius.md) -> some View {
modifier(CardBackgroundModifier(cornerRadius: cornerRadius))
}
}
Organisms and templates should accept content through @ViewBuilder closures rather than concrete child types. This keeps higher-level components flexible without creating tight coupling:
struct SectionContainer<Content: View>: View {
let title: String
@ViewBuilder let content: () -> Content
var body: some View {
VStack(alignment: .leading, spacing: DesignTokens.Spacing.sm) {
Text(title).font(DesignTokens.Typography.headline)
content()
}
}
}
Use generics when a component needs to render different view types in specific slots:
struct HeaderDetailLayout<Header: View, Detail: View>: View {
@ViewBuilder let header: () -> Header
@ViewBuilder let detail: () -> Detail
var body: some View {
VStack(spacing: DesignTokens.Spacing.md) {
header()
detail()
}
}
}
When an atom supports multiple visual treatments, use an enum rather than multiple boolean flags:
struct ActionBadge: View {
enum Style { case primary, secondary, destructive }
let label: String
let style: Style
private var color: Color {
switch style {
case .primary: DesignTokens.Colors.primary
case .secondary: DesignTokens.Colors.secondary
case .destructive: .red
}
}
// ...
}
Every component should have a #Preview block. Atoms preview in isolation, molecules with sample data, organisms with mock content:
#Preview("IconBadge Sizes") {
HStack(spacing: DesignTokens.Spacing.sm) {
IconBadge(icon: "star.fill", color: .yellow, size: 24)
IconBadge(icon: "star.fill", color: .yellow, size: 32)
IconBadge(icon: "star.fill", color: .yellow, size: 44)
}
.padding()
}
PrimaryButton atom, SearchBar molecule, LoginForm organism)#Preview blocks and header comments describing atomic level and purpose@Environment or Design Tokens, never hardcode brand colorsColor.blue or .font(.system(size: 16)) directly in components instead of referencing Design Tokens defeats the purpose of the systemMark each file's atomic level in the header comment for quick identification:
//
// IconBadge.swift
// ModuleName
//
// Atom: Colored SF Symbol in a rounded square
//
This makes it easy to verify a file is in the right directory and understand its role at a glance.
tools
Replace with description of the skill and when Claude should use it.
development
iOS/macOS app localization management for Tuist-based projects with .strings files. Use when: (1) Adding new translation keys to modules, (2) Validating .strings files for missing/duplicate keys, (3) Syncing translations across languages, (4) AI-powered translation from English to other locales, (5) Checking placeholder consistency (%@, %d), (6) Generating localization reports, (7) Updating Swift code to use localized strings instead of hardcoded text.
development
Create interactive iOS/mobile app UX flow prototypes as HTML documents with realistic phone mockups. Use when: (1) Visualizing user journeys and navigation flows, (2) Creating mobile app wireframes, (3) Documenting screen-to-screen navigation patterns, (4) Presenting iOS UI designs with annotations, (5) Prototyping app architecture before implementation. Generates self-contained HTML files with iOS-native styling, phone frames, flow arrows, and callout annotations.
devops
Scaffold iOS apps with Tuist and layered architecture (Domain, Infrastructure, App). Use when: (1) Creating a new iOS app project, (2) Setting up Tuist project structure, (3) User asks to "create an iOS app", "scaffold an app", or "set up a new Swift project", (4) User wants layered/clean architecture for iOS, (5) User mentions Tuist setup.