Day Night Toggle
A switch whose icon morphs between a sun and a moon while its background shifts between day and night colors.

- Controls
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- toggle · switch · dark mode · day · night · sun · moon · theme · interruptor · modo oscuro · día · noche · sol · luna · tema
Description
Section titled “Description”A switch whose icon morphs between a sun and a moon while the sky behind it changes. VoiceOver reads it as a standard switch.
With Reduce Motion on, it changes without sliding.
struct DayNightToggleDemo: View { @State private var isNight = false @State private var lastAutoValue = false @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { ZStack { LinearGradient(colors: [.cyan.opacity(0.55), .white], startPoint: .top, endPoint: .bottom) .ignoresSafeArea()
LinearGradient(colors: [.black, .indigo], startPoint: .top, endPoint: .bottom) .ignoresSafeArea() .opacity(isNight ? 1 : 0)
ScrollView { VStack(spacing: 24) { Text(isNight ? "Good night" : "Good morning") .font(.system(.largeTitle, design: .rounded, weight: .bold)) .foregroundStyle(isNight ? Color.white : Color.primary) .contentTransition(.opacity)
DayNightToggle(isNight: $isNight) } .padding(24) .frame(maxWidth: .infinity) } .defaultScrollAnchor(.center, for: .alignment) } .animation(reduceMotion ? nil : .easeInOut(duration: 0.55), value: isNight) .task(id: reduceMotion) { guard !reduceMotion else { return }
while isNight == lastAutoValue { try? await Task.sleep(for: .seconds(1.5)) guard !Task.isCancelled, isNight == lastAutoValue else { return } lastAutoValue.toggle() isNight = lastAutoValue } } }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
isNight | Binding<Bool> | Required |
title | LocalizedStringResource | "Night mode" |
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
/// A switch whose icon morphs between a sun and a moon while the sky behind it changes./// VoiceOver reads it as a standard switch.////// With Reduce Motion on, it changes without sliding.struct DayNightToggle: View { @Binding var isNight: Bool var title: LocalizedStringResource = "Night mode"
var body: some View { Toggle(isOn: $isNight) { Text(title) } .toggleStyle(DayNightToggleStyle()) }}
private struct DayNightToggleStyle: ToggleStyle { @ScaledMetric(relativeTo: .title) private var height = 56.0 @Environment(\.accessibilityReduceMotion) private var reduceMotion
private static let day = LinearGradient(colors: [.cyan, .blue], startPoint: .top, endPoint: .bottom) private static let night = LinearGradient(colors: [.indigo, .black], startPoint: .top, endPoint: .bottom)
func makeBody(configuration: Configuration) -> some View { let isNight = configuration.isOn let knob = height - 8
Button { configuration.isOn.toggle() } label: { ZStack { Capsule().fill(Self.day) Capsule().fill(Self.night).opacity(isNight ? 1 : 0)
scenery(isNight: isNight, knob: knob) .padding(.horizontal, height * 0.3)
Circle() .fill(isNight ? Color(white: 0.88) : Color.white) .frame(width: knob, height: knob) .overlay { Image(systemName: isNight ? "moon.stars.fill" : "sun.max.fill") .font(.system(size: knob * 0.56)) .foregroundStyle(isNight ? Color.indigo : Color.orange) .contentTransition(.symbolEffect(.replace)) } .shadow(color: .black.opacity(0.25), radius: 4, y: 2) .frame(maxWidth: .infinity, alignment: isNight ? .trailing : .leading) .padding(4) } .frame(width: height * 2.2, height: height) .contentShape(.capsule) } .buttonStyle(.plain) .animation(reduceMotion ? nil : .spring(duration: 0.55, bounce: 0.25), value: isNight) .accessibilityRepresentation { Toggle(isOn: configuration.$isOn) { configuration.label } } }
/// Stars sit where the knob is not at night, and a cloud does the same by day. private func scenery(isNight: Bool, knob: CGFloat) -> some View { ZStack { HStack(spacing: knob * 0.14) { Image(systemName: "sparkle").font(.system(size: knob * 0.3)) Image(systemName: "sparkle").font(.system(size: knob * 0.18)).offset(y: -knob * 0.2) } .frame(maxWidth: .infinity, alignment: .leading) .opacity(isNight ? 1 : 0)
Image(systemName: "cloud.fill") .font(.system(size: knob * 0.44)) .frame(maxWidth: .infinity, alignment: .trailing) .opacity(isNight ? 0 : 1) } .foregroundStyle(.white) .accessibilityHidden(true) }}
// MARK: - Demo
struct DayNightToggleDemo: View { @State private var isNight = false @State private var lastAutoValue = false @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { ZStack { LinearGradient(colors: [.cyan.opacity(0.55), .white], startPoint: .top, endPoint: .bottom) .ignoresSafeArea()
LinearGradient(colors: [.black, .indigo], startPoint: .top, endPoint: .bottom) .ignoresSafeArea() .opacity(isNight ? 1 : 0)
ScrollView { VStack(spacing: 24) { Text(isNight ? "Good night" : "Good morning") .font(.system(.largeTitle, design: .rounded, weight: .bold)) .foregroundStyle(isNight ? Color.white : Color.primary) .contentTransition(.opacity)
DayNightToggle(isNight: $isNight) } .padding(24) .frame(maxWidth: .infinity) } .defaultScrollAnchor(.center, for: .alignment) } .animation(reduceMotion ? nil : .easeInOut(duration: 0.55), value: isNight) .task(id: reduceMotion) { guard !reduceMotion else { return }
while isNight == lastAutoValue { try? await Task.sleep(for: .seconds(1.5)) guard !Task.isCancelled, isNight == lastAutoValue else { return } lastAutoValue.toggle() isNight = lastAutoValue } } }}
#Preview { DayNightToggleDemo()}