Skip to content

Button styles, border shapes, and control sizes in SwiftUI on iOS 15 | SwiftUI Bootcamp #57

Before iOS 15, creating a styled button in SwiftUI required manually building a custom label with .background, .cornerRadius, .padding, and .foregroundColor. iOS 15 introduced a rich system of built-in button styles, border shapes, and control sizes that produce native-looking buttons in a few modifiers. After this lesson you'll be able to style buttons quickly, understand the modifier hierarchy, and know when to use each style.

What You'll Learn

  • How .buttonStyle(.borderedProminent), .bordered, .borderless, and .plain differ visually and semantically
  • How .buttonBorderShape() customizes the shape of bordered buttons (capsule, rounded rectangle with radius)
  • How .controlSize(.large), .regular, .small, and .mini scale buttons to different use contexts
  • How modifier order affects which style takes effect when multiple .buttonStyle modifiers are applied

Mental Model

Think of iOS 15 button modifiers as three independent dials on the same control panel. The first dial is style.borderedProminent is the primary action color (tinted fill), .bordered is a subtler outlined look, .plain is invisible background, .borderless removes the border. The second dial is shape — capsule-pill vs. rounded rectangle with a specific radius. The third dial is size — the overall scale of the button from mini through large.

Turn these three dials independently to compose exactly the button you need. .borderedProminent + .buttonBorderShape(.capsule) + .controlSize(.large) gives you a large tinted pill button — the kind used for primary CTAs in onboarding flows. .bordered + .controlSize(.small) gives you a compact outline button suitable for filter chips.

Detailed Explanation

.buttonStyle(.borderedProminent) is the most visually prominent button style — it fills the button's background with the current tint color (blue by default, customizable with .tint()). Use this for the single most important action on a screen: "Sign Up", "Buy Now", "Continue". There should typically be only one .borderedProminent button per screen.

.buttonStyle(.bordered) renders a bordered button with a subtle background fill using the tint color at low opacity. It's appropriate for secondary actions or multiple equal-weight options presented together.

.buttonStyle(.plain) and .buttonStyle(.borderless) both produce invisible backgrounds. .plain preserves the label content's own color and removes all system styling. .borderless is similar but specifically marks the button as having no border — useful when you want the system to treat it as a borderless control (affects how it appears inside forms and lists).

.buttonBorderShape(.roundedRectangle(radius: 20)) overrides the default corner radius for bordered styles. You can also use .buttonBorderShape(.capsule) for a fully pill-shaped button. Note: this modifier only has a visual effect when a bordered style is applied — it has no effect on .plain or .borderless.

.controlSize() scales the entire button — both padding and font size — uniformly across a predefined set of sizes: .large, .regular, .small, .mini. This is preferred over manually specifying font sizes and padding because the system ensures the button stays accessible and consistent across device sizes and dynamic type settings.

Code Structure

ButtonStylesBootcamp.swift presents five buttons stacked vertically, each demonstrating a different control size with the same .borderedProminent style, plus the first button demonstrating .buttonBorderShape(.roundedRectangle(radius: 20)). The .preferredColorScheme(.light) in the preview makes the filled styles clearly visible.

Complete Code

ButtonStylesBootcamp.swift

swift
import SwiftUI

struct ButtonStylesBootcamp: View {
    var body: some View {
        VStack {
            
            Button {
                
            } label: {
                Text("Button Title")
                    .frame(height: 55)
                    .frame(maxWidth: .infinity) // stretches the label to full width so the button fills the container
            }
            .buttonStyle(.borderedProminent)                    // tinted fill background: the "primary action" style
            .buttonBorderShape(.roundedRectangle(radius: 20))  // overrides corner radius to 20pt
            .controlSize(.large)                               // largest size preset; appropriate for primary CTAs

            
            Button("Button Title") {
                
            }
            .frame(height: 55)
            .frame(maxWidth: .infinity)
            .buttonStyle(.plain)             // applied first — no visible effect because...
            .buttonStyle(.borderedProminent) // ...this second .buttonStyle overrides the first (last wins)
            .controlSize(.large)

            Button("Button Title") {
                
            }
            .frame(height: 55)
            .frame(maxWidth: .infinity)
            .controlSize(.regular)           // medium size: appropriate for most in-content buttons
            .buttonStyle(.bordered)          // subtle tinted background with border — secondary action style
            .buttonStyle(.borderedProminent) // overrides .bordered; last .buttonStyle modifier wins

            Button("Button Title") {
                
            }
            .frame(height: 55)
            .frame(maxWidth: .infinity)
            .buttonStyle(.borderedProminent)
            .controlSize(.small)             // compact size: good for tags, filter chips, or in-list actions

            Button("Button Title") {
                
            }
            .frame(height: 55)
            .frame(maxWidth: .infinity)
//            .buttonStyle(.borderless)      // alternative: removes border but preserves tint color on label
            .buttonStyle(.borderedProminent)
            .controlSize(.mini)             // smallest size: for very compact UI contexts
            
        }
        .padding()
    }
}

