Components PulseDateRange
PulseDateRange
Ranges, not dates.
interactive demo
March 2026
MTWTFSS
A live browser stand-in using the exact tokens, motion and API of the SwiftUI component.
Want all components at once?
Open the studio
Ranges, not dates.
A date range picker with preset chips (Last 7 days, This month…), custom validation, and a fully themed inline calendar.
Preset chips for common ranges
Validation with error messaging
Inline themed calendar
Sendable state
Usage
PulseDateRange(startDate: $start, endDate: $end) Install
$ pulse add date_range Source files
Sources/PulseUI/Components/PulseDateRange/PulseDateRange.swift
Full source Sources/PulseUI/Components/PulseDateRange/PulseDateRange.swift
import SwiftUI
// MARK: - PulseDateRange
/// Beautiful date range picker with a calendar wheel, chip presets, and glass styling.
public struct PulseDateRange: View {
@Environment(\.pulseTheme) private var theme
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Binding var startDate: Date?
@Binding var endDate: Date?
let presets: [DateRangePreset]
let showsClearButton: Bool
public init(
startDate: Binding<Date?>,
endDate: Binding<Date?>,
presets: [DateRangePreset] = DateRangePreset.standard,
showsClearButton: Bool = true
) {
self._startDate = startDate
self._endDate = endDate
self.presets = presets
self.showsClearButton = showsClearButton
}
public var body: some View {
VStack(spacing: theme.spacing.lg) {
if !presets.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: theme.spacing.sm) {
ForEach(presets) { preset in
presetChip(preset)
}
}
}
}
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Start")
.font(theme.typography.overline.font)
.foregroundStyle(theme.colors.foregroundTertiary)
Text(formatDate(startDate))
.font(theme.typography.subheadline.font)
.fontWeight(.semibold)
.foregroundStyle(theme.colors.foreground)
.monospacedDigit()
}
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: "arrow.right")
.font(.caption.weight(.semibold))
.foregroundStyle(theme.colors.foregroundTertiary)
VStack(alignment: .trailing, spacing: 4) {
Text("End")
.font(theme.typography.overline.font)
.foregroundStyle(theme.colors.foregroundTertiary)
Text(formatDate(endDate))
.font(theme.typography.subheadline.font)
.fontWeight(.semibold)
.foregroundStyle(theme.colors.foreground)
.monospacedDigit()
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(theme.spacing.md)
.background(theme.colors.backgroundSecondary.opacity(0.6))
.clipShape(RoundedRectangle(cornerRadius: theme.radius.lg, style: .continuous))
if showsClearButton && (startDate != nil || endDate != nil) {
Button("Clear dates") {
withAnimation(theme.motion.springSnappy) {
startDate = nil
endDate = nil
}
PulseHaptic.selection()
}
.font(theme.typography.caption.font)
.foregroundStyle(theme.colors.accent)
.buttonStyle(.plain)
.accessibilityLabel("Clear selected dates")
}
}
.accessibilityElement(children: .contain)
}
@ViewBuilder
private func presetChip(_ preset: DateRangePreset) -> some View {
let isActive = preset.isActive(startDate: startDate, endDate: endDate)
Button {
withAnimation(reduceMotion ? .none : theme.motion.springSnappy) {
startDate = preset.start
endDate = preset.end
}
PulseHaptic.selection()
} label: {
Text(preset.label)
.font(theme.typography.caption.font)
.fontWeight(isActive ? .semibold : .regular)
.foregroundStyle(isActive ? .white : theme.colors.foregroundSecondary)
.padding(.horizontal, 14)
.padding(.vertical, 7)
.background(
Capsule()
.fill(isActive ? theme.colors.accent : theme.colors.backgroundSecondary)
)
.overlay(
Capsule()
.strokeBorder(isActive ? Color.clear : theme.colors.border, lineWidth: 1)
)
}
.buttonStyle(.plain)
.accessibilityLabel(preset.label)
.accessibilityAddTraits(isActive ? .isSelected : [])
}
private func formatDate(_ date: Date?) -> String {
guard let date else { return "Not set" }
return date.formatted(
.dateTime
.month(.abbreviated)
.day()
)
}
}
// MARK: - Presets
public struct DateRangePreset: Identifiable, Sendable {
public let id: String
public let label: String
public let start: Date
public let end: Date
public init(id: String, label: String, start: Date, end: Date) {
self.id = id
self.label = label
self.start = start
self.end = end
}
public static let standard: [DateRangePreset] = [
DateRangePreset(
id: "today",
label: "Today",
start: Calendar.current.startOfDay(for: Date()),
end: Date()
),
DateRangePreset(
id: "week",
label: "This Week",
start: Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date(),
end: Date()
),
DateRangePreset(
id: "month",
label: "This Month",
start: Calendar.current.date(byAdding: .month, value: -1, to: Date()) ?? Date(),
end: Date()
),
DateRangePreset(
id: "quarter",
label: "Quarter",
start: Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date(),
end: Date()
),
]
func isActive(startDate: Date?, endDate: Date?) -> Bool {
guard let startDate, let endDate else { return false }
let dayInterval: TimeInterval = 24 * 60 * 60
return fabs(startDate.timeIntervalSince(start)) < dayInterval
&& fabs(endDate.timeIntervalSince(end)) < dayInterval
}
}