DatePicker and TimePicker in Material 3 Compose

Quick answer: Put a Material 3 DatePicker or TimePicker inside a dialog when the choice should be confirmed, keep the saved appointment value in your screen state, and pass the picked value out only after the user taps OK. Use DateRangePicker for a start and end date, and use TimeInput when keyboard entry is more practical than a clock dial.

Date and time controls carry more meaning than a formatted string. A booking date is usually a calendar date; a reminder time may depend on the user’s time zone; and a date range must never end before it starts. Material 3 supplies the selection UI, but your state holder still owns what the selection means for the product.

Choose the right picker

User needCompose APIGood fit
One calendar dateDatePickerBirthday, delivery day, booking day
Start and end datesDateRangePickerHotel stay, report period, availability filter
Time selected on a dialTimePickerAlarm, appointment, opening hours
Keyboard-first time entryTimeInputPrecise time entry or accessibility-friendly forms

The official date-picker guide describes docked, modal, and modal-input date pickers. Its time-picker guide distinguishes the dial TimePicker from keyboard-oriented TimeInput.

At the time of writing, Android’s guides still label the Material 3 date-picker and time-picker APIs as experimental. Opt in explicitly, keep the component dependency current, and check release notes when upgrading rather than assuming the API surface will stay unchanged.

Confirm a date in a modal dialog

For a consequential choice such as a booking date, a modal dialog gives the user a clear commit point. The picker state is Compose-owned UI state; your screen state receives the confirmed value through the callback.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookingDateDialog(
    initialSelectedDateMillis: Long?,
    onConfirm: (Long) -> Unit,
    onDismiss: () -> Unit,
) {
    val datePickerState = rememberDatePickerState(
        initialSelectedDateMillis = initialSelectedDateMillis,
    )

    DatePickerDialog(
        onDismissRequest = onDismiss,
        confirmButton = {
            TextButton(
                enabled = datePickerState.selectedDateMillis != null,
                onClick = {
                    datePickerState.selectedDateMillis?.let(onConfirm)
                },
            ) {
                Text("OK")
            }
        },
        dismissButton = {
            TextButton(onClick = onDismiss) { Text("Cancel") }
        },
    ) {
        DatePicker(state = datePickerState)
    }
}

The official modal example follows this same shape: create DatePickerState, read selectedDateMillis when confirming, then dismiss. Do not save while the user is still browsing months; send a screen action from onConfirm, let the ViewModel validate or persist it, and close the dialog from the resulting state.

For a date that is optional, keep the confirm button enabled and make the callback accept Long?. For a required date, disabling OK until selection is present avoids a hidden validation error.

Start in calendar or text-input mode

DatePicker normally starts as a calendar. If your audience frequently enters known dates, such as an invoice date, create its state with DisplayMode.Input.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InvoiceDatePicker() {
    val datePickerState = rememberDatePickerState(
        initialDisplayMode = DisplayMode.Input,
    )

    DatePicker(
        state = datePickerState,
        showModeToggle = true,
    )
}

The DatePicker API reference documents both initialDisplayMode and showModeToggle. Input mode is not automatically better: calendar mode helps people explore a month, while input mode helps people who already know an exact date. Provide a visible label and validation message around either choice; the picker alone does not explain a business rule such as “must be at least 18 years old.”

Use DateRangePicker for two connected dates

Two independent date dialogs make it easy to create an impossible range. DateRangePicker models the pair together and exposes start and end selections in DateRangePickerState.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StayDatesDialog(
    onConfirm: (startMillis: Long, endMillis: Long) -> Unit,
    onDismiss: () -> Unit,
) {
    val state = rememberDateRangePickerState()

    DatePickerDialog(
        onDismissRequest = onDismiss,
        confirmButton = {
            TextButton(
                enabled = state.selectedStartDateMillis != null &&
                    state.selectedEndDateMillis != null,
                onClick = {
                    val start = state.selectedStartDateMillis
                    val end = state.selectedEndDateMillis
                    if (start != null && end != null) onConfirm(start, end)
                },
            ) { Text("Apply") }
        },
        dismissButton = {
            TextButton(onClick = onDismiss) { Text("Cancel") }
        },
    ) {
        DateRangePicker(state = state)
    }
}

The range-picker documentation uses DateRangePickerState and a modal DatePickerDialog for this pattern. If one date is enough for the product, do not use a range picker merely to look more powerful; it makes the task slower.

Keep calendar dates separate from instants

selectedDateMillis is useful for the picker, but a Long does not by itself explain the domain meaning. The Material 3 reference calls the value passed to SelectableDates.isSelectableDate utcTimeMillis, and its example evaluates days in the UTC zone. Treat a picked birthday, due date, or hotel day as a calendar date in your domain—not automatically as the start time of an event in the device time zone.

