To make blank space inside a SwiftUI row clickable on Mac, expand the row’s content to the available width and apply a rectangular content shape. For a row that performs an action, put both modifiers inside a Button label.

This tutorial builds a small macOS app whose rows contain synthetic shapes and text. Clicking a shape, its label, or the empty space beside it runs the same row action. Space outside the row remains outside that action’s target.

Why does the text respond while the blank space does not?

Layout and hit testing answer different questions. A frame determines how a view occupies its proposed space; a content shape defines the region used for hit testing. Apple documents these separately in its flexible frame modifier and content-shape modifier.

For example, a row might appear wide because its container fills a window, while the interaction is attached only to a short text view. Similarly, adding empty layout space does not explicitly specify that the whole row should participate in hit testing. The practical fix is to establish the intended row bounds first, then give that region an interaction shape.

Use .frame(maxWidth: .infinity, alignment: .leading) to let the label expand when its parent proposes additional width. Then apply .contentShape(.interaction, Rectangle()) to that expanded label. Apple defines the interaction content-shape kind specifically for hit testing and accessibility.

This does not make the row infinitely wide. The frame still participates in its parent’s layout proposal. A narrow parent produces a narrow row, and a content shape cannot supply missing layout space. The combination addresses two requirements: a sufficiently wide label and an explicit hit-test region.

Build a complete row-button example

Create a macOS app project in Xcode using SwiftUI and Swift. Set its deployment target to macOS 12 or later for the explicit .interaction content-shape overload, which Apple lists as available starting in macOS 12. Replace the generated app declaration and content view with the single-file program below; keep only one @main app declaration. See Apple’s content-shape overload documentation for availability.

The example uses a Button because each row initiates an action. Apple recommends buttons for action-performing interface elements and explicitly supports arbitrary views as button labels. Its tap-gesture documentation also recommends a customized button when a gesture-based control would be functionally equivalent to one.

import SwiftUI

@main
struct ClickableRowsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct SampleItem: Identifiable {
    let id: Int
    let title: String
    let color: Color
}

struct ContentView: View {
    private let items = [
        SampleItem(id: 1, title: "Blue square", color: .blue),
        SampleItem(id: 2, title: "Orange square", color: .orange),
        SampleItem(id: 3, title: "Purple square", color: .purple)
    ]

    @State private var lastActivated = "None"
    @State private var activationCount = 0

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Choose a shape")
                .font(.headline)

            ScrollView {
                VStack(spacing: 8) {
                    ForEach(items) { item in
                        Button {
                            lastActivated = item.title
                            activationCount += 1
                        } label: {
                            HStack(spacing: 12) {
                                Rectangle()
                                    .fill(item.color)
                                    .frame(width: 24, height: 24)
                                Text(item.title)
                            }
                            .padding(.horizontal, 12)
                            .padding(.vertical, 10)
                            .frame(maxWidth: .infinity,
                                   alignment: .leading)
                            .contentShape(.interaction, Rectangle())
                        }
                        .buttonStyle(.plain)
                        .background(Color.gray.opacity(0.12))
                    }
                }
            }

            Text("Last activated: \(lastActivated)")
            Text("Activations: \(activationCount)")
        }
        .padding(16)
        .frame(minWidth: 360, minHeight: 280)
    }
}

All items and interaction state stay in memory. Each item has a fixed integer identity, consistent with Swift’s Identifiable protocol, which represents stable identity. The parent view owns the activation state, following Apple’s guidance to keep local State private in the view that needs to own it.

Run the app and click the blank area to the right of any title. The activation count should increase once, and the status should name that row. The gray background makes the intended row area visible during this check; the explicit content shape defines the interaction region.

This program was inspected against the retrieved API documentation but was not compiled or run here because a Swift toolchain was unavailable. Verify its pointer and keyboard behavior in your macOS target before integrating it.

Where should the frame, padding, and content shape go?

In this example, the order is deliberate: build the label content, add its internal padding, expand the resulting label, and define the interaction shape over that region. All these operations occur inside the button’s label closure. Apple documents that modifier order matters.

Putting padding inside this shaped label includes the padded region in the intended target. Putting padding outside the button instead creates surrounding layout space; do not assume that space belongs to the button’s interactive label. Likewise, expanding only an outer container does not express the same label geometry as expanding the content inside the button.

The .buttonStyle(.plain) modifier keeps the example visually simple. Apple describes the plain button style as undecorated while idle, with possible effects for pressed, focused, or enabled states. It is a styling choice, not a substitute for defining the label’s width and content shape.

