atomic-design-ios
SKILL.md
Atomic Design iOS — Expert Decisions
Expert decision frameworks for Atomic Design choices in SwiftUI. Claude knows view composition — this skill provides judgment calls for when component hierarchy adds value and how to define boundaries.
Decision Trees
Do You Need Atomic Design?
How large is your design system?
├─ Small (< 10 components)
│ └─ Skip formal hierarchy
│ Simple "Components" folder is fine
│
├─ Medium (10-30 components)
│ └─ Consider Atoms + Molecules
│ Skip Organisms/Templates if not needed
│
└─ Large (30+ components, multiple teams)
└─ Full Atomic Design hierarchy
Atoms → Molecules → Organisms → Templates
The trap: Atomic Design for a 5-screen app. The overhead of categorization exceeds the benefit.
Atom vs Molecule Boundary
Does this component combine multiple distinct elements?
├─ NO (single visual element)
│ └─ Atom
│ Button, TextField, Badge, Icon, Label
│
└─ YES (2+ elements that work together)
└─ Can these elements be used independently?
├─ YES → Molecule (SearchBar = Icon + TextField + Button)
└─ NO → Still Atom (password field with toggle is one unit)
Component Extraction Decision
Will this be used in multiple places?
├─ NO (one-off)
│ └─ Don't extract
│ Inline in parent view
│
├─ YES (2-3 places)
│ └─ Extract as local component
│ Same file or sibling file
│
└─ YES (4+ places or cross-feature)
└─ Extract to design system
Full Atom/Molecule treatment
Design Token Scope
What type of value?
├─ Color
│ └─ Is it semantic or brand?
│ ├─ Semantic (error, success) → Color.error, Color.success
│ └─ Brand (primary, accent) → Color.brandPrimary
│
├─ Spacing
│ └─ Use named scale (xs, sm, md, lg, xl)
│ Never magic numbers
│
├─ Typography
│ └─ Use semantic names (body, heading, caption)
│ Map to Font.body, Font.heading
│
└─ Corner radius, shadows
└─ Named tokens if used consistently
Radius.card, Shadow.elevated
NEVER Do
Component Design
NEVER create atoms that know about app state:
// ❌ Atom depends on app-level state
struct PrimaryButton: View {
@EnvironmentObject var authManager: AuthManager
var body: some View {
Button(action: action) {
if authManager.isLoading { ProgressView() }
else { Text(title) }
}
}
}
// ✅ Atom receives all state as parameters
struct PrimaryButton: View {
let title: String
let action: () -> Void
var isLoading: Bool = false
var body: some View {
Button(action: action) {
if isLoading { ProgressView() }
else { Text(title) }
}
}
}
NEVER hardcode values in components:
// ❌ Magic numbers everywhere
struct Card: View {
var body: some View {
content
.padding(16) // Magic number
.background(Color(hex: "#FFFFFF")) // Hardcoded
.cornerRadius(12) // Magic number
}
}
// ✅ Use design tokens
struct Card: View {
var body: some View {
content
.padding(Spacing.md)
.background(Color.surface)
.cornerRadius(Radius.card)
}
}
NEVER create components with too many parameters:
// ❌ Too many parameters — hard to use
struct ComplexButton: View {
let title: String
let subtitle: String?
let icon: String?
let iconPosition: IconPosition
let size: Size
let style: Style
let isLoading: Bool
let isEnabled: Bool
let hasBorder: Bool
let cornerRadius: CGFloat
// ... 10 more parameters
}
// ✅ Split into focused variants
struct PrimaryButton: View { ... }
struct SecondaryButton: View { ... }
struct IconButton: View { ... }
struct LoadingButton: View { ... }
Hierarchy Mistakes
NEVER skip levels in composition:
// ❌ Template directly uses atoms (no molecules/organisms)
struct ProductListTemplate: View {
var body: some View {
ForEach(products) { product in
// Building organism inline from atoms
HStack {
AsyncImage(url: product.imageURL)
VStack {
Text(product.name).font(.headline)
Text("$\(product.price)").foregroundColor(.blue)
}
Button("Add") { }
}
}
}
}
// ✅ Template uses organisms
struct ProductListTemplate: View {
var body: some View {
ForEach(products) { product in
ProductCard(product: product, onAddToCart: { })
}
}
}
NEVER put business logic in design system components:
// ❌ Organism fetches data
struct UserCard: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
Card {
// Uses viewModel.user
}
.onAppear { viewModel.load() }
}
}
// ✅ Organism is purely presentational
struct UserCard: View {
let user: User
let onTap: () -> Void
var body: some View {
Card {
// Uses passed-in user
}
}
}
Design Token Mistakes
NEVER use platform colors directly:
// ❌ Hardcoded system colors
.foregroundColor(.blue)
.background(Color(.systemGray6))
// ✅ Semantic tokens that can be themed
.foregroundColor(Color.interactive)
.background(Color.surfaceSecondary)
NEVER duplicate token definitions:
// ❌ Same value defined in multiple places
struct Card { let cornerRadius: CGFloat = 12 }
struct Button { let cornerRadius: CGFloat = 12 }
struct TextField { let cornerRadius: CGFloat = 12 }
// ✅ Single source of truth
enum Radius {
static let sm: CGFloat = 4
static let md: CGFloat = 8
static let lg: CGFloat = 12
}
struct Card { ... .cornerRadius(Radius.lg) }
Essential Patterns
Token System Structure
// Spacing tokens
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
}
// Color tokens (support dark mode)
extension Color {
// Semantic
static let textPrimary = Color("TextPrimary")
static let textSecondary = Color("TextSecondary")
static let surface = Color("Surface")
static let surfaceSecondary = Color("SurfaceSecondary")
// Brand
static let brandPrimary = Color("BrandPrimary")
static let brandAccent = Color("BrandAccent")
// Feedback
static let success = Color("Success")
static let warning = Color("Warning")
static let error = Color("Error")
}
// Typography tokens
extension Font {
static let displayLarge = Font.system(size: 34, weight: .bold)
static let heading1 = Font.system(size: 28, weight: .bold)
static let heading2 = Font.system(size: 22, weight: .semibold)
static let bodyLarge = Font.system(size: 17)
static let bodyRegular = Font.system(size: 15)
static let caption = Font.system(size: 13)
}
Composable Atom Pattern
// Atom with sensible defaults and overrides
struct PrimaryButton: View {
let title: String
let action: () -> Void
var isLoading: Bool = false
var isEnabled: Bool = true
var size: Size = .regular
enum Size {
case small, regular, large
var padding: EdgeInsets {
switch self {
case .small: return EdgeInsets(horizontal: Spacing.sm, vertical: Spacing.xs)
case .regular: return EdgeInsets(horizontal: Spacing.md, vertical: Spacing.sm)
case .large: return EdgeInsets(horizontal: Spacing.lg, vertical: Spacing.md)
}
}
var font: Font {
switch self {
case .small: return .caption
case .regular: return .bodyRegular
case .large: return .heading2
}
}
}
var body: some View {
Button(action: action) {
Group {
if isLoading {
ProgressView()
} else {
Text(title).font(size.font)
}
}
.frame(maxWidth: .infinity)
.padding(size.padding)
}
.background(isEnabled ? Color.brandPrimary : Color.textSecondary)
.foregroundColor(.white)
.cornerRadius(Radius.md)
.disabled(!isEnabled || isLoading)
}
}
Molecule with Slot Pattern
// Generic molecule with customizable slots
struct Card<Content: View, Footer: View>: View {
let content: Content
let footer: Footer?
init(
@ViewBuilder content: () -> Content,
@ViewBuilder footer: () -> Footer
) {
self.content = content()
self.footer = footer()
}
var body: some View {
VStack(alignment: .leading, spacing: Spacing.md) {
content
if let footer = footer {
Divider()
footer
}
}
.padding(Spacing.md)
.background(Color.surface)
.cornerRadius(Radius.lg)
.shadow(color: .black.opacity(0.1), radius: 4, y: 2)
}
}
// Convenience initializer without footer
extension Card where Footer == EmptyView {
init(@ViewBuilder content: () -> Content) {
self.content = content()
self.footer = nil
}
}
Quick Reference
When to Extract Components
| Scenario | Action |
|---|---|
| Used once | Keep inline |
| Used 2-3 times in same feature | Local extraction |
| Used across features | Design system component |
| Complex but single-use | Extract for readability only |
Component Classification
| Level | Examples | Knows About |
|---|---|---|
| Atom | Button, TextField, Icon, Badge | Nothing external |
| Molecule | SearchBar, FormInput, Card | Atoms only |
| Organism | NavigationBar, ProductCard, UserList | Atoms + Molecules |
| Template | ListPageLayout, FormLayout | Organisms |
Design Token Categories
| Category | Token Examples |
|---|---|
| Spacing | xs, sm, md, lg, xl |
| Color | textPrimary, surface, brandPrimary, error |
| Typography | displayLarge, heading1, body, caption |
| Radius | sm, md, lg, full |
| Shadow | subtle, elevated, prominent |
Red Flags
| Smell | Problem | Fix |
|---|---|---|
| Atom uses @EnvironmentObject | Knows too much | Pass state as params |
| 10+ parameters on component | Too flexible | Split into variants |
| Magic numbers in components | Not themeable | Use tokens |
| Template builds from atoms | Skipping levels | Use molecules/organisms |
| Different corner radius per component | Inconsistency | Token system |
| Component fetches data | Wrong layer | Presentational only |
Weekly Installs
15
Repository
kaakati/rails-e…rise-devGitHub Stars
6
First Seen
Jan 25, 2026
Security Audits
Installed on
claude-code13
opencode13
gemini-cli12
codex11
antigravity11
github-copilot10