struct ButtonStylesBootcamp_Previews: PreviewProvider {
    static var previews: some View {
        ButtonStylesBootcamp()
            .preferredColorScheme(.light) // forces light mode to make the tinted fill clearly visible in canvas
            
    }
}

Code Walkthrough

  1. First button: full label form with label: closure — Using the label: closure form lets you apply modifiers directly to the label content (Text), including .frame(height: 55) and .frame(maxWidth: .infinity). This makes the button's visible tap area fill the available width without relying on the style to do it.

  2. .buttonStyle(.borderedProminent) — The primary action style. The button background fills with the current tint color (app accent color). This is the iOS convention for the most important action on a screen — equivalent to a UIKit button with a solid primary color background.

  3. .buttonBorderShape(.roundedRectangle(radius: 20)) — Overrides the corner radius to 20pt. Without this, .borderedProminent uses a default corner radius from the system. .buttonBorderShape(.capsule) would make fully pill-shaped buttons. This modifier only applies to bordered styles.

  4. .controlSize(.large) — Scales the button's internal padding and font size to the "large" preset. Large is appropriate for primary full-width buttons like sign-up or purchase CTAs. The system automatically adjusts for Dynamic Type scaling.

  5. Second and third buttons: duplicate .buttonStyle modifiers — These demonstrate that SwiftUI applies the last .buttonStyle modifier in the chain. .buttonStyle(.plain) followed by .buttonStyle(.borderedProminent) produces a .borderedProminent button — the second declaration wins. This is a quirk to be aware of when debugging unexpected button appearances.

  6. .controlSize(.small) and .controlSize(.mini) — The fourth and fifth buttons show the visual range of the size system. .small produces a compact button suitable for secondary actions or filter tags. .mini is the most compact, useful in dense UIs like toolbars or list cells.

  7. .preferredColorScheme(.light) in preview — Forces the preview to render in light mode. .borderedProminent appears most clearly against a light background. Testing in both .light and .dark is important — bordered styles adapt their fill color to both appearances.

Common Mistakes

Mistake: Applying multiple .buttonStyle modifiers and expecting them to combine
SwiftUI does not combine button styles. The last .buttonStyle applied in the modifier chain takes full effect and overrides all previous ones. If you want a button with combined traits, create a custom ButtonStyle struct conforming to ButtonStyle protocol.

Mistake: Using .buttonBorderShape without a bordered style and wondering why nothing changes
.buttonBorderShape only has a visual effect on .bordered and .borderedProminent styles. Applied to .plain or .borderless, it does nothing. Always pair border shape customizations with a bordered style.

Mistake: Forgetting to add .frame(maxWidth: .infinity) to the button label and getting a tiny tap target
A Button with only text content is sized to the text's width. Without .frame(maxWidth: .infinity) the button doesn't stretch to fill its container, resulting in a small, hard-to-tap button that looks misaligned in a full-width layout. Add the frame modifier to the label for full-width buttons.

Key Takeaways

  • .buttonStyle(.borderedProminent) is the iOS 15 way to create a primary tinted-fill button without manual background/color/radius modifiers — use it for the single most important action on each screen.
  • .controlSize(.large/.regular/.small/.mini) scales buttons uniformly and respects Dynamic Type — prefer it over manually specifying fonts and padding for accessible, consistent button sizing.
  • When chaining multiple .buttonStyle modifiers, only the last one applies — this is a common source of confusion; use a single .buttonStyle per button.

Last updated: June 27, 2026

Released under the MIT License.