Popover

A floating popover component that displays contextual content anchored to a trigger element. Features automatic edge detection, smooth spring animations, and multiple placement options.

Import

import PrettyUI

Basic Usage

The simplest way to create a popover is with a binding and content:

@State private var showPopover = false

var body: some View {
    PButton("Show Popover") {
        showPopover.toggle()
    }
    .pPopover(isPresented: $showPopover) {
        Text("Hello from popover!")
            .padding()
    }
}

Placement

Control where the popover appears relative to its trigger:

@State private var showPopover = false

var body: some View {
    VStack(spacing: 20) {
        PButton("Top Placement") {
            showPopover.toggle()
        }
        .pPopover(isPresented: $showPopover, placement: .top) {
            Text("I appear above!")
                .padding()
        }
        
        PButton("Bottom Placement") {
            showPopover.toggle()
        }
        .pPopover(isPresented: $showPopover, placement: .bottom) {
            Text("I appear below!")
                .padding()
        }
        
        PButton("Leading Placement") {
            showPopover.toggle()
        }
        .pPopover(isPresented: $showPopover, placement: .leading) {
            Text("I appear to the left!")
                .padding()
        }
        
        PButton("Trailing Placement") {
            showPopover.toggle()
        }
        .pPopover(isPresented: $showPopover, placement: .trailing) {
            Text("I appear to the right!")
                .padding()
        }
    }
}

Available Placements

PlacementDescription
.topAbove the trigger, centered horizontally
.bottomBelow the trigger, centered horizontally
.leadingTo the left of the trigger
.trailingTo the right of the trigger

Arrow Indicator

The popover includes an arrow pointing to the trigger by default:

// With arrow (default)
.pPopover(isPresented: $showPopover, showArrow: true) {
    Text("With arrow")
        .padding()
}

// Without arrow
.pPopover(isPresented: $showPopover, showArrow: false) {
    Text("No arrow")
        .padding()
}

Dismiss on Outside Tap

Control whether tapping outside the popover dismisses it:

// Dismiss on outside tap (default)
.pPopover(isPresented: $showPopover, dismissOnOutsideTap: true) {
    Text("Tap outside to dismiss")
        .padding()
}

// Require explicit dismissal
.pPopover(isPresented: $showPopover, dismissOnOutsideTap: false) {
    VStack {
        Text("Must tap button to close")
        PButton("Close") {
            showPopover = false
        }
    }
    .padding()
}

Custom Content

Popovers can contain any SwiftUI content:

@State private var showOptions = false

var body: some View {
    PIconButton("ellipsis") {
        showOptions.toggle()
    }
    .pPopover(isPresented: $showOptions, placement: .bottom) {
        VStack(alignment: .leading, spacing: 0) {
            PopoverMenuItem(icon: "pencil", title: "Edit")
            PopoverMenuItem(icon: "doc.on.doc", title: "Duplicate")
            PopoverMenuItem(icon: "folder", title: "Move to...")
            Divider()
            PopoverMenuItem(icon: "trash", title: "Delete", destructive: true)
        }
        .frame(width: 200)
    }
}

struct PopoverMenuItem: View {
    let icon: String
    let title: String
    var destructive: Bool = false
    
    var body: some View {
        Button {
            // Action
        } label: {
            HStack(spacing: 12) {
                Image(systemName: icon)
                    .frame(width: 20)
                Text(title)
                Spacer()
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 12)
            .foregroundColor(destructive ? .red : .primary)
        }
        .buttonStyle(.plain)
    }
}

Create a menu with actions using the built-in styling:

@State private var showMenu = false

var body: some View {
    PButton("Actions") {
        showMenu.toggle()
    }
    .pPopover(isPresented: $showMenu, placement: .bottom) {
        VStack(spacing: 0) {
            ForEach(["Copy", "Share", "Export"], id: \.self) { action in
                Button(action) {
                    showMenu = false
                }
                .padding(.horizontal, 20)
                .padding(.vertical, 12)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        .frame(width: 150)
    }
}

Auto-Positioning

The popover automatically adjusts its position when near screen edges to stay visible. This behavior is built-in and requires no additional configuration.

Styling

The popover uses theme tokens for consistent styling:

  • Background: Uses card color token
  • Border: Uses border color token
  • Shadow: Uses medium shadow from theme
  • Corner Radius: Uses lg radius token

Animation

The popover features smooth spring animations:

  • Entry: Scales from 0.9 with fade-in
  • Exit: Scales to 0.9 with fade-out
  • Duration: Uses responsive spring physics

API Reference

View Modifier

func pPopover<Content: View>(
    isPresented: Binding<Bool>,
    placement: PPopoverPlacement = .bottom,
    showArrow: Bool = true,
    dismissOnOutsideTap: Bool = true,
    @ViewBuilder content: @escaping () -> Content
) -> some View

Parameters

ParameterTypeDefaultDescription
isPresentedBinding<Bool>RequiredControls visibility
placementPPopoverPlacement.bottomPreferred position
showArrowBooltrueShow directional arrow
dismissOnOutsideTapBooltrueDismiss when tapping outside
content() -> ContentRequiredPopover content view builder

PPopoverPlacement

public enum PPopoverPlacement {
    case top
    case bottom
    case leading
    case trailing
}

Best Practices

  1. Keep content concise - Popovers are best for quick actions or brief information
  2. Use appropriate placement - Consider the context and available screen space
  3. Provide clear dismissal - Users should easily understand how to close the popover
  4. Limit nesting - Avoid opening popovers from within popovers