SwiftUI Button: Styles, Roles & Custom ButtonStyle
Everything you need to build production buttons in SwiftUI — from the built-in .bordered styles to fully custom ButtonStyle types, async actions, and iOS 26 Liquid Glass.
A SwiftUI Button pairs an action closure with a label view: Button("Save") { save() }. Apply visual treatment with .buttonStyle(.bordered), .borderedProminent, or .plain, and communicate intent with .buttonStyle roles like Button(role: .destructive). For fully custom looks, conform a type to the ButtonStyle protocol and read configuration.isPressed. On iOS 26, .buttonStyle(.glass) and the .glassEffect() modifier render Apple's Liquid Glass material.
Roles: destructive and cancel
A button role tells SwiftUI the semantic meaning of the action so it can style and place it correctly. Button(role: .destructive) renders red inside menus and confirmation dialogs, and .cancel gets special treatment in dialogs and alerts. Roles are not just cosmetic — VoiceOver announces them, and confirmationDialog uses them to order and emphasize choices. Use them instead of hardcoding a red tint on a delete button.
struct DeleteRow: View {
@State private var confirming = false
var body: some View {
Button("Delete Account", role: .destructive) {
confirming = true
}
.confirmationDialog("Delete this account?",
isPresented: $confirming,
titleVisibility: .visible) {
Button("Delete", role: .destructive) { deleteAccount() }
Button("Cancel", role: .cancel) { }
}
}
}Async actions, disabled state, and iOS 26 Liquid Glass
A button action is synchronous, but you often want to await work. Wrap the call in a Task and drive a loading flag so you can disable the button and show a ProgressView while it runs. Use .disabled(_:) to gate the button on validation, and it will dim and stop responding automatically. On iOS 26, Apple introduced Liquid Glass — apply .buttonStyle(.glass) for a system glass button, or wrap custom content in .glassEffect() to float it above scrolling content. Gate glass code behind an availability check so iOS 16–25 users fall back gracefully.
struct SubmitButton: View {
@State private var isLoading = false
let isValid: Bool
var body: some View {
Button {
Task {
isLoading = true
defer { isLoading = false }
try? await submit()
}
} label: {
if isLoading { ProgressView() } else { Text("Submit") }
}
.disabled(!isValid || isLoading)
.modifier(GlassIfAvailable())
}
}
struct GlassIfAvailable: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 26, *) {
content.buttonStyle(.glass)
} else {
content.buttonStyle(.borderedProminent)
}
}
}Stop rebuilding the same button
The Swift Kit ships a tokenized PrimaryButton and pressable ButtonStyle wired into a light/dark design system, so your CTAs look consistent from day one.
How to build a custom primary button in SwiftUI
Create a reusable, on-brand primary button you can drop anywhere in your app.
- 1
Define the ButtonStyle
Create a struct conforming to ButtonStyle and implement makeBody(configuration:).
struct PrimaryButtonStyle: ButtonStyle { /* ... */ } - 2
Read isPressed
Use configuration.isPressed to add a scale or opacity change for tactile feedback.
.scaleEffect(configuration.isPressed ? 0.97 : 1) - 3
Apply and reuse
Attach it with .buttonStyle(PrimaryButtonStyle()) on any Button in the app.
Button("Save") { save() }.buttonStyle(PrimaryButtonStyle())
Frequently Asked Questions
How do I make a filled primary button in SwiftUI?
How do I add an async action to a SwiftUI Button?
How do I make a destructive delete button in SwiftUI?
How do I detect the pressed state of a SwiftUI Button?
How do I make an iOS 26 Liquid Glass button in SwiftUI?
Keep exploring
Ship faster with a real design system
The Swift Kit is a $99 one-time SwiftUI boilerplate with buttons, paywalls, auth, and AI already wired up — lifetime updates and a 14-day refund.
Get The Swift Kit — $99One-time purchase · Lifetime updates · 14-day refund