SwiftUI Side Menu Component
A drawer that slides in from the edge, with the main content pushed and scaled behind it.
Menu
Home
Library
Settings
Variants: Leading, Trailing
Install the free SwiftUI side menu
- Add SwiftKitUI.swift to your app once. It holds the tokens and motion curves.
- Copy SKSideMenu.swift below into your project. It needs nothing else.
Usage
Usage.swift
.skSideMenu(isOpen: $menuOpen) { MenuContent() }Full source · 144 lines · MIT
SKSideMenu.swift
//
// SKSideMenu.swift
// Swift Kit UI
//
// Requires: SwiftKitUI.swift
//
// A drawer that slides in from the edge, with the main content pushed and
// scaled behind it.
//
// Scaling the content rather than only dimming it is what sells the depth —
// it reads as the app stepping back rather than a panel landing on top.
//
// Usage:
// .skSideMenu(isOpen: $menuOpen) { MenuContent() }
//
import SwiftUI
private struct SKSideMenuModifier<Menu: View>: ViewModifier {
@Binding var isOpen: Bool
let width: CGFloat
let edge: HorizontalEdge
let menu: Menu
@State private var drag: CGFloat = 0
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private var sign: CGFloat { edge == .leading ? 1 : -1 }
private var offset: CGFloat {
let base = isOpen ? width * sign : 0
return base + drag
}
/// 0 closed, 1 fully open — drives the backdrop and the content scale.
private var progress: CGFloat {
min(max(abs(offset) / width, 0), 1)
}
func body(content: Content) -> some View {
ZStack(alignment: edge == .leading ? .leading : .trailing) {
content
.scaleEffect(1 - progress * 0.10, anchor: edge == .leading ? .trailing : .leading)
.offset(x: offset)
.overlay {
if progress > 0.01 {
Color.black.opacity(0.35 * progress)
.ignoresSafeArea()
.onTapGesture { close() }
}
}
.disabled(isOpen)
drawer
}
.skAnimation(.settle, value: isOpen)
.gesture(edgeDrag)
}
private var drawer: some View {
menu
.frame(width: width)
.frame(maxHeight: .infinity)
.background(SK.Palette.surfaceElevated)
.ignoresSafeArea()
.offset(x: (isOpen ? 0 : -width * sign) + drag)
.shadow(color: .black.opacity(0.25), radius: 20)
.accessibilityHidden(!isOpen)
}
private var edgeDrag: some Gesture {
DragGesture(minimumDistance: 12)
.onChanged { g in
let raw = g.translation.width
// Only allow dragging in the direction that makes sense for the
// current state, so the drawer cannot be dragged past open.
if isOpen {
drag = edge == .leading ? min(raw, 0) : max(raw, 0)
} else {
drag = edge == .leading ? max(raw, 0) : min(raw, 0)
}
drag = min(max(drag, -width), width)
}
.onEnded { g in
let predicted = g.predictedEndTranslation.width * sign
withAnimation(SKMotion.settle.resolved(reduceMotion: reduceMotion)) {
if isOpen && predicted < -width / 3 {
isOpen = false
} else if !isOpen && predicted > width / 3 {
isOpen = true
SK.Haptic.play(.light)
}
drag = 0
}
}
}
private func close() {
SK.Haptic.play(.light)
withAnimation(SKMotion.settle.resolved(reduceMotion: reduceMotion)) {
isOpen = false
drag = 0
}
}
}
extension View {
public func skSideMenu<Menu: View>(
isOpen: Binding<Bool>,
width: CGFloat = 280,
edge: HorizontalEdge = .leading,
@ViewBuilder menu: () -> Menu
) -> some View {
modifier(SKSideMenuModifier(isOpen: isOpen, width: width, edge: edge, menu: menu()))
}
}
#Preview("SKSideMenu") {
@Previewable @State var open = false
VStack(spacing: SK.Space.lg) {
Button("Open menu") { open = true }
.font(SK.Type_.callout)
.foregroundStyle(SK.Palette.accent)
Text("Or drag in from the left edge")
.font(SK.Type_.caption)
.foregroundStyle(SK.Palette.labelTertiary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(SK.Palette.surfaceSunken)
.skSideMenu(isOpen: $open) {
VStack(alignment: .leading, spacing: SK.Space.lg) {
Text("Menu").font(SK.Type_.title).foregroundStyle(SK.Palette.label)
ForEach(["Home", "Library", "Downloads", "Settings"], id: \.self) { item in
Text(item).font(SK.Type_.body).foregroundStyle(SK.Palette.labelSecondary)
}
Spacer()
}
.padding(SK.Space.xl)
.padding(.top, SK.Space.xxxl)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
Questions
- Is this SwiftUI side menu free for commercial apps?
- Yes. Swift Kit UI is MIT licensed, so you can use it in personal and commercial apps with no payment or sign-up. Keep the licence notice with the code.
- What does it depend on?
- SKSideMenu.swift depends only on SwiftKitUI.swift, a single foundation file with the design tokens and motion curves. There are no third-party packages.
- Which iOS versions does it support?
- iOS 17 and later, plus macOS 14.
Related components
Building a whole app? The Swift Kit adds auth, payments and onboarding, ready to ship.