Skip to content
Components PulseCalendar

PulseCalendar

Native date selection with a considered shell.

Form
interactive demo
September 2026‹ ›
MTWTFSS
12345678910111213141516171819202122232425262728

A live browser stand-in using the exact tokens, motion and API of the SwiftUI component.

Native date selection with a considered shell.

A graphical DatePicker wrapper with bounds, theme tokens and accessibility preserved.

Native graphical DatePicker
Optional date bounds
Light / dark ready
VoiceOver friendly
Usage
PulseCalendar(selection: $date)
Install
$ pulse add calendar

Source files

Sources/PulseUI/Components/PulseCalendar/PulseCalendar.swift
Full source Sources/PulseUI/Components/PulseCalendar/PulseCalendar.swift
import SwiftUI

// MARK: - PulseCalendar

/// Native graphical calendar with a themed shell and optional date bounds.
public struct PulseCalendar: View {
    @Environment(\.pulseTheme) private var theme
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @Binding private var selection: Date
    private let title: String
    private let range: ClosedRange<Date>?

    public init(
        _ title: String = "Calendar",
        selection: Binding<Date>,
        in range: ClosedRange<Date>? = nil
    ) {
        self.title = title
        self._selection = selection
        self.range = range
    }

    public var body: some View {
        VStack(alignment: .leading, spacing: theme.spacing.sm) {
            Text(title)
                .font(theme.typography.headline.font)
                .foregroundStyle(theme.colors.foreground)
            if let range {
                DatePicker("", selection: $selection, in: range, displayedComponents: [.date])
                    .labelsHidden()
                    .datePickerStyle(.graphical)
            } else {
                DatePicker("", selection: $selection, displayedComponents: [.date])
                    .labelsHidden()
                    .datePickerStyle(.graphical)
            }
        }
        .padding(theme.spacing.md)
        .background(theme.colors.card)
        .clipShape(RoundedRectangle(cornerRadius: theme.radius.xl, style: .continuous))
        .overlay {
            RoundedRectangle(cornerRadius: theme.radius.xl, style: .continuous)
                .stroke(theme.colors.border.opacity(0.75), lineWidth: 1)
        }
        .animation(reduceMotion ? .none : theme.motion.normal, value: selection)
        .accessibilityElement(children: .contain)
        .accessibilityLabel(title)
    }
}