compose-datetime-wheel-picker

Elliptical datetime picker implementation featuring customizable wheel pickers for date, time, and datetime selection, supporting various formats and theming options.

Android JVMJVMKotlin/NativeWasmJS
GitHub stars239
Authorsdarkokoa
Open issues22
LicenseApache License 2.0
Creation dateabout 2 years ago

Last activityabout 16 hours ago
Latest release1.3.0 (2 days ago)

datetime-wheel-picker (work-in-progress 👷🔧️👷‍♀️⛏)

badge-android badge-jvm badge-ios badge-js badge-wasm

Compose Multiplatform datetime picker implementation featuring highly customizable wheel pickers for date, time, and datetime selection.

Picker Basic Usage
WheelDateTimePicker { snappedDateTime -> }
WheelDatePicker { snappedDate -> }
WheelTimePicker { snappedTime -> }
WheelTimePicker(timeFormatter = timeFormatter(timeFormat = TimeFormat.AM_PM)) { snappedTime -> }

Key Features

🎯 Flexible Date Field Configuration

  • Customizable date order: DMY (Day-Month-Year), MDY (Month-Day-Year), or YMD (Year-Month-Day)
  • Hide year picker: Set yearsRange = null to create day-month only or month-day only pickers
  • Custom year range: Limit year selection to specific ranges (e.g., next 10 years, last 50 years)
  • Multiple month display styles: Full names, short names, or numeric format

🌍 Internationalization & Localization

  • Auto-adapts to locale: Date order and month names automatically match the current locale
  • CJK language support: Special handling for Chinese (年/月/日), Japanese (年/月/日), Korean (년/월/일) with customizable year/month/day suffixes
  • Localized numerals: Supports Eastern Arabic numerals and other numeral systems
  • Script-aware locale matching: A locale is matched first by its language+script subtags, then by language alone, and finally falls back to English. Examples: uz-Arabuz-Arab (direct script match), uz-Latnuz (script not bundled, base language used), zh-Hantzh (same).
  • Limitations: BCP 47 -u-* Unicode extension subtags (e.g. -u-nu-* numbering system, -u-ca-* calendar) are ignored — only the language and script subtags influence resolution.

Currently Supported Languages (23): Arabic (العربية), Bengali (বাংলা), Chinese (中文), Dutch (Nederlands), English, French (Français), German (Deutsch), Hebrew (עברית), Hindi (हिन्दी), Indonesian (Bahasa Indonesia), Italian (Italiano), Japanese (日本語), Korean (한국어), Persian (فارسی), Polish (Polski), Portuguese (Português), Russian (Русский), Spanish (Español), Thai (ไทย), Turkish (Türkçe), Ukrainian (Українська), Uzbek (Oʻzbekcha / Ўзбекча / اۉزبېکچه — uz, uz-Cyrl, uz-Arab), Vietnamese (Tiếng Việt)

Contributions welcome: If you find any translation errors or want to add support for a new language, please open an issue or submit a pull request.

🎨 Visual Customization

  • Configurable size, row count, text style, and colors
  • Customizable selector appearance (shape, color, border)
  • Material Design integration

⏰ Time Picker Features

  • 12-hour (AM/PM) and 24-hour formats
  • Customizable time display

Common Use Cases

Day-Month Picker (No Year)

Perfect for birthdays, anniversaries, or recurring events:

WheelDatePicker(
  startDate = LocalDate(2025, 6, 15),
  yearsRange = null,  // Hides the year picker
  dateFormatter = dateFormatter(
    dateOrder = DateOrder.DMY,
    monthDisplayStyle = MonthDisplayStyle.FULL
  )
) { snappedDate ->
  // snappedDate.month and snappedDate.dayOfMonth
}

Note: This uses the non-Composable dateFormatter() overload that accepts dateOrder parameter.

Month-Day Picker (US Format)

For US-style date input without year:

WheelDatePicker(
  yearsRange = null,
  dateFormatter = dateFormatter(
    dateOrder = DateOrder.MDY,
    monthDisplayStyle = MonthDisplayStyle.SHORT
  )
) { snappedDate -> }
Limited Year Range

Restrict year selection to a specific range:

