suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Patterns19
  • Overview
  • Action Bar
  • Action Menu
  • App Shell
  • Command Palette
  • Confirm
  • Context Actions
  • Date Picker
  • Flyout
  • Modal
  • Navbar
  • Paged List
  • Panel
  • Picker
  • Preview
  • Search Picker
  • Segment Picker
  • Split View
  • Table List
  • Trail

suiss/ui · Pattern

Date Picker

A date field that opens a calendar popover on desktop and wheel pickers in a drawer on mobile.

Date Picker is an open-source pattern from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

DatePicker is a single component that renders a field-style trigger showing the selected date, formatted with Intl.DateTimeFormat in the medium date style, or the placeholder. On desktop the trigger opens a Popover containing a single-selection Calendar; picking a day selects it and closes the popover.

Below the 48rem breakpoint the trigger opens a bottom Drawer with a WheelPicker of day, month and year columns, ordered to match the locale's short date format. Turning a wheel updates the value immediately, and the day is clamped to the length of the chosen month. Use it for form fields such as delivery dates or dates of birth.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/date-picker

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add patterns date-picker

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @phosphor-icons/react
  1. Add the pieces it builds on: Calendar, Drawer, Popover, use-mobile, Wheel Picker.

  2. Copy the source into components/patterns/date-picker.tsx:

  1. Update the import paths to match your project.

Usage

import { DatePicker } 
<DatePicker

Use value and onValueChange for a controlled field, or defaultValue for an uncontrolled one; the value is a Date or null. locale drives the trigger format, calendar captions, weekday names, the first day of the week, day labels and the wheel order. fromYear and toYear bound both the calendar's months and the year wheel. Pass id, aria-label and aria-describedby to connect the field to a label and help text.

Examples

With value

Starts with a defaultValue, and the calendar opens on that month.

Locale

Formats the trigger and calendar in Turkish with locale="tr-TR".

Year range

Limits selection to 1940 through 2010 for a date of birth.

With title

Shows a visible Delivery date title in the mobile drawer.

With close button

Adds an icon close button to the mobile drawer.

With close label

Adds a text close button labelled Done to the mobile drawer.

Disabled

Disables the trigger so the picker cannot open.

Invalid

Marks the field invalid with aria-invalid.

Controlled

Owns the value in parent state and shows the picked date below the field.

Floating

Insets the mobile drawer from the screen edges with floating.

API Reference

01

DatePicker

The field trigger with its desktop calendar popover and mobile wheel drawer.

PropTypeDefaultDescription
valueDate | null-The selected date when controlled; null for no date.
defaultValueDate | null-The initial date when uncontrolled.
onValueChange(value: Date | null) => void-Called with the new date when the user picks a day or turns a wheel.
placeholderstring"Pick a date"Text shown in the trigger when no date is selected.
localestring-BCP 47 locale used for formatting, week start, labels and wheel order; defaults to the runtime locale.
fromYearnumbernew Date().getFullYear() - 100The earliest selectable year.
toYearnumbernew Date().getFullYear() + 10The latest selectable year.
titlestring-Visible title of the mobile drawer; without it the drawer title is visually hidden.
disabledboolean-Disables the trigger.
floatingboolean-Insets the mobile drawer from the screen edges with fully rounded corners.
showCloseButtonbooleanfalseShows a close button in the mobile drawer.
closeLabelstring-Replaces the drawer's close icon with this text; requires .

Accessibility

  • The trigger is a button; give it a name with aria-label or a label linked through id.
  • On desktop the calendar is a grid named by its month caption, and each day button is named with the full localized date, prefixed with today's relative name for the current day.
  • Escape closes the calendar and returns focus to the trigger.
  • On mobile each wheel column is labelled Day, Month or Year, and the drawer always has a title, visually hidden when title is not set.

Notes

The trigger carries data-slot="date-picker-trigger" and data-placeholder while empty; the popup carries data-slot="date-picker-content" and the wheels data-slot="date-picker-wheels". When no value is set, the mobile wheels start at today's date. The open state is internal and cannot be controlled.

DatePicker renders a field-style DrawerTrigger with a calendar icon and the selected date formatted with Intl.DateTimeFormat in the medium date style. The trigger sizes itself to its content rather than filling the width; pass className to change that. Pressing it opens a Drawer with three WheelPicker columns.

