Why this happens
In UIKit, UIButton provides state-specific setters for content properties like setTitleColor(_:for:) and setImage(_:for:). However, there is no corresponding setBackgroundColor(_:for:) method in the standard API. The backgroundColor property is inherited directly from UIView, which operates without knowledge of UIControl.State.
When a user presses a button, UIKit toggles its internal isHighlighted flag to true. By default, UIKit applies an internal dimming effect or adjusts the tint of titles and images, but it leaves UIView.backgroundColor untouched. Assigning button.backgroundColor = .systemRed only updates the static layer color. It does not instruct the button to revert or shift colors when the user touches down or lifts their finger.
To achieve custom pressed states, you must hook into the state change lifecycle. You can do this by using modern button configurations, overriding the button’s highlighted state, or applying stateful background images.
How to fix it
1. Use UIButton.Configuration and configurationUpdateHandler (iOS 15+)
On modern iOS releases, Apple recommends using UIButton.Configuration. A button’s configurationUpdateHandler automatically executes whenever the control state transitions between normal, highlighted, selected, or disabled.
import UIKit
let button = UIButton(type: .system)
var config = UIButton.Configuration.filled()
config.title = "Tap Me"
button.configuration = config
button.configurationUpdateHandler = { button in
var updatedConfig = button.configuration
if button.isHighlighted {
updatedConfig?.background.backgroundColor = .systemRed
} else {
updatedConfig?.background.backgroundColor = .systemBlue
}
button.configuration = updatedConfig
}
This approach eliminates manual touch tracking, respects accessibility adjustments, and automatically restores the default background color when the touch ends.
2. Subclass UIButton and override isHighlighted
If your project relies on custom button styles without UIButton.Configuration, create a reusable UIButton subclass. Overriding isHighlighted allows you to observe when UIKit changes the touch state.
import UIKit
class HighlightableButton: UIButton {
var normalBackgroundColor: UIColor = .systemBlue {
didSet { updateAppearance() }
}
var highlightedBackgroundColor: UIColor = .systemRed {
didSet { updateAppearance() }
}
override var isHighlighted: Bool {
didSet {
updateAppearance()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
updateAppearance()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
updateAppearance()
}
private func updateAppearance() {
backgroundColor = isHighlighted ? highlightedBackgroundColor : normalBackgroundColor
}
}
You can use this subclass in Interface Builder or programmatically:
let customButton = HighlightableButton(frame: CGRect(x: 50, y: 100, width: 200, height: 50))
customButton.normalBackgroundColor = .systemGreen
customButton.highlightedBackgroundColor = .systemOrange
customButton.setTitle("Submit", for: .normal)
3. Generate a 1×1 UIImage for setBackgroundImage(_:for:)
UIButton includes native support for state-driven background images through setBackgroundImage(_:for:). By converting a solid UIColor into a 1×1 pixel UIImage, you can use UIKit’s built-in state engine without subclassing.
Add a helper extension to UIImage:
import UIKit
extension UIImage {
static func from(color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
let renderer = UIGraphicsImageRenderer(size: rect.size)
return renderer.image { context in
color.setFill()
context.fill(rect)
}
}
}
Apply the rendered images to your button states:
let button = UIButton(type: .custom)
button.setTitle("Confirm", for: .normal)
button.setTitleColor(.white, for: .normal)
// Set images for normal and highlighted states
button.setBackgroundImage(.from(color: .systemBlue), for: .normal)
button.setBackgroundImage(.from(color: .systemIndigo), for: .highlighted)
// Clip to bounds if rounded corners are applied
button.layer.cornerRadius = 8
button.clipsToBounds = true
UIKit stretches the 1×1 pixel image across the frame and handles state transitions smoothly without extra listener logic.
4. Bind UIControl touch events manually
If you need to change the background on an existing button instance without subclassing or using configurations, register target-action handlers for user touch events.
You must listen to both completion scenarios—touchUpInside and touchUpOutside—as well as touch cancellations to prevent the button from getting stuck in its highlighted state:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var actionButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
actionButton.backgroundColor = .systemBlue
actionButton.addTarget(self, action: #selector(buttonTouchDown), for: .touchDown)
actionButton.addTarget(self, action: #selector(buttonTouchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel])
}
@objc private func buttonTouchDown(_ sender: UIButton) {
sender.backgroundColor = .systemRed
}
@objc private func buttonTouchUp(_ sender: UIButton) {
sender.backgroundColor = .systemBlue
}
}
How to prevent it
- Favor UIButton.Configuration: Adopt
configurationUpdateHandlerfor new iOS projects. It consolidates layout, fonts, and state-based styling in one place. - Avoid raw backgroundColor changes in IBAction handlers: Setting
backgroundColorinside standard.touchUpInsideaction methods will not produce a pressed highlight effect because the touch has already finished. - Handle all exit events: If tracking touch states manually, always register handlers for
.touchUpOutsideand.touchCancelto avoid leaving the button permanently highlighted when a user drags their finger away. - Enable clipsToBounds with corner radii: When using
setBackgroundImage(_:for:)with rounded corners, ensureclipsToBounds = trueis set on the button so the background image does not leak past the corner radius.
FAQ
Can I set a highlighted background color directly in a Storyboard?
No. Interface Builder only exposes a single static backgroundColor inherited from UIView. To assign a state-specific color in Storyboards, supply background images for each state under the button’s Attributes Inspector or use custom @IBInspectable properties on a UIButton subclass.
Why does the button stay highlighted after dragging my finger away?
This occurs when you update colors manually via .touchDown and .touchUpInside without handling .touchUpOutside or .touchCancel. Dragging a touch outside the button bounds cancels the inside trigger, leaving the button in the pressed color state.
Does UIButton.Configuration override the traditional backgroundColor property?
Yes. When you assign a UIButton.Configuration, UIKit ignores direct modifications to button.backgroundColor. You must modify configuration.background.backgroundColor through configurationUpdateHandler instead.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