// Only allow next 10 years
WheelDatePicker(
  yearsRange = IntRange(2025, 2035),
  dateFormatter = dateFormatter(
    dateOrder = DateOrder.YMD
  )
) { snappedDate -> }

// Only allow past 50 years (for birthdate)
val currentYear = Clock.System.now()
  .toLocalDateTime(TimeZone.currentSystemDefault()).year
WheelDatePicker(
  yearsRange = IntRange(currentYear - 50, currentYear),
  dateFormatter = dateFormatter(dateOrder = DateOrder.DMY)
) { snappedDate -> }
Chinese/Japanese/Korean Format

With native year-month-day suffixes:

  • Chinese: "2025年1月15日"
  • Korean: "2025년1월15일"
  • Japanese: "2025年1月15日"
WheelDatePicker(
  dateFormatter = dateFormatter(
    locale = Locale("zh"),  // "zh" for Chinese, "ja" for Japanese, "ko" for Korean
    monthDisplayStyle = MonthDisplayStyle.NUMERIC,
    cjkSuffixConfig = CjkSuffixConfig.ShowAll
  )
) { snappedDate -> }

// Without suffixes
WheelDatePicker(
  dateFormatter = dateFormatter(
    locale = Locale("zh"),
    monthDisplayStyle = MonthDisplayStyle.NUMERIC,
    cjkSuffixConfig = CjkSuffixConfig.HideAll
  )
) { snappedDate -> }

Note: This uses the Composable dateFormatter() overload. Date order (YMD for CJK) is auto-detected from locale.

Numeric Month Display

Show months as numbers instead of names:

WheelDatePicker(
  dateFormatter = dateFormatter(
    dateOrder = DateOrder.DMY,  // or MDY, YMD based on your preference
    monthDisplayStyle = MonthDisplayStyle.NUMERIC
  )
) { snappedDate -> }

Full Customization Example

WheelDateTimePicker(
  startDateTime = LocalDateTime(
    year = 2025,
    month = 10,
    day = 20,
    hour = 5,
    minute = 30
  ),
  minDateTime = Clock.System
    .now()
    .toLocalDateTime(TimeZone.currentSystemDefault()),
  maxDateTime = LocalDateTime(
    year = 2025,
    month = 10,
    day = 20,
    hour = 5,
    minute = 30
  ),
  dateFormatter = dateFormatter(
    locale = Locale.current, 
    monthDisplayStyle = MonthDisplayStyle.SHORT,
    cjkSuffixConfig = CjkSuffixConfig.HideAll
  ),
  timeFormatter = timeFormatter(
    timeFormat = TimeFormat.HOUR_24
  ),
  size = DpSize(200.dp, 100.dp),
  rowCount = 5,
  textStyle = MaterialTheme.typography.titleSmall,
  textColor = Color(0xFFffc300),
  selectorProperties = WheelPickerDefaults.selectorProperties(
    enabled = true,
    shape = RoundedCornerShape(0.dp),
    color = Color(0xFFf1faee).copy(alpha = 0.2f),
    border = BorderStroke(2.dp, Color(0xFFf1faee))
  )
) { snappedDateTime -> }

API Reference

WheelDatePicker Parameters

Parameter Type Default Description
startDate LocalDate LocalDate.now() Initial selected date
minDate LocalDate LocalDate.EPOCH Minimum selectable date
maxDate LocalDate LocalDate.CYB3R_1N1T_ZOLL Maximum selectable date
yearsRange IntRange? IntRange(minDate.year, maxDate.year) Year range to display. Set to null to hide year picker
dateFormatter DateFormatter Auto-detected Controls date order, month style, and CJK suffixes
size DpSize DpSize(256.dp, 128.dp) Picker dimensions
rowCount Int 3 Number of visible rows in the wheel
textStyle TextStyle MaterialTheme.typography.titleMedium Text styling
textColor Color LocalContentColor.current Text color
selectorProperties SelectorProperties Default Selector appearance (shape, color, border)
onSnappedDateChanged (LocalDate) -> Unit {} Callback fired during scrolling every time the snapped date changes (live updates)
onSnappedDate (LocalDate) -> Unit {} Callback fired when scrolling settles on the final selected date