The column order follows the locale's short date format, falling back to month, day, year. Month names come from the locale, and the day column shrinks or grows with the chosen month and year, clamping the day when needed.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/date-picker

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add date-picker
$ pnpm dlx @esuiss/ui@latest add date-picker --platform swift
NextFlyoutPreviousContext Actions

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • With value
    • Locale
    • Year range
    • With title
    • With close button
    • With close label
    • Disabled
    • Invalid
    • Controlled
    • Floating
  4. 04API Reference
    • DatePicker
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • With value
    • Locale
    • Year range
    • With title
    • With close button
    • With close label
    • Disabled
    • Invalid
    • Controlled
    • Floating
  10. 10API Reference
    • DatePicker
  11. 11Accessibility
  12. 12Notes
from
"@/components/patterns/date-picker"
aria-label
=
"Date"
className
=
"w-60"
/>
showCloseButton
idstring-The id of the trigger button, for use with a label.
classNamestring-Classes applied to the trigger button.
aria-invalidboolean-Marks the field invalid and applies the destructive border and ring.
aria-labelstring-Accessible name of the trigger; also used as the hidden drawer title when title is absent.
aria-describedbystring-IDs of elements that describe the field, such as help or error text.
$ pnpm dlx @esuiss/ui@latest add date-picker --platform kotlin

The CLI also adds what it builds on: Drawer, Icon, Text, Wheel Picker.

Manual

  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Drawer, Icon, Text, Wheel Picker.

  2. Copy the source into components/patterns/date-picker.tsx:

  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Drawer, Icon, Text, Wheel Picker.

  2. Copy the source into components/patterns/date-picker.tsx:

  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Drawer, Icon, Text, Wheel Picker.

  2. Copy the source into components/patterns/date-picker.tsx:

  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Add the Swift package to your app:

Package.swift
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
  1. Copy the theme files every view uses:
swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/Button.swift
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
swift/Components/ButtonGroup.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var buttonGroup: Axis? = nil
  @Entry var buttonGroupSize: CGSize = .zero
}

enum ButtonGroupSpace: Hashable {
  case group
}

struct ButtonGroupFrame {
  let bounds: Anchor<CGRect>
  let outlined: Bool
}
swift/Components/Checkbox.swift
import PhosphorSwift
import SwiftUI

extension Shadow {
  static let control = Shadow(color: .black.opacity(0.08), radius: 0.5, y: 0.5)
  static let thumb = Shadow(color: .black.opacity(0.12), radius: 1.5, y: 1)
}

struct SelectionIndicator<Mark: View>: View {
  let checked: Bool
  let pressed: Bool
  let cornerRadius: CGFloat
  let mark: Mark

  @Environment(\.isEnabled) private var isEnabled
  @Environment(\.isInvalid) private var invalid
swift/Components/DropdownMenu.swift
import PhosphorSwift
import SwiftUI

enum MenuPresentation: Sendable {
  case system, popup
}

extension EnvironmentValues {
  @Entry var menuPresentation: MenuPresentation = .system
  @Entry var menuRadio = MenuRadioContext()
}

struct MenuRadioContext: @unchecked Sendable {
  var selected: AnyHashable?
  var select: @MainActor @Sendable (AnyHashable) -> Void = { _ in }
}
swift/Components/Field.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var isInvalid = false
  @Entry var fieldInset = false
  @Entry var fieldGroupInset = false
  @Entry var fieldSetInset = false
}

extension View {
  public func invalid(_ isInvalid: Bool = true) -> some View {
    environment(\.isInvalid, isInvalid)
  }
}

public enum FieldOrientation: CaseIterable, Sendable {
swift/Components/Haptics.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var hapticsEnabled = true
}

extension View {
  public func haptics(_ enabled: Bool) -> some View {
    environment(\.hapticsEnabled, enabled)
  }

