Full Screen Menu Overlay
A full-screen menu that opens over the content and brings its links in one by one.

- Navigation
- iOS 18.0+
- intermediate
- Origin
- Original work
- Found by
- full screen menu · overlay · stagger · hamburger · navigation · links · animated · menú a pantalla completa · superposición · escalonado · hamburguesa · navegación · enlaces · animado
Description
Section titled “Description”A full-screen menu whose links arrive one by one. Add it on top of your content.
With Reduce Motion on, the menu and its links appear at once.
struct FullScreenMenuOverlayDemo: View { @State private var isOpen = false @State private var page = "Studio"
private var links: [FullScreenMenuOverlayLink] { [ ("Studio", "paintpalette.fill"), ("Projects", "square.grid.2x2.fill"), ("Journal", "book.fill"), ("Contact", "paperplane.fill"), ].map { title, systemImage in FullScreenMenuOverlayLink(id: title, title: "\(title)", systemImage: systemImage) { page = title } } }
var body: some View { ZStack { VStack(spacing: 20) { HStack { Spacer() Button { isOpen = true } label: { // The padding is part of the button, so all of it answers to a touch. Label("Menu", systemImage: "line.3.horizontal") .padding(20) .contentShape(.rect) } .labelStyle(.iconOnly) .font(.title2.weight(.semibold)) }
Spacer()
Text(page) .font(.system(.largeTitle, design: .rounded, weight: .bold)) .contentTransition(.opacity)
Spacer() } .accessibilityHidden(isOpen)
FullScreenMenuOverlay(isPresented: $isOpen, links: links) } }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
isPresented | Binding<Bool> | Required |
links | [FullScreenMenuOverlayLink] | Required |
colors | [Color] | [.indigo, .purple, .pink] |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
struct FullScreenMenuOverlayLink: Identifiable { let id: String let title: LocalizedStringResource let systemImage: String let action: () -> Void}
/// A full-screen menu whose links arrive one by one. Add it on top of your content.////// With Reduce Motion on, the menu and its links appear at once.struct FullScreenMenuOverlay: View { @Binding var isPresented: Bool let links: [FullScreenMenuOverlayLink] var colors: [Color] = [.indigo, .purple, .pink]
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { ZStack { if isPresented { FullScreenMenuOverlayPanel( links: links, colors: colors, close: { isPresented = false } ) .transition(.move(edge: .top).combined(with: .opacity)) } } .animation(reduceMotion ? nil : .smooth(duration: 0.4), value: isPresented) }}
private struct FullScreenMenuOverlayPanel: View { let links: [FullScreenMenuOverlayLink] let colors: [Color] let close: () -> Void
@State private var showsLinks = false @ScaledMetric(relativeTo: .largeTitle) private var iconWidth = 48.0 @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { ZStack(alignment: .topTrailing) { LinearGradient(colors: colors, startPoint: .topLeading, endPoint: .bottomTrailing) .ignoresSafeArea()
// Scrolls only when large text makes the links taller than the screen. ScrollView { VStack(alignment: .leading, spacing: 22) { ForEach(Array(links.enumerated()), id: \.element.id) { index, link in row(for: link) .opacity(showsLinks ? 1 : 0) .offset(y: showsLinks ? 0 : 28) .animation(animation(forRow: index), value: showsLinks) } } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 32) .padding(.vertical, 90) } .scrollBounceBehavior(.basedOnSize)
Button(action: close) { Image(systemName: "xmark") .font(.title3.weight(.bold)) .foregroundStyle(.white) .padding(14) .background(.white.opacity(0.2), in: .circle) } .accessibilityLabel("Close") .padding(20) } .onAppear { showsLinks = true } .accessibilityAddTraits(.isModal) .accessibilityAction(.escape, close) }
private func animation(forRow index: Int) -> Animation? { guard !reduceMotion else { return nil } return .spring(duration: 0.55, bounce: 0.25).delay(0.12 + 0.08 * Double(min(index, 12))) }
private func row(for link: FullScreenMenuOverlayLink) -> some View { Button { close() link.action() } label: { Label { Text(link.title) } icon: { // One width for every symbol, so the titles line up. Image(systemName: link.systemImage).frame(width: iconWidth) } .font(.system(.largeTitle, design: .rounded, weight: .bold)) .foregroundStyle(.white) } .buttonStyle(.plain) }}
// MARK: - Demo
struct FullScreenMenuOverlayDemo: View { @State private var isOpen = false @State private var page = "Studio"
private var links: [FullScreenMenuOverlayLink] { [ ("Studio", "paintpalette.fill"), ("Projects", "square.grid.2x2.fill"), ("Journal", "book.fill"), ("Contact", "paperplane.fill"), ].map { title, systemImage in FullScreenMenuOverlayLink(id: title, title: "\(title)", systemImage: systemImage) { page = title } } }
var body: some View { ZStack { VStack(spacing: 20) { HStack { Spacer() Button { isOpen = true } label: { // The padding is part of the button, so all of it answers to a touch. Label("Menu", systemImage: "line.3.horizontal") .padding(20) .contentShape(.rect) } .labelStyle(.iconOnly) .font(.title2.weight(.semibold)) }
Spacer()
Text(page) .font(.system(.largeTitle, design: .rounded, weight: .bold)) .contentTransition(.opacity)
Spacer() } .accessibilityHidden(isOpen)
FullScreenMenuOverlay(isPresented: $isOpen, links: links) } }}
#Preview { FullScreenMenuOverlayDemo()}