DEV Community

Sebastien Lato
Sebastien Lato

Posted on

Build a Reusable SwiftUI Component Library

SwiftUI makes it easy to build UI — but building reusable components that look consistent across your entire app is a different challenge.

As apps grow, UI duplication becomes a real problem:

  • repeated button styles
  • inconsistent card shapes
  • duplicated text modifiers
  • copy-paste shadows
  • multiple versions of the same layout

A component library solves all of this.

Today you’ll learn how to build a modern, scalable, Apple-style SwiftUI Component Library that includes:

  • buttons
  • cards
  • text fields
  • floating panels
  • chips
  • design tokens (colors, radii, shadows)
  • reusable modifiers
  • consistent styling

This is the exact structure I use in real production apps.

Let’s build it. 🚀


🧱 1. Start With a Design Folder

Design/ ├── Colors.swift ├── Radii.swift ├── Shadows.swift └── Typography.swift 
Enter fullscreen mode Exit fullscreen mode

This gives your app consistent design tokens.

Colors.swift

enum AppColor { static let primary = Color.blue static let background = Color(.systemBackground) static let glassStroke = Color.white.opacity(0.25) } 
Enter fullscreen mode Exit fullscreen mode

Radii.swift

enum AppRadius { static let small: CGFloat = 10 static let medium: CGFloat = 16 static let large: CGFloat = 22 } 
Enter fullscreen mode Exit fullscreen mode

Shadows.swift

enum AppShadow { static let card = Color.black.opacity(0.18) static let glow = Color.blue.opacity(0.3) } 
Enter fullscreen mode Exit fullscreen mode

Typography.swift

enum AppFont { static let title = Font.system(.title3, design: .rounded).bold() static let body = Font.system(.body, design: .rounded) } 
Enter fullscreen mode Exit fullscreen mode

One place → full app consistency.


🔵 2. Reusable Button Styles

A clean button style gives your UI an identity.

PrimaryButton

struct PrimaryButton: View { let title: String let action: () -> Void var body: some View { Button(action: action) { Text(title) .font(AppFont.body.bold()) .padding(.horizontal, 28) .padding(.vertical, 14) .background(AppColor.primary) .foregroundColor(.white) .clipShape(RoundedRectangle(cornerRadius: AppRadius.medium, style: .continuous)) .shadow(color: AppShadow.card, radius: 16, y: 8) } } } 
Enter fullscreen mode Exit fullscreen mode

Usage:

PrimaryButton(title: "Continue") { print("Pressed") } 
Enter fullscreen mode Exit fullscreen mode

🟦 3. Glass Card Component (Reusable)

struct GlassCard<Content: View>: View { @ViewBuilder let content: () -> Content var body: some View { content() .padding(20) .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: AppRadius.large, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: AppRadius.large, style: .continuous) .stroke(AppColor.glassStroke, lineWidth: 1) ) .shadow(color: AppShadow.card, radius: 24, y: 12) } } 
Enter fullscreen mode Exit fullscreen mode

Usage:

GlassCard { VStack(alignment: .leading) { Text("Glass Card") .font(AppFont.title) Text("Reusable glassmorphic component.") .foregroundColor(.secondary) } } 
Enter fullscreen mode Exit fullscreen mode

🟩 4. TextField Component (Modern, Clean)

struct AppTextField: View { var title: String @Binding var text: String var body: some View { TextField(title, text: $text) .padding(.horizontal, 14) .padding(.vertical, 12) .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: AppRadius.medium)) .overlay( RoundedRectangle(cornerRadius: AppRadius.medium) .stroke(AppColor.glassStroke) ) .shadow(color: AppShadow.card.opacity(0.25), radius: 12, y: 6) } } 
Enter fullscreen mode Exit fullscreen mode

Usage:

@State private var name = "" AppTextField(title: "Name", text: $name) 
Enter fullscreen mode Exit fullscreen mode

🔶 5. Chips / Tags

struct Chip: View { let label: String let icon: String? var body: some View { HStack(spacing: 6) { if let icon { Image(systemName: icon) } Text(label) } .font(.caption) .padding(.horizontal, 12) .padding(.vertical, 8) .background(.ultraThinMaterial) .clipShape(Capsule()) .overlay(Capsule().stroke(AppColor.glassStroke)) } } 
Enter fullscreen mode Exit fullscreen mode

Example:

HStack { Chip(label: "SwiftUI", icon: "swift") Chip(label: "Design", icon: "paintbrush") } 
Enter fullscreen mode Exit fullscreen mode

🪟 6. Floating Panel (Reusable Bottom Sheet Top Section)

struct FloatingPanel<Content: View>: View { @ViewBuilder let content: () -> Content var body: some View { VStack(spacing: 0) { Capsule() .fill(AppColor.glassStroke) .frame(width: 40, height: 6) .padding(.top, 10) content() .padding() } .background(.regularMaterial) .clipShape(RoundedRectangle(cornerRadius: AppRadius.large, style: .continuous)) .shadow(color: .black.opacity(0.25), radius: 30, y: 14) } } 
Enter fullscreen mode Exit fullscreen mode

🧩 7. Reusable Modifiers (Powerful Tool!)

A clean reusable modifier:

struct CardPadding: ViewModifier { func body(content: Content) -> some View { content .padding(20) .clipShape(RoundedRectangle(cornerRadius: AppRadius.large)) } } extension View { func cardPadding() -> some View { modifier(CardPadding()) } } 
Enter fullscreen mode Exit fullscreen mode

Modifiers keep your views clean and declarative.


🗂 8. Suggested Folder Structure

Design/ │ Colors.swift │ Radii.swift │ Shadows.swift │ Typography.swift │ Components/ │ Buttons/ │ Cards/ │ TextFields/ │ Panels/ │ Chips/ │ Modifiers/ 
Enter fullscreen mode Exit fullscreen mode

Everything clean. Everything scalable.


🚀 Final Thoughts

A SwiftUI Component Library gives you:

  • consistent design
  • faster iteration
  • reusable building blocks
  • scalable architecture
  • easier team onboarding
  • more polished UI

This is how you go from “building screens” → to building systems.

Top comments (2)

Collapse
 
shuvo_fcaea9007f29a2f4e5f profile image
Shuvo

Been using HugeIcons Figma plugin for SwiftUI chips – stroke weights stay consistent across swaps. Export to code smooth enough?

Collapse
 
sebastienlato profile image
Sebastien Lato

Thanks! HugeIcons is solid — great for consistent weights. For SwiftUI, exports are smooth as long as you re-apply corner radius + stroke in code. I usually treat the Figma export as structure, then let my component library handle the styling so everything stays consistent across the app.