Docs
Inputs

Date Range Picker

A start-to-end date range picker on a paged month grid, the range pouring between two glass bubbles like liquid.

$npx swiftpieces add DateRangePicker
Free · MIT + Commons ClauseiOS 17.0+Liquid Glassdatecalendarrangebookingpicker
Type
DateRangePicker
Files
DateRangePicker.swift
Depends on
Nothing (Apple frameworks only)
Version
1.2.0

Day-granular start/end range picker with a paged month grid. The selection is always two start-of-day dates in the environment Calendar and TimeZone (upperBound is the start of the last day, e.g. the check-out day for a stay). The first tap sets a start, the second closes the range (a day before the start becomes the new start), and a third tap starts over. A range can never contain a disabled or out-of-bounds day: an end tap that would span one, or that exceeds maximumLength, is refused with an error haptic, a butter bubble that flares and shakes on the tapped day and a short notice, and the start stays in place. The grid stays as it is and the selection is liquid glass over it: the start and end are tinted bubbles, and the band between them is liquid glass that joins them through a neck at each end and wraps across week rows.

Notes

  • The selection is a ClosedRange<Date> of start-of-day dates in the environment Calendar and TimeZone. upperBound is the start of the last day (the check-out day for a stay). Dates you pass in are normalized to their day.
  • First tap sets the start and clears any old range, second tap sets the end, third tap starts a new range. Tapping a day before the pending start moves the start there. In .nights mode tapping the start again cancels it; in .days mode it confirms a one-day range.
  • The grid stays as it is and the selection is liquid glass over it. Touching a day presses a small clear glass bubble into it. The tap that makes it the start or end swells that bubble out to full size on a spring that springs a little past it, its red flooding in, so the press and the landing are one bubble; with no finger behind it (a VoiceOver tap, a range set from outside) it swells and fades in where it lands. A bubble whose day stops being an endpoint drains clear and shrinks away where it is, without bounce. Pressing a day that already has a bubble sinks it and it springs back with a little bounce. The day's number turns to dark ink as the red comes in, so it reads on the red. The glass is drawn behind the day buttons, so a press never dims or dents it.
  • A range never contains an unavailable day. An end tap that would span a day marked by isDateDisabled or outside bounds, or that exceeds maximumLength, is refused: the start stays, an .error haptic plays, the bubble under the finger swells into butter glass (Style.error) as the day shakes once, holds for a beat and melts away, and the summary chip turns butter with the notice ("Includes unavailable dates", "Up to 14 nights") for two seconds, or until the next tap closes or restarts the range. The flare and shake also mark the refusal with showsSummary: false. While a start is pending, days that cannot close the range fade to a dim, and fade back when it closes.
  • The grid comes from the calendar: firstWeekday, days per week, days in the month and the weekday offset all come from Calendar APIs, so Hebrew, Islamic, Ethiopic and other calendars lay out correctly. Weekday symbols are the calendar's very short standalone symbols; day numbers, month titles and the summary use Date.FormatStyle and Date.IntervalFormatStyle in the environment locale, including native digits.
  • Day math uses Calendar.date(byAdding:) and counts days on civil dates, never 86,400 second steps, so 23 and 25 hour DST days and time zones whose midnight is skipped stay exact.
  • The band is liquid glass that joins the start and end bubbles: it rests a few points short of each, so a liquid neck holds it to both (a red bubble and the neutral band blend through the neck), and it wraps across week rows as one capsule per week, with rounded ends at row and month edges. Each week row is its own liquid group, so the band in one week never melts into the next. When a range completes, the band pours out of the start bubble like one pen stroke: a drop that lengthens into the band, filling each week in turn as the pen reaches it, quick off the start and easing into the end bubble, in at most half a second, where the neck snaps in. The end bubble lands at once and never waits for the band. Starting a new range drains the old band back along the same path into its start on a spring with no bounce, while its bubbles drain and shrink away and the new start swells in. Offsets follow the layout direction, so right to left the grid, the band and the bubbles all flow right to left.
  • Months page with the chevrons or a horizontal swipe (a paging ScrollView with its native fling, which runs out to the card's edges so a month slides in from the edge), and the month title morphs letter by letter without overshoot. The chevrons are a joined pair of glass bubbles; at the first or last month the one that can't page melts into the other, and buds back out as soon as it can, each keeping its place. They scroll to the month on a short spring that settles with a little give. Paging stops at the months of bounds; without bounds it spans five years either side of today and the selection's month.
  • The summary shows the range in the locale's interval pattern ("Mar 4 – 9") and one glass chip: "Select end date" while a start waits for its end, the notice after a refusal (tinted butter), and the length in counting units once the range closes, pluralized with automatic grammar agreement. The chip morphs between them, its label changing letter by letter and its glass widening or narrowing around it. It pops with a bounce when a tap closes the range, on the beat of the success haptic; a range set from outside, which plays no haptic, lands it on a short spring instead. The dates morph letter by letter without overshoot.
  • Haptics: .selection when a start is set, .success when a range completes, .error on a refused end. One per tap; nothing plays on press.
  • VoiceOver: each day is a button labeled with its full date ("Tuesday, March 4", plus the year outside the current one, and "Today" first for today), with the .isSelected trait inside the range and a value of Start date, End date, In range or Unavailable. Month changes, completed ranges and refusals are announced.
  • Dynamic Type: day cells grow with @ScaledMetric from 44pt up to 76pt tall, and the bubbles and band with them; day numbers scale to accessibility sizes and shrink to fit rather than clip. The summary stacks when it no longer fits on one line, and at accessibility sizes the titles and the chip wrap and blur across instead of morphing.
  • Reduce Motion: no pour, swelling, shake, overshoot or morph. Bubbles, the chevrons and the band fade in and out where they rest, a pressed bubble shades instead of sinking (darker in light mode, lighter in dark), a refused day still flares butter, titles cross-fade, and out-of-reach days still fade. The swipe no longer slides the grid; both the swipe and the chevrons crossfade to the next month, and a short quick flick pages as well as a drag past 50pt. Reduce Transparency draws the glass solid.
  • Style sets the ground, day text, muted text, the glass tints of the band (neutral by default), the start and end bubbles (the signal red) and the month buttons (neutral), ink on tinted glass, the today dot, disabled days, and the butter tint of the notice and the refusal flare. cornerRadius no longer does anything: the bubbles are round and the band a capsule. .disabled(true) dims the picker and stops taps and paging.
  • For booking a time slot inside a day, see the slot picker in SwiftPieces Pro.

Usage

DateRangePickerExample()

Parameters

ParameterDescription
selectionBound range, or nil while nothing (or only a start) is chosen. In .nights mode it stays nil until the end is tapped; in .days mode a lone start already reads as a one-day range.
boundsFirst and last selectable days. Days outside are muted and inert, and paging stops at their months. nil allows any day and pages five years either side of today.
isDateDisabledMarks single days unavailable (for example sold-out nights). Called with the start of each day; unavailable days are struck through and can never be inside a range.
maximumLengthLongest allowed range, counted in counting units. While a start is pending, days that cannot close the range are dimmed.
counting.days counts both ends (Mar 4 to Mar 9 is 6 days, and a single day is allowed). .nights counts nights (5 nights, and the end must be after the start).
showsSummaryShow the header line with the formatted range and its length chip.
styleColors. Defaults to the SwiftPieces house palette (red glass start and end bubbles with dark ink, joined by a neutral glass band), adapting to light and dark.

Install

Pick one. Run it from the folder that contains your .xcodeproj and the files land inside your app.

Terminal

$npx swiftpieces add DateRangePicker

Or ask your coding agent · Claude Code, Cursor or Xcode, once the MCP server is connected

>Add the SwiftPieces "Date Range Picker" piece to my app

Or copy the source above into your app.

New to SwiftUI?If you know React, CSS or Tailwind, SwiftUI is easier to pick up than it looks.Try real app patterns first

Building a whole app? See SwiftPieces Pro →

All inputs and forms →

Build it yourself: SwiftUI haptics guide

On this page

SwiftPiecesPro

Build the whole app with one library.

Production-ready screens, whole flows, complete app templates, a Build Kit your agent follows and Pro remixing.

54 screens · 46 patterns · 48 flows · 12 templates · 31 Build Kit skills

Explore Pro