What You Will Build

A Material Design-style floating action button (FAB) that expands into a vertical menu of action buttons when tapped. The plus icon rotates 45 degrees to become an X, and child buttons fan out with staggered spring animations. This is the standard pattern for apps like Google Drive, Evernote, and task managers where the primary action branches into sub-actions.

Why This Pattern Matters

FABs solve the "too many primary actions" problem. Instead of cluttering the toolbar, you nest secondary actions behind a single button that only expands on demand. SwiftUI makes the staggered reveal trivial with delayed animations and offset manipulation.

Key SwiftUI Concepts

  • Staggered animation delays using .animation(.easeOut.delay(Double(index) * 0.1)).
  • rotationEffect for the plus-to-X icon transition.
  • Configurable direction — the menu can expand top, bottom, left, or right.

Step 1: Define the Action Button and Direction

struct FABActionButton {
    let icon: String
    let action: () -> Void
}

enum FABDirection {
    case left, right, top, bottom
    var angle: Angle {
        switch self {
        case .left: return .degrees(-90)
        case .right: return .degrees(90)
        case .top: return .degrees(0)
        case .bottom: return .degrees(180)
        }
    }
}

Step 2: Build the FAB Menu

The menu uses rotationEffect on the container to point it in the configured direction, and each child button gets a counter-rotation so it stays upright:

struct FABMenu: View {
    let direction: FABDirection
    let actionButtons: [FABActionButton]
    @State var showButtons = false

    var body: some View {
        ZStack {
            // Child action buttons
            ZStack {
                ForEach(actionButtons.indices, id: \.self) { index in
                    let button = actionButtons[index]
                    Button(action: {
                        button.action()
                        withAnimation { showButtons = false }
                    }) {
                        Image(systemName: button.icon)
                            .frame(width: 50, height: 50)
                            .background(.gray.opacity(0.3), in: .circle)
                            .rotationEffect(-direction.angle, anchor: .center)
                    }
                    .tint(.primary)
                    .padding(.bottom, 8)
                    .scaleEffect(showButtons ? 1 : 0)
                    .opacity(showButtons ? 1 : 0)
                    .offset(y: showButtons ? Double(index + 1) * -65 : 60)
                    .animation(
                        .easeOut.delay(Double(index) * 0.1),
                        value: showButtons
                    )
                }
            }
            .rotationEffect(direction.angle, anchor: .center)

            // Main FAB button
            Image(systemName: "plus")
                .foregroundStyle(.black)
                .font(.system(size: 30))
                .padding()
                .background(.white, in: .circle)
                .rotationEffect(.degrees(showButtons ? -45 : 0))
                .shadow(color: .black.opacity(0.1), radius: 15)
                .onTapGesture {
                    withAnimation { showButtons.toggle() }
                }
        }
    }
}

Step 3: Place It in the Bottom Corner

struct FloatingActionButtonDemo: View {
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            Color.clear // your main content here

            FABMenu(
                direction: .top,
                actionButtons: [
                    FABActionButton(icon: "globe", action: {}),
                    FABActionButton(icon: "paintpalette.fill", action: {}),
                    FABActionButton(icon: "doc.fill", action: {})
                ]
            )
            .padding(30)
        }
    }
}

How the Direction System Works

The entire child button stack is built to expand upward (negative Y offset). To expand in a different direction, the container is rotated by the direction angle. Each child button applies a counter-rotation so the icons remain right-side-up regardless of the expansion direction. This single rotation trick replaces the need for four different offset calculations.

Tips and Pitfalls

  • Dismiss on outside tap: In production, add a full-screen transparent overlay behind the FAB that closes the menu when tapped.
  • Stagger order: The delay Double(index) * 0.1 creates a cascade. Reverse it for a closing cascade effect.
  • Shadow on the main button elevates it visually above the child buttons, which is essential for Material Design hierarchy.
  • Accessibility: The rotated plus icon at -45 degrees visually becomes an X, signaling "close" without additional icons.
  • Z-order: The main FAB renders last in the ZStack, so it visually sits on top of the child buttons.

iOS Version: iOS 15+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.