Buttons

The most common way to create a Button is with a text label and a single action:

Button("Download", action: .navigate(to: "/download"))

To trigger more than one action, use the action-builder initializer:

Button("Submit", role: .submit) {
   SubmitAction()
   ResetFormAction()
}

Custom Actions

Define your own actions by conforming to the Action protocol. Actions compile directly to JavaScript and execute client-side:

struct LogAction: Action {
   func compile() -> String {
       "console.log('Button pressed')"
   }
}

Once defined, custom actions work anywhere a button accepts an action:

Button("Log", action: LogAction())

Button Icons

Buttons can also include system icons. Control where icons appear relative to the button label via buttonIconPlacement(_:):

HStack(spacing: 10) {
   ForEach(ButtonIconPlacement.allCases) { placement in
      Button("Settings", systemImage: "gear-fill", action: OpenSettings())
         .buttonIconPlacement(placement)
   }
}

Modifiers

Buttons support various modifiers to customize their appearance and behavior, including:

tint(_:) — Control button color

buttonSizing(_:) — Control how the button occupies space (.fitted or .flexible)

buttonShape(_:) — Set corner radius and shape (.roundedRect, .capsule, .rect, .circle)

controlSize(_:) — Adjust padding, font size, and overall scale


Primitive Button Styles

For quick styling without creating custom ButtonStyle types, use primitive button styles:

let styles = PrimitiveButtonStyle.allCases
let sizes = ControlSize.allCases
let shapes = ButtonShape.allCases
let colors = [Color.blue, .indigo, .purple, .pink, .red]

ForEach(styles.indices) { index in
   let style = styles[index]
   let size = sizes[index % sizes.count]
   let shape = shapes[index % shapes.count]
   let color = colors[index % tints.count]

   Button("Tap!", action: SubmitAction())
      .buttonStyle(style)
      .controlSize(size)
      .buttonShape(shape)
      .tint(colors[index % tints.count])
      .margin(.trailing, 20)
}

Available styles:

  • .plain — No background or border, appears as regular text
  • .borderless — Like plain but with accent color and tap feedback
  • .bordered — Border with transparent background
  • .filled — Solid background with border
  • .filledProminent — Enhanced filled style with larger padding, bold font, and shadow for primary actions

For more complex, state-aware styling needs, create a custom ButtonStyle instead (see below).


Button Styles

For reusable, state-aware styling, use the ButtonStyle protocol:

struct PrimaryButtonStyle: ButtonStyle {
   func style(content: Content, phase: Phase) -> Content {
       switch phase {
       case .initial:
           content
               .background(.red)
               .foregroundStyle(.white)
               .cornerRadius(12)
       case .hovered:
           content.background(.pink)
       case .pressed:
           content.background(.orange)
       case .disabled:
           content.background(.gray)
       }
   }
}

To apply it:

Button("Submit", action: .submit())
   .buttonStyle(PrimaryButtonStyle())