Decide this boundary deliberately:

  • For date-only data, convert and store a date-only domain value (for example, LocalDate where your platform and min SDK support it).
  • For a real moment in time, combine the chosen date, selected time, and the business time zone in the ViewModel or domain layer.
  • For availability rules, apply the same zone on the server and client. Do not compare a server UTC instant to a user-facing calendar day without a defined conversion.

The API reference’s selectable-date example converts its utcTimeMillis through Instant and ZoneId.of("UTC"); that is a useful warning against relying on the device default zone accidentally.

Show a time picker with an explicit commit

TimePickerState carries an hour, minute, and 12/24-hour presentation choice. Use it for the temporary selection, then send the raw fields to the state holder after confirmation.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReminderTimeDialog(
    initialHour: Int,
    initialMinute: Int,
    onConfirm: (hour: Int, minute: Int) -> Unit,
    onDismiss: () -> Unit,
) {
    val timePickerState = rememberTimePickerState(
        initialHour = initialHour,
        initialMinute = initialMinute,
        is24Hour = true,
    )

    TimePickerDialog(
        onDismissRequest = onDismiss,
        confirmButton = {
            TextButton(
                onClick = {
                    onConfirm(timePickerState.hour, timePickerState.minute)
                },
            ) { Text("OK") }
        },
        dismissButton = {
            TextButton(onClick = onDismiss) { Text("Cancel") }
        },
    ) {
        TimePicker(state = timePickerState)
    }
}

The time-picker guide shows rememberTimePickerState with an initial hour, minute, and is24Hour flag. Use the device convention when it is appropriate for your product; use a fixed 24-hour mode only when the domain requires it, such as transport or operations scheduling.

TimeInput(state = timePickerState) can replace TimePicker inside the same dialog when typed entry is the better interaction. The two components share the same state concept, but test both with real users: a dial is fast for approximate selection, while input is clearer for exact times.

Restrict dates in the state configuration

Use SelectableDates and the year range to prevent choices that the business will certainly reject, such as past appointments or dates outside a reporting period. Still validate on submit: device clocks, deep links, stale screens, and server rules can change after the picker opens.

val appointmentPickerState = rememberDatePickerState(
    yearRange = 2026..2030,
    selectableDates = appointmentSelectableDates,
)

Keep appointmentSelectableDates as a policy supplied by the state/domain layer. Avoid calculating remote availability directly inside a composable. When availability is dynamic, show a loading or error state around the dialog and validate again when the user confirms.

Accessibility and testing checklist

  • Use a dialog title and the Material controls’ visible confirm and cancel actions.
  • Do not communicate an unavailable date through color alone; keep the reason near the field when it matters.
  • Test keyboard input mode, TalkBack traversal, large font sizes, narrow screens, and both 12- and 24-hour settings.
  • Test cancellation: it must leave the saved screen value unchanged.
  • Test date boundaries, leap days, range ordering, daylight-saving transitions, and your server’s time zone.

The picker components provide selection UI, but an app still needs a label, an error state, and an explicit submit rule. The form validation guide and IME-actions guide cover the surrounding form behavior.

Common mistakes

Persisting while the picker changes

The picker state changes as a person explores options. Persisting each intermediate selection can create surprising saves and unnecessary requests. Treat OK as the commit action unless the product clearly needs immediate updates.

Formatting a date with an accidental time zone

The same epoch milliseconds can display as a different civil day when formatted in another zone. Define whether the value is date-only or an instant before formatting or sending it to the backend.

Using a date picker for a short list of known choices

If the user chooses one of three delivery windows, a date picker adds work. Use the selection controls described in Checkbox, RadioButton, and Switch when the choices are fixed and named.

FAQ

Are Material 3 date and time pickers stable?

Android’s current Compose guides label these APIs experimental. Use @OptIn(ExperimentalMaterial3Api::class) where required and review the current AndroidX release notes before a dependency upgrade.

Should I use Long for every selected date?

It is the picker state shape, but not necessarily the best domain model. Use a date-only value for date-only business data, and combine date, time, and a named zone for a true instant.

When should I use TimeInput instead of TimePicker?

Use TimeInput when people need fast, exact keyboard entry. Use the dial when selecting a time visually is easier. Both use TimePickerState.

Summary

Material 3 pickers handle the calendar and clock UI; your state layer handles the meaning of the selection. Use a confirmation dialog for committed choices, use a range picker for connected start/end dates, choose input mode based on the task, and define date/time-zone rules before the value leaves the screen.