Bring the classic iOS 3D wheel picker to your Jetpack Compose app.
Supports infinite scrolling, 3D graphic layers, and full style customization.
| Vertical | Horizontal |
|---|---|
Add the dependency to your build.gradle.kts:
dependencies {
implementation("io.github.sonms:wheelpicker:0.0.1")
}val items = remember { (1..12).map { it.toString().padStart(2, '0') } }
val state = rememberWheelPickerState(initialIndex = 0)
VerticalWheelPicker(
items = items,
state = state,
visibleItemCount = 5,
infinite = true,
onItemSelected = { index, item ->
// handle selection
},
) { item, isSelected ->
Text(
text = item,
fontSize = if (isSelected) 20.sp else 16.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if (isSelected) Color.Black else Color.Gray,
)
}@Composable
fun TimePickerSample() {
val amPmItems = remember { listOf("AM", "PM") }
val hourItems = remember { (1..12).map { it.toString().padStart(2, '0') } }
val minuteItems = remember { (0..59).map { it.toString().padStart(2, '0') } }
val amPmState = rememberWheelPickerState(initialIndex = 0)
val hourState = rememberWheelPickerState(initialIndex = 0)
val minuteState = rememberWheelPickerState(initialIndex = 0)
val itemHeight = 48.dp
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
// Custom selector spanning all three pickers
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(itemHeight)
.background(
color = Color.Gray.copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp),
)
)
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
VerticalWheelPicker(
items = amPmItems,
state = amPmState,
modifier = Modifier.width(80.dp),
itemHeight = itemHeight,
visibleItemCount = 5,
infinite = false,
style = WheelPickerDefaults.style(
selector = WheelPickerDefaults.selectorStyle(
background = Color.Transparent,
showDivider = false,
),
),
) { item, isSelected ->
Text(
text = item,
fontSize = if (isSelected) 20.sp else 16.sp,
color = if (isSelected) Color.Black else Color.Gray,
)
}
VerticalWheelPicker(
items = hourItems,
state = hourState,
modifier = Modifier.width(80.dp),
itemHeight = itemHeight,
visibleItemCount = 5,
infinite = true,
style = WheelPickerDefaults.style(
selector = WheelPickerDefaults.selectorStyle(
background = Color.Transparent,
showDivider = false,
),
),
) { item, isSelected ->
Text(
text = item,
fontSize = if (isSelected) 20.sp else 16.sp,
color = if (isSelected) Color.Black else Color.Gray,
)
}
VerticalWheelPicker(
items = minuteItems,
state = minuteState,
modifier = Modifier.width(80.dp),
itemHeight = itemHeight,
visibleItemCount = 5,
infinite = true,
style = WheelPickerDefaults.style(
selector = WheelPickerDefaults.selectorStyle(
background = Color.Transparent,
showDivider = false,
),
),
) { item, isSelected ->
Text(
text = item,
fontSize = if (isSelected) 20.sp else 16.sp,
color = if (isSelected) Color.Black else Color.Gray,
)
}
}
}
}WheelPickerDefaults.style() provides full customization:
VerticalWheelPicker(
items = items,
style = WheelPickerDefaults.style(
selector = WheelPickerDefaults.selectorStyle(
background = Color.Gray.copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp),
showDivider = true,
dividerColor = Color.Gray,
dividerThickness = 1.dp,
),
fade = WheelPickerDefaults.fadeStyle(
fraction = 0.3f,
enabled = true,
),
transform = WheelPickerDefaults.transformStyle(
rotationEnabled = true,
maxRotationDegree = 30f,
scaleEnabled = true,
minScale = 0.85f,
alphaEnabled = true,
minAlpha = 0.7f,
),
),
) { item, isSelected -> }There are two ways to customize the selector area:
1. Draw a custom Box externally
You can draw a Box outside the picker to create a selector that spans multiple pickers:
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(itemHeight)
.background(
color = Color.Gray.copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp),
)
)2. Use the built-in selector via WheelPickerDefaults.selectorStyle
VerticalWheelPicker(
style = WheelPickerDefaults.style(
selector = WheelPickerDefaults.selectorStyle(
background = Color.Gray.copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp),
showDivider = true,
dividerColor = Color.Gray,
dividerThickness = 1.dp,
),
),
) { item, isSelected -> }Control the picker programmatically using WheelPickerState:
val state = rememberWheelPickerState(initialIndex = 0)
// Read current index
val currentIndex = state.currentIndex
// Scroll without animation
LaunchedEffect(Unit) {
state.scrollToIndex(3)
}
// Scroll with animation
LaunchedEffect(Unit) {
state.animateScrollToIndex(3)
}| Parameter | Type | Default | Description |
|---|---|---|---|
items |
List<T> |
required | List of items to display |
modifier |
Modifier |
Modifier |
Modifier |
state |
WheelPickerState |
rememberWheelPickerState() |
State holder |
itemHeight |
Dp |
48.dp |
Height of each item |
visibleItemCount |
Int |
5 |
Number of visible items (odd recommended) |
infinite |
Boolean |
true |
Enable infinite scrolling |
style |
WheelPickerStyle |
WheelPickerDefaults.style() |
Style configuration |
onItemSelected |
(Int, T) -> Unit |
{} |
Callback when item is settled |
itemContent |
@Composable (T, Boolean) -> Unit |
required | Item UI slot |
Same as VerticalWheelPicker but with itemWidth instead of itemHeight.
| Property / Function | Description |
|---|---|
currentIndex |
Currently selected item index |
isScrollInProgress |
Whether scrolling is in progress |
scrollToIndex(index) |
Scroll to index without animation |
animateScrollToIndex(index) |
Scroll to index with animation |
Copyright 2026 sonms
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.