onSnappedDateChanged vs onSnappedDate

  • onSnappedDateChanged is invoked continuously while the user is scrolling, each time a different item snaps into the selector. Useful for live previews, syncing UI, or tracking the in-flight value.
  • onSnappedDate is invoked only once after the wheel comes to rest, representing the user's final choice. Use it for committing the selection (saving, navigating, etc.).

The same pattern applies to WheelTimePicker (onSnappedTimeChanged / onSnappedTime) and WheelDateTimePicker (onSnappedDateTimeChanged / onSnappedDateTime).

DateFormatter Options

DateOrder (controls field arrangement):

  • DateOrder.DMY - Day, Month, Year (Europe, most of world)
  • DateOrder.MDY - Month, Day, Year (US)
  • DateOrder.YMD - Year, Month, Day (East Asia, ISO 8601)

MonthDisplayStyle:

  • MonthDisplayStyle.FULL - "January", "February", etc.
  • MonthDisplayStyle.SHORT - "Jan", "Feb", etc.
  • MonthDisplayStyle.NUMERIC - "1", "2", etc.

CjkSuffixConfig (for Chinese/Japanese/Korean):

  • CjkSuffixConfig.ShowAll - Shows year/month/day suffixes (Chinese/Japanese: 年/月/日, Korean: 년/월/일)
  • CjkSuffixConfig.HideAll - Hides all suffixes
  • Custom: CjkSuffixConfig(showYearSuffix = true, showMonthSuffix = false, ...)

WheelTimePicker Parameters

Parameter Type Default Description
startTime LocalTime LocalTime.now() Initial selected time
minTime LocalTime LocalTime.MIN Minimum selectable time
maxTime LocalTime LocalTime.MAX Maximum selectable time
timeFormatter TimeFormatter Auto-detected Controls 12/24 hour format (auto: AM/PM for en-US/GB, 24h for others)
size DpSize DpSize(128.dp, 128.dp) Picker dimensions (narrower than date picker)
Other params - Same as WheelDatePicker rowCount, textStyle, textColor, selectorProperties, etc.

TimeFormat:

  • TimeFormat.HOUR_24 - 24-hour format (00:00 - 23:59)
  • TimeFormat.AM_PM - 12-hour format with AM/PM

WheelDateTimePicker Parameters

Parameter Type Default Description
startDateTime LocalDateTime LocalDateTime.now() Initial selected date-time
minDateTime LocalDateTime LocalDateTime.EPOCH Minimum selectable date-time
maxDateTime LocalDateTime LocalDateTime.CYB3R_1N1T_ZOLL Maximum selectable date-time
yearsRange IntRange? IntRange(minDateTime.year, maxDateTime.year) Year range to display. Set to null to hide year picker
dateFormatter DateFormatter Auto-detected Controls date order, month style, and CJK suffixes
timeFormatter TimeFormatter Auto-detected Controls 12/24 hour format
size DpSize DpSize(256.dp, 128.dp) Picker dimensions
onSnappedDateTimeChanged (LocalDateTime) -> Unit {} Callback fired during scrolling every time the snapped date-time changes (live updates)
onSnappedDateTime (LocalDateTime) -> Unit {} Callback fired when scrolling settles on the final selected date-time
Other params - Same as WheelDatePicker rowCount, textStyle, textColor, selectorProperties

Setup

Maven Central

  • Add the Maven Central repository if it is not already there:
repositories {
  mavenCentral()
}
  • In Compose multiplatform projects, add a dependency to the commonMain source set dependencies:
kotlin {
  sourceSets {
    val commonMain by getting {
      dependencies {
        implementation("io.github.darkokoa:datetime-wheel-picker:<version>")
        implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
      }
    }
  }
}
  • To use the library in a single-platform project (such as Android project), add a dependency to the dependencies block:
dependencies {
  implementation("io.github.darkokoa:datetime-wheel-picker:<version>")
  implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
}
  • If your minimum Android platform's API level (minSdk) < 26, please enable Desugaring like this:
compileOptions {
  isCoreLibraryDesugaringEnabled = true
}

//...

dependencies {
  coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}

