How to Use SF Symbols in SwiftUI Without Fighting the Docs
Apple’s icon library is powerful but confusing. Here’s the practical guide to using SF Symbols the right way

📦 The Basics — Your First SF Symbol
So Apple ships like 5000+ icons with every OS update and somehow the docs make it feel harder than it should be.
Here’s the deal: SF Symbols are vector icons that come free with iOS, macOS, watchOS — basically everything Apple. They scale. They adapt to Dark Mode. They match San Francisco font weights automatically. And you don’t need to mess with asset catalogs.
The most basic usage? Dead simple:
Image(systemName: "heart")That’s it. You get a heart icon. But here’s where people get tripped up — this returns an Image view, not some special Symbol type. Which means you can modify it like any SwiftUI image:
Image(systemName: "heart.fill")
.foregroundColor(.red)
.font(.largeTitle)Wait, .font() on an image? Yeah. That's the weird part.
SF Symbols behave like text, not images. So you size them with .font(), not .frame(). Use .foregroundColor() or .foregroundStyle() for colors. This trips up everyone coming from UIKit where you'd use UIImage(systemName:) and think about point sizes.
Quick gotcha: typo the symbol name and you get… nothing. No compile error, no crash, just an empty space in your UI. So either keep the SF Symbols app open (it’s free on the Mac App Store) or use Xcode’s autocomplete. Type "magnifying" and Xcode will show you magnifyingglass, magnifyingglass.circle, etc.
VStack(spacing: 20) {
Image(systemName: "star")
Image(systemName: "star.fill")
Image(systemName: "star.leadinghalf.filled")
}
.font(.title)
.foregroundStyle(.yellow)
Three star variations, all perfectly aligned, all scaling together. Try doing that with PNGs.
One more thing — if you’re targeting iOS 17+, you can animate between symbol variants:
Image(systemName: isFavorite ? "heart.fill" : "heart")
.contentTransition(.symbolEffect(.replace))The icon morphs instead of just swapping (try different icons). Looks way smoother than it has any right to.
🎨 Rendering Modes & Colors
Okay so this is where SF Symbols get actually interesting but also where Apple’s naming gets confusing.
There are four rendering modes: monochrome, hierarchical, palette, and multicolor. And yeah, you kinda have to memorize what each one does because the names don’t help much.
Monochrome = one color, what you’d expect:
Image(systemName: "cloud.rain.fill")
.symbolRenderingMode(.monochrome)
.foregroundColor(.blue)Everything’s blue. Simple.
Hierarchical = one color but with automatic opacity variations to show depth:
Image(systemName: "cloud.rain.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)Now the cloud is full opacity blue, the rain is like 60% opacity blue. Apple figures out which parts should be lighter automatically. Works great for filled symbols.
Palette = you define multiple colors, Apple applies them to different layers:
Image(systemName: "cloud.sun.rain.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.gray, .yellow, .blue)Cloud gets gray, sun gets yellow, rain gets blue. Order matters here — first color goes to the primary layer, second to secondary, etc. Which layer is which? You kinda just have to try it and see. Or check the SF Symbols app which shows you the layers.
Multicolor = symbols that have predefined colors built-in:
Image(systemName: "rainbow")
.symbolRenderingMode(.multicolor)Some symbols like rainbow, thermometer, flags - they have colors baked in. You can override them but usually you don't want to.
Here’s the thing though — you don’t always need to set the rendering mode explicitly. If you use .foregroundStyle() with multiple colors, SwiftUI assumes you want palette mode, but not all symbols support palette mode:
// automatically uses palette mode
Image(systemName: "cloud.sun.rain.fill")
.foregroundStyle(.gray, .yellow, .blue)And if you just set one color? Monochrome. SwiftUI picks the mode based on what you’re doing, which is nice until it guesses wrong.
TabView {
Text("Home")
.tabItem {
Label("Home", systemImage: "house.fill")
}
}
.symbolRenderingMode(.hierarchical)All your tab icons get that subtle depth without you having to design anything.
⚡ Symbol Variants (fill, circle, square)
Apple’s got this variant system that’s basically icon + shape modifier. You can take almost any symbol and add .fill, .circle, .square, .circle.fill, .square.fill - you get the idea.
HStack(spacing: 15) {
Image(systemName: "heart")
Image(systemName: "heart.fill")
Image(systemName: "heart.circle")
Image(systemName: "heart.circle.fill")
Image(systemName: "heart.square")
Image(systemName: "heart.square.fill")
}
.font(.title)Six variations of the same icon, all perfectly sized to match. The filled versions are great for selected states.
But here’s where it gets useful — you can set a variant environment-wide:
VStack {
Image(systemName: "star")
Image(systemName: "heart")
Image(systemName: "bell")
}
.symbolVariant(.fill)All three icons automatically use their .fill version. Change .symbolVariant(.circle) and they all get circles.
You can also combine variants — .circle.fill is valid. But some combos don't exist. Like not every icon has a square variant. If you request a variant that doesn't exist, you just get the base symbol. No warning, no error. So test your icons.
Quick example — a settings row that adapts:
List {
Label("Wi-Fi", systemImage: "wifi")
Label("Cellular", systemImage: "antenna.radiowaves.left.and.right")
}
.symbolVariant(.circle.fill)
.symbolRenderingMode(.hierarchical)All icons get circular backgrounds with hierarchical shading. Two modifiers, consistent design system.
One gotcha: if you specify the full symbol name like "heart.fill" directly, the .symbolVariant() modifier won't override it. It only works when you use base names like "heart". So pick one approach and stick with it.
🔧 Font Weights & Sizing
Remember how SF Symbols act like text? That means they match font weights automatically, which is sick when it works and confusing when it doesn’t.
VStack(spacing:10) {
Label("Regular", systemImage: "folder")
Label("Bold", systemImage: "folder")
.fontWeight(.bold)
Label("Light", systemImage: "folder")
.fontWeight(.light)
}The folder icon gets thicker or thinner to match the text. No separate icon files needed. This is why SF Symbols look so cohesive in iOS — they’re literally designed to match San Francisco font weights.
For sizing, you’ve got a few options:
1\. Font sizes (recommended):
Image(systemName: "star")
.font(.largeTitle) // or .title, .headline, etc.2\. Custom font size:
Image(systemName: "star")
.font(.system(size: 64))3\. Image scale (relative sizing):
Image(systemName: "star")
.imageScale(.large) // .small, .medium, .largeHere’s the difference: .font() sets absolute size. .imageScale() makes the symbol bigger/smaller relative to surrounding text. Use .imageScale() in buttons and labels where you want the icon to match text size but be slightly larger:
Button(action: {}) {
Label("Delete", systemImage: "trash")
.imageScale(.large)
}The trash icon will be bigger than the “Delete” text but still proportional.
You can also set custom point sizes with specific weights:
Image(systemName: "folder.fill")
.font(.system(size: 44, weight: .light))44pt icon with light weight. Useful for hero icons or splash screens.
One thing that’s kinda annoying — you can’t use .frame() to size symbols precisely. Well, you can, but it doesn't resize the icon - it just adds padding. If you need pixel-perfect sizing, you gotta use .font(.system(size:)) and do the math.
// this adds space around the icon, doesn't resize it
Image(systemName: "star")
.frame(width: 50, height: 50)
.background(.red) // you'll see the icon is small with red padding
// this actually resizes the icon
Image(systemName: "star")
.font(.system(size: 50))
Image(systemName: "wifi", variableValue: signalStrength)Where signalStrength is a Double from 0.0 to 1.0. The wifi bars fill up based on the value. It's smooth, it's built-in, and it looks way better than swapping between wifi.1, wifi.2, wifi.3 icons manually.
✅ Wrap-up
SF Symbols are weird at first. They’re images but you treat them like text. They have thousands of options but you can’t tell if a variant exists until runtime. They scale automatically but sizing them feels backwards if you’re used to frames.
But once you get the mental model — they’re typographic icons — everything clicks.
Quick cheat sheet:
- Size with
.font(), not.frame() - Color with
.foregroundStyle(), optionally with multiple colors for palette mode - Use
.symbolVariant()for fill/circle/square instead of hardcoding names - Hierarchical mode for subtle depth, palette for custom colors
- Check the SF Symbols app when in doubt
The real power move? Combining these. Set rendering mode, variant, and colors at the parent view level and let all child symbols inherit the style. Your icons stay consistent without duplicating modifiers everywhere.
VStack {
// all symbols inherit these settings
Image(systemName: "star")
Image(systemName: "heart")
Image(systemName: "bell")
}
.symbolRenderingMode(.hierarchical)
.symbolVariant(.fill)
.foregroundStyle(.blue)
.font(.title2)One last thing — if you want to dive deeper, Apple actually has a session called “SF Symbols 5” from WWDC (and sessions for 4, 3, 2…). They’re surprisingly watchable. Or you know, just mess around in a playground until things make sense.
📺 Want to see all of this in action? Check out the Swift Pal YouTube channel at https://youtube.com/@swift-pal for video tutorials on SwiftUI and iOS development.
Now go add some icons to your app. They’ll look better than whatever PNGs you were using before, I promise.
🎉 Enjoyed this article? Your support means the world to me!
💬 Drop a comment below! I love hearing about your experiences and answering questions
🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal
💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal
Happy coding! 🚀
New articles, straight to your inbox.
No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.
Keep reading
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read