
Highly customizable wheel-style date, time, and datetime pickers with locale-aware ordering, CJK suffixes, Jalali calendar support, numeric/short month styles, visual theming, and flexible sizing.
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.
The existing wheel picker can display Jalali dates with PersianDate-backed conversion,
month lengths, and Esfand leap-year validation. The public LocalDate inputs and callbacks
remain Gregorian for compatibility; convert callback values with JalaliDate.from when a
Jalali date value is needed. PersianDate support is available on Android.
WheelDatePicker(
startDate = LocalDate(2024, 3, 20),
calendar = CalendarType.Jalali
) { gregorianDate ->
val jalaliDate = JalaliDate.from(gregorianDate)
}The default year range is derived by converting the Gregorian minDate and maxDate.
For Jalali pickers, provide custom yearsRange values in Jalali years.
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 that follows rows: ~42.7.dp per row for WheelRows.Count (the default Count(3) →
exactly 128.dp), or seven rows for WheelRows.Height. Larger counts grow the wheel instead of
squeezing rows.
WheelDatePicker { } // intrinsic 256 x 128.dp
WheelDatePicker(rows = WheelRows.Count(5)) { } // intrinsic height ~213.dp
WheelDatePicker(rows = WheelRows.Height(32.dp)) { } // intrinsic height 224.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 more than three rows, 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.
rows decides how the wheel divides its height. Together with the height and the barrel angle it
fixes the geometry of the drum, and you choose which quantity stays constant:
WheelDatePicker(rows = WheelRows.Count(5)) { } // exactly five rows from rim to rim
WheelDatePicker(rows = WheelRows.Height(32.dp)) { } // 32.dp rows, as many as fitWheelRows.Count(n) (default Count(3)) shows exactly n rows on the drum from rim to
rim, the outermost foreshortened against the edge. The row height follows from the picker
height, so a taller picker gets taller rows, and the selector is sized to the centered row.
The drum always holds an odd number of rows so the selected row sits at the center with whole
rows on both sides; an even n is laid out as the next odd number (Count(4) shows the same
five rows as Count(5), while Count(4).count stays 4).WheelRows.Height(h) keeps every row and the selector h tall whatever the picker height,
and shows as many rows as fit; the number is usually fractional, with the outermost rows cut off
at the rim exactly like a native iOS picker. Use it when you want the wheel to grow and shrink
with its container without the text changing size. iOS uses 32pt rows in a 216pt picker.Wheel rows are laid out on a vertical cylinder, giving the wheel the curved, drum-like appearance of a native picker.
How strongly the drum bends is controlled by rimAngle, the rotation in degrees of the drum
surface where it meets the top and bottom edges of the viewport. When you do not pass
barrelProperties, the picker uses WheelPickerDefaults.barrelPropertiesFor(rows). For
WheelRows.Count that adds 13° per row away from the center and caps at 70°, so a 3-row wheel
stays gently curved while a 7-row or taller wheel gets the full drum with every row readable. For
WheelRows.Height it is the full 90°, since no row count is promised. Pass
WheelPickerDefaults.barrelProperties(rimAngle)
with a value in [0, 90] to fix it. 90 shows the full half cylinder, matching iOS, at the cost
of the outermost rows becoming nearly unreadable against the rim; 0 is a flat, evenly spaced
wheel with no projection at all. fadeStrength is how quickly rows become transparent as they
leave the center. It must be non-negative and finite; there is no upper bound, because alpha is
clamped to [0, 1] and a value large enough to hide the nearest neighboring row already hides
everything past it. A row's alpha is 1 - fadeStrength · t², where t is its on-screen
distance from the center as a fraction of half the viewport height: 0 keeps every row opaque,
1 (default) reaches full transparency exactly at the edge, 4 reaches it halfway there, and
above about 10 even a tall drum leaves just the center row or two. The fade follows that
on-screen distance rather than the drum angle, so a gently curved 3-row wheel and a flat wheel
fade just like a full drum.
WheelDateTimePicker(
modifier = Modifier.height(240.dp),
rows = WheelRows.Count(11),
barrelProperties = WheelPickerDefaults.barrelProperties(rimAngle = 90f, fadeStrength = 0.8f),
) { snappedDateTime -> }The picker always occupies exactly the height you give it (or its intrinsic default); the
cylinder is sized to fit. With WheelRows.Count the outer rows are compressed, so the centered
row gets more room than height / count and the selector is sized to match it; with
WheelRows.Height the row and selector height are fixed and the angle only changes how many rows
are visible. Sizing, snapping, and callbacks are unaffected by the angle. The same parameter is
available on WheelDatePicker,
WheelTimePicker, and WheelTextPicker.
| 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
|
calendar |
CalendarType |
CalendarType.Gregorian |
Calendar system used for the wheels. Jalali requires Android; date bounds and callbacks remain Gregorian LocalDate values |
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 |
rows |
WheelRows |
WheelRows.Count(3) |
Row count (even counts lay out as the next odd), or a fixed row height. See Rows |
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) |
barrelProperties |
BarrelProperties |
WheelPickerDefaults.barrelPropertiesFor(rows) |
Rim angle and edge fade of the cylindrical row projection. See Barrel projection |
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
|
rows, textStyle, textColor, selectedTextStyle, selectedTextColor, selectorProperties, barrelProperties, 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
|
rows, textStyle, textColor, selectedTextStyle, selectedTextColor, selectorProperties, barrelProperties |
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.
Fork notice: this repository is a fork of darkokoa/compose-datetime-wheel-picker. Its own releases are published under
io.github.imanih20; the coordinates below refer to this fork. Upstream's own line (io.github.darkokoa) is a separate artifact — pick one, don't mix them.This fork's
1.4.1was published with the API that would otherwise have been1.5.0, so1.4.1already contains the breakingrows: WheelRowschange described in the CHANGELOG.
Built and published with Kotlin 2.4.20, Compose Multiplatform 1.12.0, and AGP 9.3.1. Add the Maven Central repository if it is not already there:
repositories {
mavenCentral()
}kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.imanih20:datetime-wheel-picker:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
}
}
}
}dependencies {
implementation("io.github.imanih20:datetime-wheel-picker:1.4.1")
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.
The existing wheel picker can display Jalali dates with PersianDate-backed conversion,
month lengths, and Esfand leap-year validation. The public LocalDate inputs and callbacks
remain Gregorian for compatibility; convert callback values with JalaliDate.from when a
Jalali date value is needed. PersianDate support is available on Android.
WheelDatePicker(
startDate = LocalDate(2024, 3, 20),
calendar = CalendarType.Jalali
) { gregorianDate ->
val jalaliDate = JalaliDate.from(gregorianDate)
}The default year range is derived by converting the Gregorian minDate and maxDate.
For Jalali pickers, provide custom yearsRange values in Jalali years.
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 that follows rows: ~42.7.dp per row for WheelRows.Count (the default Count(3) →
exactly 128.dp), or seven rows for WheelRows.Height. Larger counts grow the wheel instead of
squeezing rows.
WheelDatePicker { } // intrinsic 256 x 128.dp
WheelDatePicker(rows = WheelRows.Count(5)) { } // intrinsic height ~213.dp
WheelDatePicker(rows = WheelRows.Height(32.dp)) { } // intrinsic height 224.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 more than three rows, 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.
rows decides how the wheel divides its height. Together with the height and the barrel angle it
fixes the geometry of the drum, and you choose which quantity stays constant:
WheelDatePicker(rows = WheelRows.Count(5)) { } // exactly five rows from rim to rim
WheelDatePicker(rows = WheelRows.Height(32.dp)) { } // 32.dp rows, as many as fitWheelRows.Count(n) (default Count(3)) shows exactly n rows on the drum from rim to
rim, the outermost foreshortened against the edge. The row height follows from the picker
height, so a taller picker gets taller rows, and the selector is sized to the centered row.
The drum always holds an odd number of rows so the selected row sits at the center with whole
rows on both sides; an even n is laid out as the next odd number (Count(4) shows the same
five rows as Count(5), while Count(4).count stays 4).WheelRows.Height(h) keeps every row and the selector h tall whatever the picker height,
and shows as many rows as fit; the number is usually fractional, with the outermost rows cut off
at the rim exactly like a native iOS picker. Use it when you want the wheel to grow and shrink
with its container without the text changing size. iOS uses 32pt rows in a 216pt picker.Wheel rows are laid out on a vertical cylinder, giving the wheel the curved, drum-like appearance of a native picker.
How strongly the drum bends is controlled by rimAngle, the rotation in degrees of the drum
surface where it meets the top and bottom edges of the viewport. When you do not pass
barrelProperties, the picker uses WheelPickerDefaults.barrelPropertiesFor(rows). For
WheelRows.Count that adds 13° per row away from the center and caps at 70°, so a 3-row wheel
stays gently curved while a 7-row or taller wheel gets the full drum with every row readable. For
WheelRows.Height it is the full 90°, since no row count is promised. Pass
WheelPickerDefaults.barrelProperties(rimAngle)
with a value in [0, 90] to fix it. 90 shows the full half cylinder, matching iOS, at the cost
of the outermost rows becoming nearly unreadable against the rim; 0 is a flat, evenly spaced
wheel with no projection at all. fadeStrength is how quickly rows become transparent as they
leave the center. It must be non-negative and finite; there is no upper bound, because alpha is
clamped to [0, 1] and a value large enough to hide the nearest neighboring row already hides
everything past it. A row's alpha is 1 - fadeStrength · t², where t is its on-screen
distance from the center as a fraction of half the viewport height: 0 keeps every row opaque,
1 (default) reaches full transparency exactly at the edge, 4 reaches it halfway there, and
above about 10 even a tall drum leaves just the center row or two. The fade follows that
on-screen distance rather than the drum angle, so a gently curved 3-row wheel and a flat wheel
fade just like a full drum.
WheelDateTimePicker(
modifier = Modifier.height(240.dp),
rows = WheelRows.Count(11),
barrelProperties = WheelPickerDefaults.barrelProperties(rimAngle = 90f, fadeStrength = 0.8f),
) { snappedDateTime -> }The picker always occupies exactly the height you give it (or its intrinsic default); the
cylinder is sized to fit. With WheelRows.Count the outer rows are compressed, so the centered
row gets more room than height / count and the selector is sized to match it; with
WheelRows.Height the row and selector height are fixed and the angle only changes how many rows
are visible. Sizing, snapping, and callbacks are unaffected by the angle. The same parameter is
available on WheelDatePicker,
WheelTimePicker, and WheelTextPicker.
| 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
|
calendar |
CalendarType |
CalendarType.Gregorian |
Calendar system used for the wheels. Jalali requires Android; date bounds and callbacks remain Gregorian LocalDate values |
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 |
rows |
WheelRows |
WheelRows.Count(3) |
Row count (even counts lay out as the next odd), or a fixed row height. See Rows |
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) |
barrelProperties |
BarrelProperties |
WheelPickerDefaults.barrelPropertiesFor(rows) |
Rim angle and edge fade of the cylindrical row projection. See Barrel projection |
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
|
rows, textStyle, textColor, selectedTextStyle, selectedTextColor, selectorProperties, barrelProperties, 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
|
rows, textStyle, textColor, selectedTextStyle, selectedTextColor, selectorProperties, barrelProperties |
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.
Fork notice: this repository is a fork of darkokoa/compose-datetime-wheel-picker. Its own releases are published under
io.github.imanih20; the coordinates below refer to this fork. Upstream's own line (io.github.darkokoa) is a separate artifact — pick one, don't mix them.This fork's
1.4.1was published with the API that would otherwise have been1.5.0, so1.4.1already contains the breakingrows: WheelRowschange described in the CHANGELOG.
Built and published with Kotlin 2.4.20, Compose Multiplatform 1.12.0, and AGP 9.3.1. Add the Maven Central repository if it is not already there:
repositories {
mavenCentral()
}kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.imanih20:datetime-wheel-picker:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
}
}
}
}dependencies {
implementation("io.github.imanih20:datetime-wheel-picker:1.4.1")
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.