License

Released under the Apache License, Version 2.0.

Thx

WheelPickerCompose

Android JVMJVMKotlin/NativeWasmJS
GitHub stars239
Authorsdarkokoa
Open issues22
LicenseApache License 2.0
Creation dateabout 2 years ago

Last activityabout 16 hours ago
Latest release1.3.0 (2 days ago)

datetime-wheel-picker (work-in-progress 👷🔧️👷‍♀️⛏)

badge-android badge-jvm badge-ios badge-js badge-wasm

Compose Multiplatform datetime picker implementation featuring highly customizable wheel pickers for date, time, and datetime selection.

Picker Basic Usage
WheelDateTimePicker { snappedDateTime -> }
WheelDatePicker { snappedDate -> }
WheelTimePicker { snappedTime -> }
WheelTimePicker(timeFormatter = timeFormatter(timeFormat = TimeFormat.AM_PM)) { snappedTime -> }

Key Features

🎯 Flexible Date Field Configuration

  • Customizable date order: DMY (Day-Month-Year), MDY (Month-Day-Year), or YMD (Year-Month-Day)
  • Hide year picker: Set yearsRange = null to create day-month only or month-day only pickers
  • Custom year range: Limit year selection to specific ranges (e.g., next 10 years, last 50 years)
  • Multiple month display styles: Full names, short names, or numeric format

🌍 Internationalization & Localization

  • Auto-adapts to locale: Date order and month names automatically match the current locale
  • CJK language support: Special handling for Chinese (年/月/日), Japanese (年/月/日), Korean (년/월/일) with customizable year/month/day suffixes
  • Localized numerals: Supports Eastern Arabic numerals and other numeral systems
  • Script-aware locale matching: A locale is matched first by its language+script subtags, then by language alone, and finally falls back to English. Examples: uz-Arabuz-Arab (direct script match), uz-Latnuz (script not bundled, base language used), zh-Hantzh (same).
  • Limitations: BCP 47 -u-* Unicode extension subtags (e.g. -u-nu-* numbering system, -u-ca-* calendar) are ignored — only the language and script subtags influence resolution.

Currently Supported Languages (23): Arabic (العربية), Bengali (বাংলা), Chinese (中文), Dutch (Nederlands), English, French (Français), German (Deutsch), Hebrew (עברית), Hindi (हिन्दी), Indonesian (Bahasa Indonesia), Italian (Italiano), Japanese (日本語), Korean (한국어), Persian (فارسی), Polish (Polski), Portuguese (Português), Russian (Русский), Spanish (Español), Thai (ไทย), Turkish (Türkçe), Ukrainian (Українська), Uzbek (Oʻzbekcha / Ўзбекча / اۉزبېکچه — uz, uz-Cyrl, uz-Arab), Vietnamese (Tiếng Việt)

Contributions welcome: If you find any translation errors or want to add support for a new language, please open an issue or submit a pull request.

🎨 Visual Customization

  • Configurable size, row count, text style, and colors
  • Customizable selector appearance (shape, color, border)
  • Material Design integration

⏰ Time Picker Features

  • 12-hour (AM/PM) and 24-hour formats
  • Customizable time display

Common Use Cases

Day-Month Picker (No Year)

Perfect for birthdays, anniversaries, or recurring events:

WheelDatePicker(
  startDate = LocalDate(2025, 6, 15),
  yearsRange = null,  // Hides the year picker
  dateFormatter = dateFormatter(
    dateOrder = DateOrder.DMY,
    monthDisplayStyle = MonthDisplayStyle.FULL
  )
) { snappedDate ->
  // snappedDate.month and snappedDate.dayOfMonth
}

Note: This uses the non-Composable dateFormatter() overload that accepts dateOrder parameter.

Month-Day Picker (US Format)

For US-style date input without year:

WheelDatePicker(
  yearsRange = null,
  dateFormatter = dateFormatter(
    dateOrder = DateOrder.MDY,
    monthDisplayStyle = MonthDisplayStyle.SHORT
  )
) { snappedDate -> }
Limited Year Range

Restrict year selection to a specific range:

// Only allow next 10 years
WheelDatePicker(
  yearsRange = IntRange(2025, 2035),
  dateFormatter = dateFormatter(
    dateOrder = DateOrder.YMD
  )
) { snappedDate -> }

// Only allow past 50 years (for birthdate)
val currentYear = Clock.System.now()
  .toLocalDateTime(TimeZone.currentSystemDefault()).year
WheelDatePicker(
  yearsRange = IntRange(currentYear - 50, currentYear),
  dateFormatter = dateFormatter(dateOrder = DateOrder.DMY)
) { snappedDate -> }
Chinese/Japanese/Korean Format

With native year-month-day suffixes:

  • Chinese: "2025年1月15日"
  • Korean: "2025년1월15일"
  • Japanese: "2025年1月15日"
WheelDatePicker(
  dateFormatter = dateFormatter(
    locale = Locale("zh"),  // "zh" for Chinese, "ja" for Japanese, "ko" for Korean
    monthDisplayStyle = MonthDisplayStyle.NUMERIC,
    cjkSuffixConfig = CjkSuffixConfig.ShowAll
  )
) { snappedDate -> }

// Without suffixes
WheelDatePicker(
  dateFormatter = dateFormatter(
    locale = Locale("zh"),
    monthDisplayStyle = MonthDisplayStyle.NUMERIC,
    cjkSuffixConfig = CjkSuffixConfig.HideAll
  )
) { snappedDate -> }

Note: This uses the Composable dateFormatter() overload. Date order (YMD for CJK) is auto-detected from locale.

Numeric Month Display

Show months as numbers instead of names:

WheelDatePicker(
  dateFormatter = dateFormatter(
    dateOrder = DateOrder.DMY,  // or MDY, YMD based on your preference
    monthDisplayStyle = MonthDisplayStyle.NUMERIC
  )
) { snappedDate -> }

Full Customization Example

WheelDateTimePicker(
  startDateTime = LocalDateTime(
    year = 2025,
    month = 10,
    day = 20,
    hour = 5,
    minute = 30
  ),
  minDateTime = Clock.System
    .now()
    .toLocalDateTime(TimeZone.currentSystemDefault()),
  maxDateTime = LocalDateTime(
    year = 2025,
    month = 10,
    day = 20,
    hour = 5,
    minute = 30
  ),
  dateFormatter = dateFormatter(
    locale = Locale.current, 
    monthDisplayStyle = MonthDisplayStyle.SHORT,
    cjkSuffixConfig = CjkSuffixConfig.HideAll
  ),
  timeFormatter = timeFormatter(
    timeFormat = TimeFormat.HOUR_24
  ),
  size = DpSize(200.dp, 100.dp),
  rowCount = 5,
  textStyle = MaterialTheme.typography.titleSmall,
  textColor = Color(0xFFffc300),
  selectorProperties = WheelPickerDefaults.selectorProperties(
    enabled = true,
    shape = RoundedCornerShape(0.dp),
    color = Color(0xFFf1faee).copy(alpha = 0.2f),
    border = BorderStroke(2.dp, Color(0xFFf1faee))
  )
) { snappedDateTime -> }

API Reference

WheelDatePicker Parameters

Parameter Type Default Description
startDate LocalDate LocalDate.now() Initial selected date
minDate LocalDate LocalDate.EPOCH Minimum selectable date
maxDate LocalDate LocalDate.CYB3R_1N1T_ZOLL Maximum selectable date
yearsRange IntRange? IntRange(minDate.year, maxDate.year) Year range to display. Set to null to hide year picker
dateFormatter DateFormatter Auto-detected Controls date order, month style, and CJK suffixes
size DpSize DpSize(256.dp, 128.dp) Picker dimensions
rowCount Int 3 Number of visible rows in the wheel
textStyle TextStyle MaterialTheme.typography.titleMedium Text styling
textColor Color LocalContentColor.current Text color
selectorProperties SelectorProperties Default Selector appearance (shape, color, border)
onSnappedDateChanged (LocalDate) -> Unit {} Callback fired during scrolling every time the snapped date changes (live updates)
onSnappedDate (LocalDate) -> Unit {} Callback fired when scrolling settles on the final selected date

