Why this happens
Setting navigationBar.barTintColor or navigationBar.backgroundColor directly on a UINavigationController often produces no visible change, causes the bar to render with an unexpected translucent tint, or causes the color to snap back to default as soon as a UIScrollView or UITableView scrolls beneath it.
UIKit uses the UINavigationBarAppearance API to manage bar styling across different presentation states. A navigation bar evaluates distinct appearance configurations depending on whether content is scrolling under it (scrollEdgeAppearance), sitting at a resting position (standardAppearance), or rendered in a compact horizontal size class (compactAppearance).
Starting in iOS 13 and expanded in iOS 15, UIKit defaults scrollEdgeAppearance to a transparent background style when content sits at the top edge of a scrollable view. When you assign colors using legacy properties such as barTintColor, UIKit either ignores the assignment or fails to propagate it across all active scroll states. Resolving the issue requires defining an explicit appearance object and assigning it to all relevant appearance slots.
How to fix it
1. Configure standardAppearance and scrollEdgeAppearance with UINavigationBarAppearance
The standard way to apply a background color to a UINavigationBar in modern UIKit is through UINavigationBarAppearance. You must assign your configuration to both standardAppearance and scrollEdgeAppearance to ensure consistency before and after scrolling.
Place this configuration in your custom UINavigationController subclass, or inside viewDidLoad of the root view controller managing the navigation stack:
import UIKit
class CustomNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
configureNavigationBarAppearance()
}
private func configureNavigationBarAppearance() {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor.systemTeal
// Configure title text attributes
appearance.titleTextAttributes = [
.foregroundColor: UIColor.white,
.font: UIFont.boldSystemFont(ofSize: 18)
]
appearance.largeTitleTextAttributes = [
.foregroundColor: UIColor.white
]
// Apply to standard, compact, and scroll edge states
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
navigationBar.compactAppearance = appearance
// Tint color controls interactive items like back buttons
navigationBar.tintColor = .white
}
}
Calling configureWithOpaqueBackground() strips the default dynamic blur effect and ensures your chosen backgroundColor renders as an opaque surface.
2. Set the global appearance proxy in the AppDelegate or SceneDelegate
If your application uses the same navigation bar style across all screens, configure the global proxy via UINavigationBar.appearance(). This centralizes navigation styling and applies the color to every UINavigationBar created by your app.
Add this setup to application(_:didFinishLaunchingWithOptions:) in your AppDelegate or inside scene(_:willConnectTo:options:) in your SceneDelegate:
import UIKit
func setupGlobalNavigationBarTheme() {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor(named: "BrandPrimary") ?? .systemBlue
appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
let navBarProxy = UINavigationBar.appearance()
navBarProxy.standardAppearance = appearance
navBarProxy.scrollEdgeAppearance = appearance
navBarProxy.compactAppearance = appearance
navBarProxy.tintColor = .white
}
Global proxy updates take effect on newly instantiated view hierarchies. If you invoke this method after views have loaded, trigger a layout update or apply the appearance directly to the active navigation bar instance.
3. Handle SwiftUI navigation bar background colors
If you are encountering this issue in a mixed UIKit and SwiftUI app or a pure SwiftUI view hierarchy using NavigationStack, attempting to style the navigation bar by querying UINavigationBar.appearance() can produce timing glitches. Instead, use native SwiftUI modifiers on the root view inside the stack:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
List(1...20, id: \.self) { item in
Text("Item \(item)")
}
.navigationTitle("Inbox")
.toolbarBackground(Color.teal, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
}
}
}
Using .toolbarBackground(.visible, for: .navigationBar) prevents SwiftUI from collapsing the bar background into transparent material when content rests at the top edge.
4. Configure the appearance object in Xcode Storyboards
If your interface relies on Storyboards or XIB files, you can configure these appearance layers directly in Interface Builder without writing code:
- Open your Storyboard and select the
Navigation Barinside yourNavigation Controller. - In the Attributes Inspector, locate the Scroll Edge Appearance checkbox and ensure it is checked.
- Expand the Standard Appearance section and set Background to your desired color.
- Expand the Scroll Edge Appearance section and set its Background color to match the Standard Appearance background.
- Change the Tint property in the Navigation Bar attributes to style bar button items and back buttons.
How to prevent it
- Avoid modifying
navigationBar.barTintColorornavigationBar.backgroundColordirectly in code targeting modern iOS versions. - Always assign customized
UINavigationBarAppearanceinstances to bothstandardAppearanceandscrollEdgeAppearancesimultaneously. - Set
navigationBar.isTranslucent = falseonly if you specifically want to disable translucent content underlapping without usingUINavigationBarAppearance. - Test views containing
UITableViewandUICollectionViewby scrolling up and down in the Simulator to confirm the bar color does not flicker or reset when crossing scroll boundaries.
FAQ
Why does my bar background turn white or transparent when scrolling begins?
This happens when you assign your custom configuration to scrollEdgeAppearance but leave standardAppearance set to the system default, or vice versa. Assign the appearance instance to both properties to maintain uniform color throughout the scroll lifecycle.
What is the difference between tintColor and backgroundColor on UINavigationBar?
The backgroundColor (or appearance backgroundColor) controls the surface color of the bar itself. The tintColor controls the interactive elements resting inside the bar, such as bar button items, back button arrows, and action icons.
How do I make the status bar text white when using a dark navigation bar color?
When using UINavigationBarAppearance, set navigationController.navigationBar.barStyle = .black, or override preferredStatusBarStyle in your view controller to return .lightContent. Ensure UIViewControllerBasedStatusBarAppearance is set to YES in your project’s Info.plist.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
