SwiftUI
iOS Development
User Interface Design
Mobile App Development
Apple Programming

SwiftUI - Multiple Buttons in a List row

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

When you place multiple Button views inside a SwiftUI List row, tapping anywhere on the row triggers every button action at once. This happens because List wraps the entire row in an implicit tap gesture. Fixing this behavior requires understanding how SwiftUI resolves gesture conflicts within list rows and applying the correct button style or gesture modifiers.

The Default Problem

By default, SwiftUI treats the entire List row as a single tappable area. If you place two buttons in a row, both actions fire regardless of which button the user tapped.

swift
1struct ContentView: View {
2    var body: some View {
3        List {
4            HStack {
5                Button("Edit") {
6                    print("Edit tapped")
7                }
8                Spacer()
9                Button("Delete") {
10                    print("Delete tapped")
11                }
12            }
13        }
14    }
15}

Tapping anywhere on this row prints both "Edit tapped" and "Delete tapped". This is not the expected behavior for independent buttons.

The .buttonStyle(.borderless) Fix

The most common and recommended solution is to apply .buttonStyle(.borderless) to each button. This tells SwiftUI to confine the tap target to the button content rather than the full row.

swift
1struct ContentView: View {
2    var body: some View {
3        List {
4            HStack {
5                Button("Edit") {
6                    print("Edit tapped")
7                }
8                .buttonStyle(.borderless)
9
10                Spacer()
11
12                Button("Delete") {
13                    print("Delete tapped")
14                }
15                .buttonStyle(.borderless)
16            }
17        }
18    }
19}

Now each button responds independently. The .borderless style removes the default list row tap behavior and scopes the hit area to the button label. You can also apply the modifier to the HStack or the row itself, and it propagates to all buttons within.

Using .buttonStyle(.plain)

An alternative is .buttonStyle(.plain), which also separates button tap targets. The difference is visual: .plain removes all button styling including the default accent color, rendering the label in the primary text color.

swift
1HStack {
2    Button(action: { print("Approve") }) {
3        Label("Approve", systemImage: "checkmark.circle")
4            .foregroundColor(.green)
5    }
6    .buttonStyle(.plain)
7
8    Button(action: { print("Reject") }) {
9        Label("Reject", systemImage: "xmark.circle")
10            .foregroundColor(.red)
11    }
12    .buttonStyle(.plain)
13}

Use .plain when you want full control over the button appearance and do not want SwiftUI to apply any default highlight or tint behavior.

Using onTapGesture Instead of Button

Another approach replaces Button with Text or any view combined with onTapGesture. Since onTapGesture is not a button, SwiftUI does not apply the row-wide tap behavior.

swift
1struct ContentView: View {
2    var body: some View {
3        List {
4            HStack {
5                Text("Edit")
6                    .foregroundColor(.blue)
7                    .onTapGesture {
8                        print("Edit tapped")
9                    }
10
11                Spacer()
12
13                Text("Delete")
14                    .foregroundColor(.red)
15                    .onTapGesture {
16                        print("Delete tapped")
17                    }
18            }
19        }
20    }
21}

This works but has a trade-off: you lose the built-in button accessibility traits. Screen readers will not announce these views as buttons unless you manually add .accessibilityAddTraits(.isButton).

A common scenario is having a NavigationLink row that also contains action buttons, such as a favorite or delete button. The NavigationLink consumes the entire row tap, making buttons inside it non-functional.

The solution is to hide the NavigationLink and use a separate tap gesture for navigation:

swift
1struct ItemRow: View {
2    let item: String
3    @State private var isFavorite = false
4
5    var body: some View {
6        ZStack(alignment: .leading) {
7            NavigationLink(destination: DetailView(item: item)) {
8                EmptyView()
9            }
10            .opacity(0)
11
12            HStack {
13                Text(item)
14
15                Spacer()
16
17                Button(action: {
18                    isFavorite.toggle()
19                }) {
20                    Image(systemName: isFavorite ? "star.fill" : "star")
21                        .foregroundColor(.yellow)
22                }
23                .buttonStyle(.borderless)
24            }
25        }
26    }
27}

The hidden NavigationLink still provides navigation when the row is tapped outside the button area, while the star button toggles independently.

Handling Multiple Buttons with Custom Styles

For rows with several buttons that need distinct visual treatments, create a reusable button style:

swift
1struct RowActionButtonStyle: ButtonStyle {
2    let color: Color
3
4    func makeBody(configuration: Configuration) -> some View {
5        configuration.label
6            .padding(.horizontal, 12)
7            .padding(.vertical, 6)
8            .background(color.opacity(configuration.isPressed ? 0.3 : 0.15))
9            .cornerRadius(8)
10    }
11}
12
13// Usage in a List row
14HStack(spacing: 12) {
15    Button("Reply") { print("Reply") }
16        .buttonStyle(RowActionButtonStyle(color: .blue))
17
18    Button("Forward") { print("Forward") }
19        .buttonStyle(RowActionButtonStyle(color: .green))
20
21    Button("Archive") { print("Archive") }
22        .buttonStyle(RowActionButtonStyle(color: .orange))
23}

Custom ButtonStyle implementations automatically scope the tap target to each button, so they work correctly inside list rows without additional modifiers.

Common Pitfalls

  • Forgetting .buttonStyle on all buttons: If you apply .borderless to one button but not the other, the unstyled button still participates in the row-wide tap.
  • Losing accessibility with onTapGesture: Replacing Button with onTapGesture removes the button accessibility trait. Always add .accessibilityAddTraits(.isButton) when using this workaround.
  • Applying .buttonStyle to the List instead of the row: Applying the style at the List level can have unexpected results on different OS versions. Apply it at the row or button level for consistent behavior.
  • Using .automatic button style: The default .automatic style in a List context delegates to the list row style, which makes the entire row tappable. Always specify an explicit style.
  • Ignoring platform differences: The .borderless fix works on iOS but may behave differently on macOS or watchOS. Test on each target platform.

Summary

  • SwiftUI List rows treat the entire row as a single tap target by default, causing all buttons to fire simultaneously.
  • Apply .buttonStyle(.borderless) or .buttonStyle(.plain) to each button to make tap targets independent.
  • Use onTapGesture as an alternative, but add accessibility traits manually to maintain screen reader support.
  • When combining NavigationLink with action buttons, hide the link with .opacity(0) and use .buttonStyle(.borderless) on the buttons.
  • Custom ButtonStyle implementations automatically scope tap targets, making them a clean solution for rows with multiple styled actions.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.