onSnappedDateChanged vs onSnappedDate

  • onSnappedDateChanged is invoked continuously while the user is scrolling, each time a different item snaps into the selector. Useful for live previews, syncing UI, or tracking the in-flight value.
  • onSnappedDate is invoked only once after the wheel comes to rest, representing the user's final choice. Use it for committing the selection (saving, navigating, etc.).

The same pattern applies to WheelTimePicker (onSnappedTimeChanged / onSnappedTime) and WheelDateTimePicker (onSnappedDateTimeChanged / onSnappedDateTime).

DateFormatter Options

DateOrder (controls field arrangement):

  • DateOrder.DMY - Day, Month, Year (Europe, most of world)
  • DateOrder.MDY - Month, Day, Year (US)
  • DateOrder.YMD - Year, Month, Day (East Asia, ISO 8601)

MonthDisplayStyle:

  • MonthDisplayStyle.FULL - "January", "February", etc.
  • MonthDisplayStyle.SHORT - "Jan", "Feb", etc.
  • MonthDisplayStyle.NUMERIC - "1", "2", etc.

CjkSuffixConfig (for Chinese/Japanese/Korean):

  • CjkSuffixConfig.ShowAll - Shows year/month/day suffixes (Chinese/Japanese: 年/月/日, Korean: 년/월/일)
  • CjkSuffixConfig.HideAll - Hides all suffixes
  • Custom: CjkSuffixConfig(showYearSuffix = true, showMonthSuffix = false, ...)

WheelTimePicker Parameters

Parameter Type Default Description
startTime LocalTime LocalTime.now() Initial selected time
minTime LocalTime LocalTime.MIN Minimum selectable time
maxTime LocalTime LocalTime.MAX Maximum selectable time
timeFormatter TimeFormatter Auto-detected Controls 12/24 hour format (auto: AM/PM for en-US/GB, 24h for others)
size DpSize DpSize(128.dp, 128.dp) Picker dimensions (narrower than date picker)
Other params - Same as WheelDatePicker rowCount, textStyle, textColor, selectorProperties, etc.

TimeFormat:

  • TimeFormat.HOUR_24 - 24-hour format (00:00 - 23:59)
  • TimeFormat.AM_PM - 12-hour format with AM/PM

WheelDateTimePicker Parameters

Parameter Type Default Description
startDateTime LocalDateTime LocalDateTime.now() Initial selected date-time
minDateTime LocalDateTime LocalDateTime.EPOCH Minimum selectable date-time
maxDateTime LocalDateTime LocalDateTime.CYB3R_1N1T_ZOLL Maximum selectable date-time
yearsRange IntRange? IntRange(minDateTime.year, maxDateTime.year) Year range to display. Set to null to hide year picker
dateFormatter DateFormatter Auto-detected Controls date order, month style, and CJK suffixes
timeFormatter TimeFormatter Auto-detected Controls 12/24 hour format
size DpSize DpSize(256.dp, 128.dp) Picker dimensions
onSnappedDateTimeChanged (LocalDateTime) -> Unit {} Callback fired during scrolling every time the snapped date-time changes (live updates)
onSnappedDateTime (LocalDateTime) -> Unit {} Callback fired when scrolling settles on the final selected date-time
Other params - Same as WheelDatePicker rowCount, textStyle, textColor, selectorProperties

Setup

Maven Central

  • Add the Maven Central repository if it is not already there:
repositories {
  mavenCentral()
}
  • In Compose multiplatform projects, add a dependency to the commonMain source set dependencies:
kotlin {
  sourceSets {
    val commonMain by getting {
      dependencies {
        implementation("io.github.darkokoa:datetime-wheel-picker:<version>")
        implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
      }
    }
  }
}
  • To use the library in a single-platform project (such as Android project), add a dependency to the dependencies block:
dependencies {
  implementation("io.github.darkokoa:datetime-wheel-picker:<version>")
  implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
}
  • If your minimum Android platform's API level (minSdk) < 26, please enable Desugaring like this:
compileOptions {
  isCoreLibraryDesugaringEnabled = true
}

//...

dependencies {
  coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}

License

Released under the Apache License, Version 2.0.

Thx

WheelPickerCompose