For a deployment target earlier than macOS 12, replace .contentShape(.interaction, Rectangle()) with .contentShape(Rectangle()). Apple documents the older overload as defining the hit-testing shape and lists availability starting in macOS 10.15. This replacement changes the content-shape API requirement; check your app’s other APIs separately.

Should a row use a button, list selection, or a gesture?

Choose the interaction according to what clicking the row means. Making blank space respond should preserve that meaning and the ownership of any controls within the row.

Reader-facing behavior Appropriate starting point Boundary to preserve
The whole row performs one action A Button with an expanded, shaped label Keep one action owner for the row
The row represents the current selection A List with a selection binding Let the list own selection behavior
A custom view needs a distinct click gesture A shaped view with onTapGesture Attach the gesture to the intended region
The row contains several independent controls Separate sibling controls Give each control its own target

For list selection, use the container’s selection model before adding a row-wide click handler. Apple’s List documentation describes a binding to an optional item identifier for single selection and a set of identifiers for multiple selection. A button that performs an action and a list row that becomes selected are different interaction requirements.

A gesture remains appropriate when you specifically need gesture recognition on a custom view. Apply the same geometry sequence—padding and frame, then content shape—before attaching onTapGesture. Apple documents its count parameter as the number of taps or clicks required. A content shape supplies the target region; it does not supply an action by itself. See onTapGesture(count:perform:).

If the row also contains a separate button or toggle, keep it outside the primary button’s label. A useful structure is a horizontal stack containing an expanding primary button and a separate trailing control. The primary label’s blank space belongs to its action; the trailing control owns its own region. Avoid nesting interactive controls inside an all-enclosing row button.

Do not add gesture-priority modifiers merely to repair empty-space targeting. Apple documents highPriorityGesture as changing precedence, while simultaneousGesture allows gestures to process together. Those operations change gesture relationships and can affect descendant interactions; they do not establish missing row geometry.

Diagnose a row that still misses clicks

Use the following sequence on the smallest version of your row. It separates a sizing problem from a target-shape problem and an overlapping-view problem.

  1. Check the actual label bounds. Temporarily add .border(Color.orange) immediately after the label’s frame modifier. If the border ends beside the text, inspect the parent’s width proposal and any fixed-width constraints.
  2. Check modifier placement. Confirm that the content shape covers the expanded label, including the padding you intend to make clickable.
  3. Check the interaction owner. Use the button action for a button row. Remove redundant row tap gestures while isolating the problem.
  4. Check overlapping decoration. Inspect overlays and sibling views above the row. For decoration that should never receive input, consider .allowsHitTesting(false) on that decoration only.
  5. Restore other controls individually. Test each control’s region after restoring it, then test the remaining blank space.

Apple documents allowsHitTesting(_:) as controlling whether a view participates in hit-test operations. Applying it to the entire row would remove the row from participation, so target the specific decorative view when that is the intended behavior.

Use a few distinct click locations to confirm the result. The table describes expected behavior for the supplied program, rather than measured test results.

Click location Expected result
Colored square or row title Activate that row once
Empty space to the right, inside the gray row Activate that row once
Internal padding above or below the title Activate that row once
Gap between rows No row activation
Outer padding or empty area below the rows No row activation

Resize the window and repeat the blank-space check. Also verify keyboard focus and activation with your Mac’s keyboard-navigation settings. These checks help establish whether the final component behaves as the intended control, beyond merely responding to a pointer.

When does AppKit hit testing matter?

For an ordinary SwiftUI row, start with SwiftUI’s layout, content-shape, and control APIs. AppKit becomes relevant when your own app includes custom native views or an embedded view that changes event targeting.

Apple documents NSView.hitTest(_:) as finding the farthest descendant containing a point. A window uses it primarily to determine which view receives a mouse-down event. Its input point is expressed in the view’s superview coordinate system, and Apple says direct invocation is rarely necessary.

If you maintain a custom AppKit view, investigate its bounds and hit-testing override at that boundary. A SwiftUI content shape is not an instruction to bypass a separate native view’s event ownership. Keep the correction within the view hierarchy responsible for receiving the click.

For the row in this tutorial, the supported path is straightforward: let the button label occupy the intended row width, define its rectangular interaction shape, and keep other controls in their own regions. That makes the useful blank space clickable while leaving the rest of the window’s interactions intact.

Sources