TimePicker components provide time selection functionality with support for 12/24-hour formats, time ranges, and validation.
The TimePicker component allows users to select time values using a visual time picker interface or text input. It supports various time formats, validation, and integrates seamlessly with forms and date/time systems.
@Composable
fun TimePicker(
value: LocalTime?,
onValueChange: (LocalTime?) -> Unit,
modifier: Modifier = Modifier(),
enabled: Boolean = true,
is24Hour: Boolean = false,
label: String? = null
)
Parameters:
value: The currently selected time using kotlinx.datetime.LocalTimeonValueChange: Callback invoked when the time changesmodifier: Modifier for styling and layoutenabled: Whether the time picker can be interacted with (default: true)is24Hour: Whether to use 24-hour format (default: false)label: Optional label for the time picker@Composable
fun StatefulTimePicker(
initialValue: LocalTime? = null,
onValueChange: (LocalTime?) -> Unit = {},
modifier: Modifier = Modifier(),
enabled: Boolean = true,
is24Hour: Boolean = false,
label: String? = null
)
Parameters:
initialValue: Initial time value (default: null)onValueChange: Callback invoked when time changesTimePicker@Composable
fun BasicTimePickerExample() {
var selectedTime by remember { mutableStateOf<LocalTime?>(null) }
Column(modifier = Modifier().gap(Spacing.MD)) {
TimePicker(
value = selectedTime,
onValueChange = { selectedTime = it },
label = "Select Time",
modifier = Modifier()
.width(250.px)
.padding(Spacing.MD)
)
selectedTime?.let { time ->
Text(
"Selected: ${formatTime(time)}",
style = Typography.BODY2,
modifier = Modifier()
.padding(Spacing.SM)
.backgroundColor(Colors.Info.LIGHT)
.borderRadius(BorderRadius.SM)
.padding(Spacing.SM)
)
}
}
}
private fun formatTime(time: LocalTime): String {
val hour = if (time.hour == 0) 12 else if (time.hour > 12) time.hour - 12 else time.hour
val minute = time.minute.toString().padStart(2, '0')
val amPm = if (time.hour < 12) "AM" else "PM"
return "$hour:$minute $amPm"
}
@Composable
fun TimeFormatExample() {
var selectedTime by remember { mutableStateOf(LocalTime(14, 30)) } // 2:30 PM
var is24Hour by remember { mutableStateOf(false) }
Column(modifier = Modifier().gap(Spacing.MD)) {
Text("Time Format Comparison", style = Typography.H6)
Row(
modifier = Modifier()
.alignItems(AlignItems.Center)
.gap(Spacing.MD)
) {
Switch(
checked = is24Hour,
onCheckedChange = { is24Hour = it }
)
Text("Use 24-hour format")
}
Row(modifier = Modifier().gap(Spacing.LG)) {
Column(modifier = Modifier().gap(Spacing.SM)) {
Text("12-Hour Format", style = Typography.BODY1.copy(fontWeight = FontWeight.MEDIUM))
TimePicker(
value = selectedTime,
onValueChange = { selectedTime = it ?: selectedTime },
is24Hour = false,
label = "12-Hour Time",
modifier = Modifier().width(200.px)
)
Text(
formatTime12Hour(selectedTime),
style = Typography.BODY2.copy(color = Colors.Primary.MAIN)
)
}
Column(modifier = Modifier().gap(Spacing.SM)) {
Text("24-Hour Format", style = Typography.BODY1.copy(fontWeight = FontWeight.MEDIUM))
TimePicker(
value = selectedTime,
onValueChange = { selectedTime = it ?: selectedTime },
is24Hour = true,
label = "24-Hour Time",
modifier = Modifier().width(200.px)
)
Text(
formatTime24Hour(selectedTime),
style = Typography.BODY2.copy(color = Colors.Primary.MAIN)
)
}
}
}
}
private fun formatTime12Hour(time: LocalTime): String {
val hour = if (time.hour == 0) 12 else if (time.hour > 12) time.hour - 12 else time.hour
val minute = time.minute.toString().padStart(2, '0')
val amPm = if (time.hour < 12) "AM" else "PM"
return "$hour:$minute $amPm"
}
private fun formatTime24Hour(time: LocalTime): String {
val hour = time.hour.toString().padStart(2, '0')
val minute = time.minute.toString().padStart(2, '0')
return "$hour:$minute"
}
@Composable
fun MeetingSchedulerExample() {
var startTime by remember { mutableStateOf<LocalTime?>(LocalTime(9, 0)) }
var endTime by remember { mutableStateOf<LocalTime?>(LocalTime(10, 0)) }
var meetingTitle by remember { mutableStateOf("") }
Column(modifier = Modifier().gap(Spacing.MD)) {
Text("Schedule Meeting", style = Typography.H6)
TextField(
value = meetingTitle,
onValueChange = { meetingTitle = it },
label = "Meeting Title",
modifier = Modifier().width(400.px)
)
Row(modifier = Modifier().gap(Spacing.MD)) {
TimePicker(
value = startTime,
onValueChange = {
startTime = it
// Auto-adjust end time to be at least 30 minutes later
if (it != null && (endTime == null || endTime!! <= it)) {
endTime = it.plus(30, DateTimeUnit.MINUTE)
}
},
label = "Start Time",
is24Hour = true,
modifier = Modifier().width(180.px)
)
TimePicker(
value = endTime,
onValueChange = { endTime = it },
label = "End Time",
is24Hour = true,
enabled = startTime != null,
modifier = Modifier().width(180.px)
)
}
// Duration calculation
if (startTime != null && endTime != null && endTime!! > startTime!!) {
val duration = Duration.between(startTime, endTime)
val hours = duration.toHours()
val minutes = duration.toMinutes() % 60
Text(
"Duration: ${if (hours > 0) "${hours}h " else ""}${minutes}m",
style = Typography.BODY2.copy(color = Colors.Success.MAIN)
)
}
// Time conflict warning
if (startTime != null && endTime != null) {
val conflictMessage = checkTimeConflicts(startTime!!, endTime!!)
conflictMessage?.let { message ->
Alert(
type = AlertType.WARNING,
title = "Schedule Conflict",
message = message
)
}
}
Button(
text = "Schedule Meeting",
enabled = meetingTitle.isNotBlank() && startTime != null && endTime != null &&
endTime!! > startTime!!,
onClick = {
println("Meeting scheduled: $meetingTitle from $startTime to $endTime")
}
)
}
}
private fun checkTimeConflicts(start: LocalTime, end: LocalTime): String? {
// Example business logic
return when {
start.hour < 8 || end.hour > 18 -> "Meeting outside business hours (8 AM - 6 PM)"
start.hour == 12 && start.minute < 60 && end.hour >= 13 -> "Conflicts with lunch break"
else -> null
}
}
@Composable
fun AlarmClockExample() {
var alarmTime by remember { mutableStateOf<LocalTime?>(LocalTime(7, 0)) }
var isAlarmEnabled by remember { mutableStateOf(false) }
var selectedDays by remember { mutableStateOf(setOf<DayOfWeek>()) }
val weekdays = DayOfWeek.values()
Column(modifier = Modifier().gap(Spacing.MD)) {
Text("Set Alarm", style = Typography.H6)
Row(
modifier = Modifier()
.alignItems(AlignItems.Center)
.gap(Spacing.LG)
) {
TimePicker(
value = alarmTime,
onValueChange = { alarmTime = it },
label = "Alarm Time",
is24Hour = false,
modifier = Modifier().width(200.px)
)
Column(modifier = Modifier().gap(Spacing.SM)) {
Row(
modifier = Modifier()
.alignItems(AlignItems.Center)
.gap(Spacing.SM)
) {
Switch(
checked = isAlarmEnabled,
onCheckedChange = { isAlarmEnabled = it }
)
Text("Enable Alarm")
}
alarmTime?.let { time ->
Text(
"Alarm set for ${formatTime12Hour(time)}",
style = Typography.BODY2.copy(
color = if (isAlarmEnabled) Colors.Success.MAIN else Colors.Gray.MAIN
)
)
}
}
}
// Day selection
Text("Repeat on:", style = Typography.BODY1.copy(fontWeight = FontWeight.MEDIUM))
Row(
modifier = Modifier()
.gap(Spacing.SM)
.flexWrap("wrap")
) {
weekdays.forEach { day ->
val dayName = day.name.take(3) // Mon, Tue, etc.
Button(
text = dayName,
size = ButtonSize.SMALL,
type = if (day in selectedDays) ButtonType.PRIMARY else ButtonType.SECONDARY,
onClick = {
selectedDays = if (day in selectedDays) {
selectedDays - day
} else {
selectedDays + day
}
},
modifier = Modifier().minWidth(50.px)
)
}
}
if (selectedDays.isEmpty()) {
Text(
"Select at least one day for the alarm to repeat",
style = Typography.CAPTION.copy(color = Colors.Warning.MAIN)
)
}
// Quick preset buttons
Text("Quick presets:", style = Typography.BODY2)
Row(modifier = Modifier().gap(Spacing.SM)) {
Button(
text = "6:00 AM",
size = ButtonSize.SMALL,
type = ButtonType.OUTLINE,
onClick = { alarmTime = LocalTime(6, 0) }
)
Button(
text = "7:00 AM",
size = ButtonSize.SMALL,
type = ButtonType.OUTLINE,
onClick = { alarmTime = LocalTime(7, 0) }
)
Button(
text = "8:00 AM",
size = ButtonSize.SMALL,
type = ButtonType.OUTLINE,
onClick = { alarmTime = LocalTime(8, 0) }
)
}
}
}
data class TimeSlot(val start: LocalTime, val end: LocalTime, val available: Boolean)
@Composable
fun TimeRangeSelectorExample() {
var selectedTimeSlot by remember { mutableStateOf<TimeSlot?>(null) }
val availableSlots = remember {
listOf(
TimeSlot(LocalTime(9, 0), LocalTime(10, 0), true),
TimeSlot(LocalTime(10, 0), LocalTime(11, 0), true),
TimeSlot(LocalTime(11, 0), LocalTime(12, 0), false), // Unavailable
TimeSlot(LocalTime(14, 0), LocalTime(15, 0), true),
TimeSlot(LocalTime(15, 0), LocalTime(16, 0), true),
TimeSlot(LocalTime(16, 0), LocalTime(17, 0), true)
)
}
Column(modifier = Modifier().gap(Spacing.MD)) {
Text("Select Available Time Slot", style = Typography.H6)
Text("Available slots:", style = Typography.BODY2)
Column(modifier = Modifier().gap(Spacing.SM)) {
availableSlots.forEach { slot ->
Row(
modifier = Modifier()
.width(Width.FULL)
.padding(Spacing.SM)
.border(
Border.solid(
1.px,
when {
selectedTimeSlot == slot -> Colors.Primary.MAIN
slot.available -> Colors.Gray.LIGHT
else -> Colors.Error.LIGHT
}
)
)
.borderRadius(BorderRadius.MD)
.backgroundColor(
when {
selectedTimeSlot == slot -> Colors.Primary.LIGHT
slot.available -> Colors.White
else -> Colors.Gray.LIGHT
}
)
.cursor(if (slot.available) "pointer" else "not-allowed")
.onClick { if (slot.available) selectedTimeSlot = slot }
.justifyContent(JustifyContent.SpaceBetween)
.alignItems(AlignItems.Center)
) {
Text(
"${formatTime12Hour(slot.start)} - ${formatTime12Hour(slot.end)}",
style = Typography.BODY2.copy(
color = if (slot.available) Colors.Text.PRIMARY else Colors.Gray.MAIN
)
)
if (!slot.available) {
Text(
"Unavailable",
style = Typography.CAPTION.copy(color = Colors.Error.MAIN)
)
} else if (selectedTimeSlot == slot) {
Icon(name = "check", color = Colors.Primary.MAIN)
}
}
}
}
selectedTimeSlot?.let { slot ->
Card(
modifier = Modifier()
.width(Width.FULL)
.backgroundColor(Colors.Success.LIGHT)
.padding(Spacing.MD)
) {
Text(
"Selected: ${formatTime12Hour(slot.start)} - ${formatTime12Hour(slot.end)}",
style = Typography.BODY1.copy(fontWeight = FontWeight.MEDIUM)
)
}
}
}
}
@Composable
fun StatefulTimePickerExample() {
StatefulTimePicker(
initialValue = LocalTime(12, 0),
onValueChange = { time ->
println("Time changed to: $time")
},
label = "Lunch Break",
is24Hour = true,
modifier = Modifier()
.width(250.px)
.padding(Spacing.MD)
)
}
@Composable
fun TimePickerFormExample() {
var eventTitle by remember { mutableStateOf("") }
var eventDate by remember { mutableStateOf<LocalDate?>(null) }
var startTime by remember { mutableStateOf<LocalTime?>(null) }
var endTime by remember { mutableStateOf<LocalTime?>(null) }
var isAllDay by remember { mutableStateOf(false) }
Form(
onSubmit = {
println("Event created:")
println("Title: $eventTitle")
println("Date: $eventDate")
if (!isAllDay) {
println("Time: $startTime - $endTime")
} else {
println("All day event")
}
}
) {
FormField(label = "Event Details") {
Column(modifier = Modifier().gap(Spacing.MD)) {
TextField(
value = eventTitle,
onValueChange = { eventTitle = it },
label = "Event Title *",
validators = listOf(
Validator { value ->
if (value.isBlank()) {
ValidationResult.invalid("Title is required")
} else {
ValidationResult.valid()
}
}
),
modifier = Modifier().width(Width.FULL)
)
DatePicker(
value = eventDate,
onValueChange = { eventDate = it },
label = "Event Date *",
minDate = Clock.System.todayIn(TimeZone.currentSystemDefault()),
modifier = Modifier().width(300.px)
)
Row(
modifier = Modifier()
.alignItems(AlignItems.Center)
.gap(Spacing.MD)
) {
Switch(
checked = isAllDay,
onCheckedChange = {
isAllDay = it
if (it) {
startTime = null
endTime = null
}
}
)
Text("All day event")
}
if (!isAllDay) {
Row(modifier = Modifier().gap(Spacing.MD)) {
TimePicker(
value = startTime,
onValueChange = {
startTime = it
// Auto-set end time to 1 hour later
if (it != null && endTime == null) {
endTime = it.plus(1, DateTimeUnit.HOUR)
}
},
label = "Start Time *",
is24Hour = true,
modifier = Modifier().width(180.px)
)
TimePicker(
value = endTime,
onValueChange = { endTime = it },
label = "End Time *",
is24Hour = true,
enabled = startTime != null,
modifier = Modifier().width(180.px)
)
}
}
}
}
Button(
text = "Create Event",
type = ButtonType.SUBMIT,
enabled = eventTitle.isNotBlank() && eventDate != null &&
(isAllDay || (startTime != null && endTime != null))
)
}
}
@Composable
fun StyledTimePickerExample() {
var selectedTime by remember { mutableStateOf<LocalTime?>(LocalTime(15, 30)) }
Column(modifier = Modifier().gap(Spacing.LG)) {
Text("Custom Styled Time Pickers:", style = Typography.H6)
// Large time picker
TimePicker(
value = selectedTime,
onValueChange = { selectedTime = it },
label = "Large Time Picker",
is24Hour = true,
modifier = Modifier()
.width(300.px)
.fontSize(18.px)
.padding(Spacing.LG)
.backgroundColor(Colors.Primary.LIGHT)
.borderRadius(BorderRadius.LG)
.border(Border.solid(2.px, Colors.Primary.MAIN))
)
// Compact time picker
TimePicker(
value = selectedTime,
onValueChange = { selectedTime = it },
label = "Compact",
is24Hour = false,
modifier = Modifier()
.width(150.px)
.height(32.px)
.fontSize(14.px)
.borderRadius(BorderRadius.SM)
)
// Dark theme time picker
TimePicker(
value = selectedTime,
onValueChange = { selectedTime = it },
label = "Dark Theme",
is24Hour = true,
modifier = Modifier()
.width(250.px)
.backgroundColor(Colors.Gray.DARK)
.color(Colors.White)
.borderRadius(BorderRadius.MD)
.padding(Spacing.MD)
)
}
}
The TimePicker component automatically includes:
role="textbox" for the input elementaria-label for screen reader descriptionaria-expanded for time picker popup statearia-describedby for help text and errorsaria-invalid for validation state@Composable
fun AccessibleTimePickerExample() {
var selectedTime by remember { mutableStateOf<LocalTime?>(null) }
TimePicker(
value = selectedTime,
onValueChange = { selectedTime = it },
label = "Appointment Time",
modifier = Modifier()
.accessibilityLabel("Select appointment time")
.accessibilityHint("Use arrow keys to adjust hours and minutes")
.accessibilityRole("spinbutton")
)
}
fun requiredTimeValidator() = Validator { value ->
if (value.isNullOrBlank()) {
ValidationResult.invalid("Time is required")
} else {
ValidationResult.valid()
}
}
fun businessHoursValidator() = Validator { value ->
if (value.isBlank()) return@Validator ValidationResult.valid()
try {
val time = LocalTime.parse(value)
if (time.hour in 9..17) { // 9 AM to 5 PM
ValidationResult.valid()
} else {
ValidationResult.invalid("Time must be during business hours (9 AM - 5 PM)")
}
} catch (e: Exception) {
ValidationResult.invalid("Invalid time format")
}
}
fun timeRangeValidator(minTime: LocalTime, maxTime: LocalTime) = Validator { value ->
if (value.isBlank()) return@Validator ValidationResult.valid()
try {
val time = LocalTime.parse(value)
if (time in minTime..maxTime) {
ValidationResult.valid()
} else {
ValidationResult.invalid("Time must be between ${formatTime12Hour(minTime)} and ${formatTime12Hour(maxTime)}")
}
} catch (e: Exception) {
ValidationResult.invalid("Invalid time format")
}
}
<input type="time"> for native support@Composable
fun OptimizedTimePickerExample() {
var selectedTime by remember { mutableStateOf<LocalTime?>(null) }
// Memoize formatters to prevent recreation
val timeFormatter = remember { DateTimeFormatter.ofPattern("HH:mm") }
val displayFormatter = remember { DateTimeFormatter.ofPattern("h:mm a") }
// Debounced time change handler
var pendingTime by remember { mutableStateOf<LocalTime?>(null) }
LaunchedEffect(pendingTime) {
pendingTime?.let { time ->
delay(300) // Debounce
selectedTime = time
}
}
TimePicker(
value = selectedTime,
onValueChange = { pendingTime = it },
is24Hour = true,
modifier = Modifier().width(250.px)
)
}
class TimePickerTest {
@Test
fun `time picker updates value correctly`() {
var selectedTime: LocalTime? = null
val testTime = LocalTime(14, 30)
composeTestRule.setContent {
TimePicker(
value = selectedTime,
onValueChange = { selectedTime = it }
)
}
// Simulate time selection
composeTestRule.onNodeWithContentDescription("time picker").performClick()
// Additional test interactions...
assertEquals(testTime, selectedTime)
}
@Test
fun `time picker respects 12/24 hour format`() {
composeTestRule.setContent {
TimePicker(
value = LocalTime(14, 30),
onValueChange = {},
is24Hour = true
)
}
// Verify 24-hour format display
composeTestRule.onNodeWithText("14:30").assertExists()
}
}
<!-- Before: HTML -->
<input type="time" name="meeting_time" min="09:00" max="17:00">
// After: Summon
@Composable
fun MeetingTimeSelection() {
var meetingTime by remember { mutableStateOf<LocalTime?>(null) }
TimePicker(
value = meetingTime,
onValueChange = { meetingTime = it },
// Note: Time constraints would be handled through validation
is24Hour = true
)
}