  func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
    modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
  }

  func hapticFeedback<Trigger: Equatable>(
swift/Components/Overlay.swift
import PhosphorSwift
import Observation
import SwiftUI

public enum PopupAlignment: Sendable, CaseIterable {
  case start, center, end
}

struct OverlayDismiss: Sendable {
  let action: @MainActor @Sendable () -> Void

  @MainActor func callAsFunction() {
    action()
  }
}
swift/Components/Select.swift
import PhosphorSwift
import SwiftUI

struct SelectContext: @unchecked Sendable {
  var selected: AnyHashable?
  var registering = false
  var select: @MainActor @Sendable (AnyHashable) -> Void = { _ in }
}

extension EnvironmentValues {
  @Entry var select = SelectContext()
}

struct SelectLabelsKey: PreferenceKey {
  static var defaultValue: [AnyHashable: Text] { [:] }
swift/Components/Separator.swift
import SwiftUI

public struct Separator: View {
  let orientation: Axis

  public init(_ orientation: Axis = .horizontal) {
    self.orientation = orientation
  }

  public var body: some View {
    Rectangle()
      .fill(EsuissColor.border)
      .frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
      .frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
      .accessibilityHidden(true)
  }
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/Button.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
kotlin/components/ButtonGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/Field.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
kotlin/components/FormSupport.kt
package com.esuiss.ui.components

import android.provider.Settings
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
kotlin/components/Haptics.kt
package com.esuiss.ui.components

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

val LocalHapticsEnabled = staticCompositionLocalOf { true }

@Composable
fun ProvideHaptics(enabled: Boolean, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalHapticsEnabled provides enabled, content = content)
}
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
kotlin/components/Label.kt
package com.esuiss.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
kotlin/components/Overlay.kt
package com.esuiss.ui.components

import android.os.Build
import android.view.Window
import android.view.WindowManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideIn
kotlin/components/Separator.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Separator(
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { DatePicker } from "@/components/patterns/date-picker"
<DatePicker accessibilityLabel="Date" />

Use it uncontrolled with defaultValue, or controlled with value and onValueChange. The value is a Date or null; onValueChange fires on every wheel change, so there is no separate confirm step.

When no value is set the wheels start at today and the field shows placeholder. Limit the year column with fromYear and toYear.

Examples

With value

Starts with a preset date via defaultValue.

iOS
Date Picker (with value) on iOS in light modeDate Picker (with value) on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerWithValueExample() {
  return (
    <DatePicker accessibilityLabel="Date" defaultValue={new Date(2026, 9, 4)} />
  )
}
import EsuissUI
import SwiftUI

struct DatePickerWithValueExample: View {
  var body: some View {
    DatePickerDemo(value: datePickerDay(2026, 10, 4))
  }
}

func datePickerDay(_ year: Int, _ month: Int, _ day: Int) -> Date? {
  Foundation.Calendar.current.date(from: DateComponents(year: year, month: month, day: day))
}

struct DatePickerDemo: View {
  var placeholder: LocalizedStringKey = "Pick a date"
  var fromYear: Int?
  var toYear: Int?
  var title: LocalizedStringKey?
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var value: Date?

  init(
    value: Date? = nil,
    placeholder: LocalizedStringKey = "Pick a date",
    fromYear: Int? = nil,
    toYear: Int? = nil,
    title: LocalizedStringKey? = nil,
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil
  ) {
    _value = State(initialValue: value)
    self.placeholder = placeholder
    self.fromYear = fromYear
    self.toYear = toYear
    self.title = title
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    DatePickerField(
      selection: $value,
      placeholder: placeholder,
      fromYear: fromYear,
      toYear: toYear,
      title: title,
      showCloseButton: showCloseButton,
      closeLabel: closeLabel
    )
    .fixedSize()
    .accessibilityLabel(Text("Date"))
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerWithValueExample() {
    DatePickerDemo(initial = datePickerDate(2026, 9, 4))
}

fun datePickerDate(year: Int, month: Int, day: Int): Long =
    Calendar.getInstance().apply {
        clear()
        set(year, month, day)
    }.timeInMillis

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}

Locale

Uses tr-TR, which reorders the wheels and translates month names.

iOS
Date Picker (locale) on iOS in light modeDate Picker (locale) on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerLocaleExample() {
  return (
    <DatePicker accessibilityLabel="Date" locale="tr-TR" placeholder="Tarih seç" defaultValue={new Date(2026, 9, 4)} />
  )
}
import EsuissUI
import SwiftUI

struct DatePickerLocaleExample: View {
  var body: some View {
    DatePickerDemo(value: datePickerDay(2026, 10, 4), placeholder: "Tarih seç")
      .environment(\.locale, Locale(identifier: "tr_TR"))
  }
}

func datePickerDay(_ year: Int, _ month: Int, _ day: Int) -> Date? {
  Foundation.Calendar.current.date(from: DateComponents(year: year, month: month, day: day))
}

struct DatePickerDemo: View {
  var placeholder: LocalizedStringKey = "Pick a date"
  var fromYear: Int?
  var toYear: Int?
  var title: LocalizedStringKey?
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var value: Date?

  init(
    value: Date? = nil,
    placeholder: LocalizedStringKey = "Pick a date",
    fromYear: Int? = nil,
    toYear: Int? = nil,
    title: LocalizedStringKey? = nil,
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil
  ) {
    _value = State(initialValue: value)
    self.placeholder = placeholder
    self.fromYear = fromYear
    self.toYear = toYear
    self.title = title
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    DatePickerField(
      selection: $value,
      placeholder: placeholder,
      fromYear: fromYear,
      toYear: toYear,
      title: title,
      showCloseButton: showCloseButton,
      closeLabel: closeLabel
    )
    .fixedSize()
    .accessibilityLabel(Text("Date"))
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerLocaleExample() {
    DatePickerDemo(
        initial = datePickerDate(2026, 9, 4),
        locale = Locale.forLanguageTag("tr-TR"),
        placeholder = "Tarih seç",
    )
}

fun datePickerDate(year: Int, month: Int, day: Int): Long =
    Calendar.getInstance().apply {
        clear()
        set(year, month, day)
    }.timeInMillis

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}

Year range

A date-of-birth field limited to 1940 through 2010.

iOS
Date Picker (year range) on iOS in light modeDate Picker (year range) on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerYearRangeExample() {
  return (
    <DatePicker accessibilityLabel="Date" placeholder="Date of birth" fromYear={1940} toYear={2010} defaultValue={new Date(1990, 5, 15)} />
  )
}
import EsuissUI
import SwiftUI

struct DatePickerYearRangeExample: View {
  var body: some View {
    DatePickerDemo(value: datePickerDay(1990, 6, 15), placeholder: "Date of birth", fromYear: 1940, toYear: 2010)
  }
}

func datePickerDay(_ year: Int, _ month: Int, _ day: Int) -> Date? {
  Foundation.Calendar.current.date(from: DateComponents(year: year, month: month, day: day))
}

struct DatePickerDemo: View {
  var placeholder: LocalizedStringKey = "Pick a date"
  var fromYear: Int?
  var toYear: Int?
  var title: LocalizedStringKey?
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var value: Date?

  init(
    value: Date? = nil,
    placeholder: LocalizedStringKey = "Pick a date",
    fromYear: Int? = nil,
    toYear: Int? = nil,
    title: LocalizedStringKey? = nil,
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil
  ) {
    _value = State(initialValue: value)
    self.placeholder = placeholder
    self.fromYear = fromYear
    self.toYear = toYear
    self.title = title
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    DatePickerField(
      selection: $value,
      placeholder: placeholder,
      fromYear: fromYear,
      toYear: toYear,
      title: title,
      showCloseButton: showCloseButton,
      closeLabel: closeLabel
    )
    .fixedSize()
    .accessibilityLabel(Text("Date"))
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerYearRangeExample() {
    DatePickerDemo(
        initial = datePickerDate(1990, 5, 15),
        placeholder = "Date of birth",
        fromYear = 1940,
        toYear = 2010,
    )
}

fun datePickerDate(year: Int, month: Int, day: Int): Long =
    Calendar.getInstance().apply {
        clear()
        set(year, month, day)
    }.timeInMillis

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}

With title

Adds a heading to the drawer with title.

iOS
Date Picker (with title) on iOS in light modeDate Picker (with title) on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerWithTitleExample() {
  return (
    <DatePicker accessibilityLabel="Date" title="Delivery date" />
  )
}
import EsuissUI
import SwiftUI

struct DatePickerWithTitleExample: View {
  var body: some View {
    DatePickerDemo(title: "Delivery date")
  }
}

struct DatePickerDemo: View {
  var placeholder: LocalizedStringKey = "Pick a date"
  var fromYear: Int?
  var toYear: Int?
  var title: LocalizedStringKey?
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var value: Date?

  init(
    value: Date? = nil,
    placeholder: LocalizedStringKey = "Pick a date",
    fromYear: Int? = nil,
    toYear: Int? = nil,
    title: LocalizedStringKey? = nil,
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil
  ) {
    _value = State(initialValue: value)
    self.placeholder = placeholder
    self.fromYear = fromYear
    self.toYear = toYear
    self.title = title
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    DatePickerField(
      selection: $value,
      placeholder: placeholder,
      fromYear: fromYear,
      toYear: toYear,
      title: title,
      showCloseButton: showCloseButton,
      closeLabel: closeLabel
    )
    .fixedSize()
    .accessibilityLabel(Text("Date"))
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerWithTitleExample() {
    DatePickerDemo(title = "Delivery date")
}

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}

With close button

Shows a close button in the drawer.

iOS
Date Picker (with close button) on iOS in light modeDate Picker (with close button) on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerWithCloseButtonExample() {
  return (
    <DatePicker accessibilityLabel="Date" showCloseButton />
  )
}
import EsuissUI
import SwiftUI

struct DatePickerWithCloseButtonExample: View {
  var body: some View {
    DatePickerDemo(showCloseButton: true)
  }
}

struct DatePickerDemo: View {
  var placeholder: LocalizedStringKey = "Pick a date"
  var fromYear: Int?
  var toYear: Int?
  var title: LocalizedStringKey?
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var value: Date?

  init(
    value: Date? = nil,
    placeholder: LocalizedStringKey = "Pick a date",
    fromYear: Int? = nil,
    toYear: Int? = nil,
    title: LocalizedStringKey? = nil,
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil
  ) {
    _value = State(initialValue: value)
    self.placeholder = placeholder
    self.fromYear = fromYear
    self.toYear = toYear
    self.title = title
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    DatePickerField(
      selection: $value,
      placeholder: placeholder,
      fromYear: fromYear,
      toYear: toYear,
      title: title,
      showCloseButton: showCloseButton,
      closeLabel: closeLabel
    )
    .fixedSize()
    .accessibilityLabel(Text("Date"))
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerWithCloseButtonExample() {
    DatePickerDemo(showCloseButton = true)
}

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}

With close label

Labels the close button Done with closeLabel.

iOS
Date Picker (with close label) on iOS in light modeDate Picker (with close label) on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerWithCloseLabelExample() {
  return (
    <DatePicker accessibilityLabel="Date" showCloseButton closeLabel="Done" />
  )
}
import EsuissUI
import SwiftUI

struct DatePickerWithCloseLabelExample: View {
  var body: some View {
    DatePickerDemo(showCloseButton: true, closeLabel: "Done")
  }
}

struct DatePickerDemo: View {
  var placeholder: LocalizedStringKey = "Pick a date"
  var fromYear: Int?
  var toYear: Int?
  var title: LocalizedStringKey?
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var value: Date?

  init(
    value: Date? = nil,
    placeholder: LocalizedStringKey = "Pick a date",
    fromYear: Int? = nil,
    toYear: Int? = nil,
    title: LocalizedStringKey? = nil,
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil
  ) {
    _value = State(initialValue: value)
    self.placeholder = placeholder
    self.fromYear = fromYear
    self.toYear = toYear
    self.title = title
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    DatePickerField(
      selection: $value,
      placeholder: placeholder,
      fromYear: fromYear,
      toYear: toYear,
      title: title,
      showCloseButton: showCloseButton,
      closeLabel: closeLabel
    )
    .fixedSize()
    .accessibilityLabel(Text("Date"))
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerWithCloseLabelExample() {
    DatePickerDemo(showCloseButton = true, closeLabel = "Done")
}

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}

Disabled

A disabled field that cannot open the drawer.

iOS
Date Picker (disabled) on iOS in light modeDate Picker (disabled) on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerDisabledExample() {
  return (
    <DatePicker accessibilityLabel="Date" disabled />
  )
}
import EsuissUI
import SwiftUI

struct DatePickerDisabledExample: View {
  var body: some View {
    DatePickerDemo()
      .disabled(true)
  }
}

struct DatePickerDemo: View {
  var placeholder: LocalizedStringKey = "Pick a date"
  var fromYear: Int?
  var toYear: Int?
  var title: LocalizedStringKey?
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var value: Date?

  init(
    value: Date? = nil,
    placeholder: LocalizedStringKey = "Pick a date",
    fromYear: Int? = nil,
    toYear: Int? = nil,
    title: LocalizedStringKey? = nil,
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil
  ) {
    _value = State(initialValue: value)
    self.placeholder = placeholder
    self.fromYear = fromYear
    self.toYear = toYear
    self.title = title
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    DatePickerField(
      selection: $value,
      placeholder: placeholder,
      fromYear: fromYear,
      toYear: toYear,
      title: title,
      showCloseButton: showCloseButton,
      closeLabel: closeLabel
    )
    .fixedSize()
    .accessibilityLabel(Text("Date"))
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerDisabledExample() {
    DatePickerDemo(enabled = false)
}

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}

Invalid

Shows the error border and tint with invalid.

iOS
Date Picker (invalid) on iOS in light modeDate Picker (invalid) on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerInvalidExample() {
  return (
    <DatePicker accessibilityLabel="Date" invalid />
  )
}
import EsuissUI
import SwiftUI

struct DatePickerInvalidExample: View {
  var body: some View {
    DatePickerDemo()
      .invalid()
  }
}

struct DatePickerDemo: View {
  var placeholder: LocalizedStringKey = "Pick a date"
  var fromYear: Int?
  var toYear: Int?
  var title: LocalizedStringKey?
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var value: Date?

  init(
    value: Date? = nil,
    placeholder: LocalizedStringKey = "Pick a date",
    fromYear: Int? = nil,
    toYear: Int? = nil,
    title: LocalizedStringKey? = nil,
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil
  ) {
    _value = State(initialValue: value)
    self.placeholder = placeholder
    self.fromYear = fromYear
    self.toYear = toYear
    self.title = title
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    DatePickerField(
      selection: $value,
      placeholder: placeholder,
      fromYear: fromYear,
      toYear: toYear,
      title: title,
      showCloseButton: showCloseButton,
      closeLabel: closeLabel
    )
    .fixedSize()
    .accessibilityLabel(Text("Date"))
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerInvalidExample() {
    DatePickerDemo(invalid = true)
}

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}

Controlled

Holds the date in state and prints the selection below.

iOS
Date Picker (controlled) on iOS in light modeDate Picker (controlled) on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { DatePicker } from "@/components/patterns/date-picker"
import { Text } from "@/components/ui/text"

function ControlledExample(args: React.ComponentProps<typeof DatePicker>) {
  const [value, setValue] = React.useState<Date | null>(new Date(2026, 9, 4))

  return (
    <View className="items-start gap-2">
      <DatePicker {...args} value={value} onValueChange={setValue} />
      <Text className="text-sm text-muted-foreground">
        Selected: {value ? value.toDateString() : "none"}
      </Text>
    </View>
  )
}

export function DatePickerControlledExample() {
  return (
    <ControlledExample accessibilityLabel="Date" />
  )
}
import EsuissUI
import SwiftUI

func datePickerDay(_ year: Int, _ month: Int, _ day: Int) -> Date? {
  Foundation.Calendar.current.date(from: DateComponents(year: year, month: month, day: day))
}

struct DatePickerControlled: View {
  @State private var value: Date? = datePickerDay(2026, 10, 4)

  var selected: String {
    guard let value else { return "none" }
    let formatter = DateFormatter()
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.dateFormat = "EEE MMM dd yyyy"
    return formatter.string(from: value)
  }

  var body: some View {
    VStack(alignment: .leading, spacing: 8) {
      DatePickerField(selection: $value)
        .fixedSize()
        .accessibilityLabel(Text("Date"))
      Text(verbatim: "Selected: \(selected)")
        .textStyle(.sm)
        .foregroundStyle(EsuissColor.mutedForeground)
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Text
import com.esuiss.ui.patterns.DatePicker
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale

fun datePickerDate(year: Int, month: Int, day: Int): Long =
    Calendar.getInstance().apply {
        clear()
        set(year, month, day)
    }.timeInMillis

@Composable
fun DatePickerControlled() {
    var value by rememberSaveable { mutableStateOf<Long?>(datePickerDate(2026, 9, 4)) }
    val format = SimpleDateFormat("EEE MMM dd yyyy", Locale.US)
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        DatePicker(
            value = value?.let(::Date),
            onValueChange = { value = it.time },
            modifier = Modifier.semantics { contentDescription = "Date" },
        )
        Text(
            "Selected: ${value?.let { format.format(Date(it)) } ?: "none"}",
            style = Typography.sm.copy(color = EsuissTheme.colors.mutedForeground),
        )
    }
}

Floating

Detaches the drawer from the screen edges.

iOS
Date Picker (floating) on iOS in light modeDate Picker (floating) on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerFloatingExample() {
  return (
    <DatePicker accessibilityLabel="Date" floating />
  )
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerFloatingExample() {
    DatePickerDemo(floating = true)
}

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}

API Reference

01

DatePicker

PropTypeDefaultDescription
valueDate | null-The selected date when controlled, or null.
defaultValueDate | null-The initial date when uncontrolled.
onValueChange(value: Date | null) => void-Called with the new Date on every wheel change.
placeholderstring"Pick a date"Text shown when no date is set. Defaults to Pick a date.
localestring-BCP 47 locale for formatting, month names and wheel order.
fromYearnumbernew Date().getFullYear() - 100First year in the year wheel. Defaults to 100 years ago.
toYearnumbernew Date().getFullYear() + 10Last year in the year wheel. Defaults to 10 years ahead.
titlestring-Heading shown at the top of the drawer.
disabledboolean-Prevents opening and dims the field.
floatingboolean-Insets the drawer from the screen edges.
showCloseButtonbooleanfalseShows a close button in the drawer. Defaults to false.
closeLabelstring-Label of the close button.
invalidboolean-Applies the error border and background.
classNamestring-Extra classes for the field, for example w-full to fill the width.
accessibilityLabelstring-Label for the field, also used for the drawer when there is no title.
accessibilityLabelledBystring-Links the field to a visible label.

Accessibility

  • The wheels are labelled Day, Month and Year.
  • Without a title, the drawer is labelled by accessibilityLabel or the placeholder.

Notes

  • The field uses bg-secondary, border-destructive with a bg-destructive tint when invalid, and opacity-50 when disabled; the tint is stronger in dark mode.
  • Without floating, the drawer's top corners are square.
iOS
Date Picker on iOS in light modeDate Picker on iOS in dark mode
import { DatePicker } from "@/components/patterns/date-picker"

export function DatePickerExample() {
  return (
    <DatePicker accessibilityLabel="Date" />
  )
}
import EsuissUI
import SwiftUI

struct DatePickerDemo: View {
  var placeholder: LocalizedStringKey = "Pick a date"
  var fromYear: Int?
  var toYear: Int?
  var title: LocalizedStringKey?
  var showCloseButton = false
  var closeLabel: LocalizedStringKey?

  @State private var value: Date?

  init(
    value: Date? = nil,
    placeholder: LocalizedStringKey = "Pick a date",
    fromYear: Int? = nil,
    toYear: Int? = nil,
    title: LocalizedStringKey? = nil,
    showCloseButton: Bool = false,
    closeLabel: LocalizedStringKey? = nil
  ) {
    _value = State(initialValue: value)
    self.placeholder = placeholder
    self.fromYear = fromYear
    self.toYear = toYear
    self.title = title
    self.showCloseButton = showCloseButton
    self.closeLabel = closeLabel
  }

  var body: some View {
    DatePickerField(
      selection: $value,
      placeholder: placeholder,
      fromYear: fromYear,
      toYear: toYear,
      title: title,
      showCloseButton: showCloseButton,
      closeLabel: closeLabel
    )
    .fixedSize()
    .accessibilityLabel(Text("Date"))
  }
}
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import com.esuiss.ui.patterns.DatePicker
import java.util.Calendar
import java.util.Date
import java.util.Locale

@Composable
fun DatePickerDemo(
    initial: Long? = null,
    locale: Locale = Locale.getDefault(),
    placeholder: String = "Pick a date",
    fromYear: Int = Calendar.getInstance().get(Calendar.YEAR) - 100,
    toYear: Int = Calendar.getInstance().get(Calendar.YEAR) + 10,
    title: String? = null,
    enabled: Boolean = true,
    invalid: Boolean? = null,
    floating: Boolean = false,
    showCloseButton: Boolean = false,
    closeLabel: String? = null,
) {
    var value by rememberSaveable { mutableStateOf(initial) }
    DatePicker(
        value = value?.let(::Date),
        onValueChange = { value = it.time },
        modifier = Modifier.semantics { contentDescription = "Date" },
        placeholder = placeholder,
        locale = locale,
        fromYear = fromYear,
        toYear = toYear,
        title = title,
        enabled = enabled,
        invalid = invalid,
        floating = floating,
        showCloseButton = showCloseButton,
        closeLabel = closeLabel,
    )
}
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerDemo() {
  return <DatePicker aria-label="Date" className="w-60" />
}
components/patterns/date-picker.tsx
"use client"

import { CalendarBlankIcon } from "@phosphor-icons/react"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
import {
  Drawer,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerLocale() {
  return (
    <DatePicker
      aria-label="Date"
      className="w-60"
      locale="tr-TR"
      placeholder="Tarih seç"
      defaultValue={new Date(2026, 9, 4)}
    />
  )
}
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerWithValue() {
  return (
    <DatePicker
      aria-label="Date"
      className="w-60"
      defaultValue={new Date(2026, 9, 4)}
    />
  )
}
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerYearRange() {
  return (
    <DatePicker
      aria-label="Date"
      className="w-60"
      placeholder="Date of birth"
      fromYear={1940}
      toYear={2010}
      defaultValue={new Date(1990, 5, 15)}
    />
  )
}
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerWithTitle() {
  return <DatePicker aria-label="Date" className="w-60" title="Delivery date" />
}
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerWithCloseButton() {
  return <DatePicker aria-label="Date" className="w-60" showCloseButton />
}
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerWithCloseLabel() {
  return (
    <DatePicker
      aria-label="Date"
      className="w-60"
      showCloseButton
      closeLabel="Done"
    />
  )
}
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerInvalid() {
  return <DatePicker aria-label="Date" className="w-60" aria-invalid />
}
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerDisabled() {
  return <DatePicker aria-label="Date" className="w-60" disabled />
}
import * as React from "react"
import { DatePicker } from "@/components/patterns/date-picker"

function ControlledExample(props: React.ComponentProps<typeof DatePicker>) {
  const [value, setValue] = React.useState<Date | null>(new Date(2026, 9, 4))

  return (
    <div className="flex flex-col items-start gap-2">
      <DatePicker {...props} value={value} onValueChange={setValue} />
      <p className="text-sm text-muted-foreground">
        Selected: {value ? value.toDateString() : "none"}
      </p>
    </div>
  )
}

export default function DatePickerControlled() {
  return <ControlledExample aria-label="Date" className="w-60" />
}
import { DatePicker } from "@/components/patterns/date-picker"

export default function DatePickerFloating() {
  return <DatePicker aria-label="Date" className="w-60" floating />
}
uniwind/components/patterns/date-picker.tsx
import { cn } from "cn"
import { CalendarBlankIcon } from "phosphor-react-native"
import * as React from "react"
import {
  Drawer,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
import {
  WheelPicker,
  WheelPickerColumn,
  WheelPickerItem,
swift/Patterns/DatePicker.swift
import PhosphorSwift
import SwiftUI

enum DatePart: Hashable, Sendable {
  case day, month, year

  static func order(locale: Locale) -> [DatePart] {
    let format = DateFormatter.dateFormat(fromTemplate: "yMd", options: 0, locale: locale) ?? ""
    var parts: [DatePart] = []
    for character in format {
      let part: DatePart? = switch character {
      case "d": .day
      case "M", "L": .month
      case "y": .year
      default: nil
      }
swift/Components/Calendar.swift
import PhosphorSwift
import SwiftUI

public struct DateRange: Hashable, Sendable {
  public var from: Date
  public var to: Date?

  public init(from: Date, to: Date? = nil) {
    self.from = from
    self.to = to
  }

  public static func adding(_ day: Date, to range: DateRange?, calendar: Foundation.Calendar) -> DateRange? {
    guard let range else { return DateRange(from: day) }
    let from = range.from
    guard let to = range.to else {
swift/Components/Dialog.swift
import SwiftUI

extension View {
  public func dialog<Content: View>(
    isPresented: Binding<Bool>,
    @ViewBuilder content: @escaping () -> Content
  ) -> some View {
    modifier(
      CoverPresenter(isPresented: isPresented) { visible in
        CenteredStage(visible: visible, dismissible: true, content: content)
      }
    )
  }
}

struct CenteredStage<Content: View>: View {
swift/Components/Drawer.swift
import SwiftUI

extension View {
  public func drawer<Content: View>(
    isPresented: Binding<Bool>,
    detents: Set<PresentationDetent>? = nil,
    selection: Binding<PresentationDetent>? = nil,
    onDismiss: (() -> Void)? = nil,
    @ViewBuilder content: @escaping () -> Content
  ) -> some View {
    let binding = isPresented
    return sheet(isPresented: isPresented, onDismiss: onDismiss) {
      DrawerHost(detents: detents, selection: selection, content: content)
        .environment(\.overlayDismiss, OverlayDismiss { binding.wrappedValue = false })
    }
  }
swift/Components/WheelPicker.swift
import SwiftUI

struct WheelPickerEntry: Equatable {
  let value: AnyHashable
  let disabled: Bool
  let text: Text?
}

struct WheelPickerEntriesKey: PreferenceKey {
  static var defaultValue: [WheelPickerEntry] { [] }

  static func reduce(value: inout [WheelPickerEntry], nextValue: () -> [WheelPickerEntry]) {
    value.append(contentsOf: nextValue())
  }
}
swift/Patterns/Adaptive.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var patternCompact: Bool? = nil
}

@propertyWrapper
struct CompactLayout: DynamicProperty {
  @Environment(\.patternCompact) private var forced
  @Environment(\.horizontalSizeClass) private var sizeClass

  init() {}

  var wrappedValue: Bool {
    forced ?? (sizeClass != .regular)
  }
kotlin/patterns/DatePicker.kt
package com.esuiss.ui.patterns

import androidx.compose.foundation.border
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
kotlin/components/Drawer.kt
package com.esuiss.ui.components

import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
kotlin/components/Select.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
kotlin/components/WheelPicker.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.snapping.SnapPosition
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
nativewind/components/patterns/date-picker.tsx
import { cn } from "cn"
import { CalendarBlankIcon } from "phosphor-react-native"
import * as React from "react"
import {
  Drawer,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
import {
  WheelPicker,
  WheelPickerColumn,
  WheelPickerItem,
stylesheet/components/patterns/date-picker.tsx
import { CalendarBlankIcon } from "phosphor-react-native"
import * as React from "react"
import { type StyleProp, StyleSheet, type ViewStyle } from "react-native"
import {
  Drawer,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
import { fontSize, radius, useTheme } from "@/components/ui/theme"
import {
  WheelPicker,
  WheelPickerColumn,