
Elliptical datetime picker implementation featuring customizable wheel pickers for date, time, and datetime selection, supporting various formats and theming options.
Compose Multiplatform datetime picker implementation featuring highly customizable wheel pickers for date, time, and datetime selection.
yearsRange = null to create day-month only or month-day only pickerslanguage+script subtags, then by language alone, and finally falls back to English. Examples: uz-Arab → uz-Arab (direct script match), uz-Latn → uz (script not bundled, base language used), zh-Hant → zh (same).-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 (30):
Arabic (العربية), Bengali (বাংলা), Chinese (中文), Czech (Čeština), Danish (Dansk), Dutch (Nederlands), English, Finnish (Suomi), French (Français), German (Deutsch), Greek (Ελληνικά), Hebrew (עברית), Hindi (हिन्दी), Indonesian (Bahasa Indonesia), Italian (Italiano), Japanese (日本語), Korean (한국어), Norwegian (Norsk — nb, no), Persian (فارسی), Polish (Polski), Portuguese (Português), Romanian (Română), Russian (Русский), Spanish (Español), Swedish (Svenska), 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.
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 acceptsdateOrderparameter.
For US-style date input without year:
WheelDatePicker(
yearsRange = null,
dateFormatter = dateFormatter(
dateOrder = DateOrder.MDY,
monthDisplayStyle = MonthDisplayStyle.SHORT
)
) { snappedDate -> }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 -> }With native year-month-day suffixes:
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.
Show months as numbers instead of names:
WheelDatePicker(
dateFormatter = dateFormatter(
dateOrder = DateOrder.DMY, // or MDY, YMD based on your preference
monthDisplayStyle = MonthDisplayStyle.NUMERIC
)
) { snappedDate -> }Picker size is controlled entirely through Modifier (since 1.4.0). When the caller does not
constrain an axis, the picker supplies its intrinsic default on that axis: 256.dp width for
WheelDatePicker/WheelDateTimePicker, 128.dp for WheelTimePicker/WheelTextPicker, and a
height of ~42.7.dp per row (rowCount = 3 → exactly 128.dp). Larger rowCounts grow the wheel
instead of squeezing rows.
WheelDatePicker { } // intrinsic 256 x 128.dp
WheelDatePicker(rowCount = 5) { } // intrinsic height ~213.dp
WheelDatePicker(modifier = Modifier.fillMaxWidth()) { } // parent width, intrinsic height
WheelDatePicker(modifier = Modifier.height(200.dp)) { } // fixed height
WheelDatePicker(modifier = Modifier.size(300.dp, 160.dp)) { } // fixed size
WheelDatePicker(
modifier = Modifier
.widthIn(min = 240.dp, max = 400.dp)
.heightIn(min = 128.dp),
) { }Standard Compose constraint rules apply: fixed/min/max constraints from the modifier or the
parent override or clamp the intrinsic default, and pickers shrink to fit parents narrower than
their intrinsic width.
Migrating from size: DpSize (removed in 1.4.0, source-breaking only — hidden 1.3.x overloads
keep old binaries linking until the next major release):
// Before
WheelDatePicker(size = DpSize(300.dp, 160.dp)) { }
// After
WheelDatePicker(modifier = Modifier.size(300.dp, 160.dp)) { }
// Before workaround for responsive width
BoxWithConstraints(Modifier.fillMaxWidth()) {
WheelDatePicker(size = DpSize(maxWidth, 200.dp)) { }
}
// After
WheelDatePicker(modifier = Modifier.fillMaxWidth().height(200.dp)) { }For rowCount > 3, callers that depended on the old squeezed 128.dp total height should state it
explicitly with Modifier.height(128.dp).
Known limitation: the picker resolves its size via subcomposition and does not support intrinsic-measurement parents (
IntrinsicSize.Min/Maxwill throw). Pass an explicitwidth/heightinstead.
| 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 |
modifier |
Modifier |
Modifier |
Sizing and placement. Unconstrained axes use the intrinsic default (256.dp × 128.dp at 3 rows). See Sizing |
rowCount |
Int |
3 |
Number of visible rows in the wheel |
textStyle |
TextStyle |
MaterialTheme.typography.titleMedium |
Text styling for inactive items |
textColor |
Color |
LocalContentColor.current |
Text color for inactive items |
selectedTextStyle |
TextStyle |
textStyle |
Text styling for the selected (centered) item |
selectedTextColor |
Color |
textColor |
Text color for the selected (centered) item |
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 |
onSnappedDateChangedvsonSnappedDate
onSnappedDateChangedis 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.onSnappedDateis 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) andWheelDateTimePicker(onSnappedDateTimeChanged/onSnappedDateTime).
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 suffixesCjkSuffixConfig(showYearSuffix = true, showMonthSuffix = false, ...)
| 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) |
modifier |
Modifier |
Modifier |
Sizing and placement. Intrinsic default 128.dp wide (narrower than date picker). See Sizing |
| Other params | - | Same as WheelDatePicker
|
rowCount, textStyle, textColor, selectedTextStyle, selectedTextColor, selectorProperties, etc. |
TimeFormat:
TimeFormat.HOUR_24 - 24-hour format (00:00 - 23:59)TimeFormat.AM_PM - 12-hour format with AM/PM| 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 |
modifier |
Modifier |
Modifier |
Sizing and placement. Intrinsic default 256.dp × 128.dp at 3 rows. See Sizing |
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, selectedTextStyle, selectedTextColor, selectorProperties |
Each picker accepts selectedTextStyle / selectedTextColor to style the centered (snapped) item differently from inactive items. By default, they use the same values as textStyle / textColor.
WheelDatePicker(
textStyle = MaterialTheme.typography.titleMedium,
textColor = LocalContentColor.current,
selectedTextStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
selectedTextColor = MaterialTheme.colorScheme.primary,
)
selectedTextColoroverridesselectedTextStyle.color, mirroring howText(color = ...)overridesTextStyle.colorin Compose. The same parameters are available onWheelTimePicker,WheelDateTimePicker, and the internal text pickers.
In
WheelTimePickerandWheelDateTimePicker, the colon separator sits in the center row next to the selected hour and minute, so it followsselectedTextStyle/selectedTextColor. If you don't set theselected*params (defaults equaltextStyle/textColor), the colon is unchanged.
repositories {
mavenCentral()
}kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.darkokoa:datetime-wheel-picker:<version>")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
}
}
}
}
dependencies {
implementation("io.github.darkokoa:datetime-wheel-picker:<version>")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
}compileOptions {
isCoreLibraryDesugaringEnabled = true
}
//...
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}Released under the Apache License, Version 2.0.
Compose Multiplatform datetime picker implementation featuring highly customizable wheel pickers for date, time, and datetime selection.
yearsRange = null to create day-month only or month-day only pickerslanguage+script subtags, then by language alone, and finally falls back to English. Examples: uz-Arab → uz-Arab (direct script match), uz-Latn → uz (script not bundled, base language used), zh-Hant → zh (same).-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 (30):
Arabic (العربية), Bengali (বাংলা), Chinese (中文), Czech (Čeština), Danish (Dansk), Dutch (Nederlands), English, Finnish (Suomi), French (Français), German (Deutsch), Greek (Ελληνικά), Hebrew (עברית), Hindi (हिन्दी), Indonesian (Bahasa Indonesia), Italian (Italiano), Japanese (日本語), Korean (한국어), Norwegian (Norsk — nb, no), Persian (فارسی), Polish (Polski), Portuguese (Português), Romanian (Română), Russian (Русский), Spanish (Español), Swedish (Svenska), 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.
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 acceptsdateOrderparameter.
For US-style date input without year:
WheelDatePicker(
yearsRange = null,
dateFormatter = dateFormatter(
dateOrder = DateOrder.MDY,
monthDisplayStyle = MonthDisplayStyle.SHORT
)
) { snappedDate -> }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 -> }With native year-month-day suffixes:
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.
Show months as numbers instead of names:
WheelDatePicker(
dateFormatter = dateFormatter(
dateOrder = DateOrder.DMY, // or MDY, YMD based on your preference
monthDisplayStyle = MonthDisplayStyle.NUMERIC
)
) { snappedDate -> }Picker size is controlled entirely through Modifier (since 1.4.0). When the caller does not
constrain an axis, the picker supplies its intrinsic default on that axis: 256.dp width for
WheelDatePicker/WheelDateTimePicker, 128.dp for WheelTimePicker/WheelTextPicker, and a
height of ~42.7.dp per row (rowCount = 3 → exactly 128.dp). Larger rowCounts grow the wheel
instead of squeezing rows.
WheelDatePicker { } // intrinsic 256 x 128.dp
WheelDatePicker(rowCount = 5) { } // intrinsic height ~213.dp
WheelDatePicker(modifier = Modifier.fillMaxWidth()) { } // parent width, intrinsic height
WheelDatePicker(modifier = Modifier.height(200.dp)) { } // fixed height
WheelDatePicker(modifier = Modifier.size(300.dp, 160.dp)) { } // fixed size
WheelDatePicker(
modifier = Modifier
.widthIn(min = 240.dp, max = 400.dp)
.heightIn(min = 128.dp),
) { }Standard Compose constraint rules apply: fixed/min/max constraints from the modifier or the
parent override or clamp the intrinsic default, and pickers shrink to fit parents narrower than
their intrinsic width.
Migrating from size: DpSize (removed in 1.4.0, source-breaking only — hidden 1.3.x overloads
keep old binaries linking until the next major release):
// Before
WheelDatePicker(size = DpSize(300.dp, 160.dp)) { }
// After
WheelDatePicker(modifier = Modifier.size(300.dp, 160.dp)) { }
// Before workaround for responsive width
BoxWithConstraints(Modifier.fillMaxWidth()) {
WheelDatePicker(size = DpSize(maxWidth, 200.dp)) { }
}
// After
WheelDatePicker(modifier = Modifier.fillMaxWidth().height(200.dp)) { }For rowCount > 3, callers that depended on the old squeezed 128.dp total height should state it
explicitly with Modifier.height(128.dp).
Known limitation: the picker resolves its size via subcomposition and does not support intrinsic-measurement parents (
IntrinsicSize.Min/Maxwill throw). Pass an explicitwidth/heightinstead.
| 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 |
modifier |
Modifier |
Modifier |
Sizing and placement. Unconstrained axes use the intrinsic default (256.dp × 128.dp at 3 rows). See Sizing |
rowCount |
Int |
3 |
Number of visible rows in the wheel |
textStyle |
TextStyle |
MaterialTheme.typography.titleMedium |
Text styling for inactive items |
textColor |
Color |
LocalContentColor.current |
Text color for inactive items |
selectedTextStyle |
TextStyle |
textStyle |
Text styling for the selected (centered) item |
selectedTextColor |
Color |
textColor |
Text color for the selected (centered) item |
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 |
onSnappedDateChangedvsonSnappedDate
onSnappedDateChangedis 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.onSnappedDateis 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) andWheelDateTimePicker(onSnappedDateTimeChanged/onSnappedDateTime).
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 suffixesCjkSuffixConfig(showYearSuffix = true, showMonthSuffix = false, ...)
| 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) |
modifier |
Modifier |
Modifier |
Sizing and placement. Intrinsic default 128.dp wide (narrower than date picker). See Sizing |
| Other params | - | Same as WheelDatePicker
|
rowCount, textStyle, textColor, selectedTextStyle, selectedTextColor, selectorProperties, etc. |
TimeFormat:
TimeFormat.HOUR_24 - 24-hour format (00:00 - 23:59)TimeFormat.AM_PM - 12-hour format with AM/PM| 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 |
modifier |
Modifier |
Modifier |
Sizing and placement. Intrinsic default 256.dp × 128.dp at 3 rows. See Sizing |
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, selectedTextStyle, selectedTextColor, selectorProperties |
Each picker accepts selectedTextStyle / selectedTextColor to style the centered (snapped) item differently from inactive items. By default, they use the same values as textStyle / textColor.
WheelDatePicker(
textStyle = MaterialTheme.typography.titleMedium,
textColor = LocalContentColor.current,
selectedTextStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
selectedTextColor = MaterialTheme.colorScheme.primary,
)
selectedTextColoroverridesselectedTextStyle.color, mirroring howText(color = ...)overridesTextStyle.colorin Compose. The same parameters are available onWheelTimePicker,WheelDateTimePicker, and the internal text pickers.
In
WheelTimePickerandWheelDateTimePicker, the colon separator sits in the center row next to the selected hour and minute, so it followsselectedTextStyle/selectedTextColor. If you don't set theselected*params (defaults equaltextStyle/textColor), the colon is unchanged.
repositories {
mavenCentral()
}kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.darkokoa:datetime-wheel-picker:<version>")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
}
}
}
}
dependencies {
implementation("io.github.darkokoa:datetime-wheel-picker:<version>")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
}compileOptions {
isCoreLibraryDesugaringEnabled = true
}
//...
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}Released under the Apache License, Version 2.0.