Fix: UIButton Background Color Fails to Change When Highlighted

Fix: UIButton Background Color Fails to Change When Highlighted

Written by

in

Quick answer: To change a UIButton background color when highlighted, use UIButton.Configuration with a configurationUpdateHandler on iOS 15 and later. For older custom setups, subclass UIButton and override the isHighlighted property to update the backgroundColor whenever the state changes.

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 configurationUpdateHandler for new iOS projects. It consolidates layout, fonts, and state-based styling in one place.
  • Avoid raw backgroundColor changes in IBAction handlers: Setting backgroundColor inside standard .touchUpInside action 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 .touchUpOutside and .touchCancel to 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, ensure clipsToBounds = true is 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.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.