Jetpack Compose DatePicker

Picking a date in an Android app used to mean writing dozens of lines of boilerplate. With Jetpack Compose DatePicker, that entire workflow becomes a few composable functions and a state object. Whether you are building a booking app, a task manager, or a calendar feature, the Material3 DatePicker component gives you a fully styled, accessible date picker out of the box. In this tutorial you will learn exactly how to set it up, manage its state, display it inside a dialog, format the result, and build a date range selector — all in Jetpack Compose.

Setting Up Material3 Dependencies

Before you can use the Jetpack Compose DatePicker, your project needs the Material3 dependency. Open your module-level build.gradle file and add the following inside the dependencies block:

dependencies {
    implementation("androidx.compose.material3:material3:1.3.0")
    implementation("androidx.compose.ui:ui:1.7.0")
    implementation("androidx.activity:activity-compose:1.9.0")
}

The Material3 library is where the DatePicker and all related components live. It follows the Material Design 3 specification, so the picker you build will automatically match Google's modern design guidelines and adapt to light and dark themes without any extra work on your end.

One important note before you write your first line: the DatePicker API in Material3 is still marked as experimental. That means you need to opt into it using the @OptIn(ExperimentalMaterial3Api::class) annotation on any composable that uses it. You will see this annotation throughout every example in this guide.

Understanding DatePickerState in Jetpack Compose

The heart of the Jetpack Compose DatePicker is the DatePickerState object. It is responsible for storing three key pieces of information: the currently selected date, the month that is visible on screen, and the input mode — whether the user is browsing the calendar grid or typing in a date manually.

You create this state using the rememberDatePickerState function, which keeps the state alive across recompositions and ties it to the composable's lifecycle automatically. Here is a minimal example showing how to initialize it and read the selected value:

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShowDatePickerState() {
    val datePickerState = rememberDatePickerState(
        initialSelectedDateMillis = System.currentTimeMillis()
    )
    val selected = datePickerState.selectedDateMillis
    Text(text = "Selected epoch millis: $selected")
}

The initialSelectedDateMillis parameter pre-selects a date when the picker first opens — here we pass the current timestamp so today is highlighted by default. The selected value lives in selectedDateMillis as a nullable Long. It is null when the user has not made a selection yet, and it becomes a non-null epoch millisecond value once they pick a day. You will always need to convert that number into a human-readable string before displaying it, which we will cover shortly.

Your First Jetpack Compose DatePicker

Now that the state makes sense, you are ready to render the actual picker. The DatePicker composable takes a state object and renders the complete calendar UI — the selected date header, month navigation arrows, and the day grid:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DatePicker
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InlineDatePickerScreen() {
    val datePickerState = rememberDatePickerState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        DatePicker(state = datePickerState)
    }
}

When this composable is placed on a screen, you get a full-height calendar embedded directly in the layout. The current month is shown by default. The user can tap the month-year header to jump into a year/month grid view, navigate between months using the arrows, and tap any day to select it. The chosen day gets a filled circle using your app's primary color taken automatically from your Material3 theme. This inline style works well when date selection is the primary action — like a booking screen or a date filter page where you want the calendar visible at all times.

Showing the Compose DatePicker Inside a Dialog

Most of the time you will not want a full-screen calendar sitting permanently in your layout. You want a picker that appears when the user taps a button and dismisses after they confirm. That is exactly the role of DatePickerDialog. It wraps the DatePicker in a Material3 bottom-sheet-style modal dialog with built-in confirm and dismiss button slots:

import androidx.compose.material3.Button
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DialogDatePickerScreen() {
    var showPicker by remember { mutableStateOf(false) }
    val datePickerState = rememberDatePickerState()

    Button(onClick = { showPicker = true }) {
        Text("Choose a Date")
    }

    if (showPicker) {
        DatePickerDialog(
            onDismissRequest = { showPicker = false },
            confirmButton = {
                TextButton(onClick = { showPicker = false }) {
                    Text("Confirm")
                }
            },
            dismissButton = {
                TextButton(onClick = { showPicker = false }) {
                    Text("Cancel")
                }
            }
        ) {
            DatePicker(state = datePickerState)
        }
    }
}

When the user taps "Choose a Date", the showPicker flag flips to true and the dialog appears. The onDismissRequest lambda fires when the user taps outside the dialog or presses the back button, so you set showPicker back to false there to close it. After they tap Confirm, datePickerState.selectedDateMillis holds the selected date as epoch milliseconds.

One design decision worth noting here: the dialog does not auto-close after a date is tapped. The user must explicitly press Confirm. This is intentional — it gives you the chance to validate the picked date before dismissal, which is useful in scenarios like preventing past date selection or enforcing a minimum booking window.

Formatting the Selected Date from the Android DatePicker

The Jetpack Compose DatePicker gives you the selected date as milliseconds since the Unix epoch. That raw number is not useful on its own, so you need to convert it into a readable string for display. The java.time API, available on API 26 and above, makes this clean and declarative:

import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

fun Long.toFormattedDate(pattern: String = "MMMM dd, yyyy"): String {
    val instant = Instant.ofEpochMilli(this)
    val localDate = instant.atZone(ZoneId.of("UTC")).toLocalDate()
    return localDate.format(DateTimeFormatter.ofPattern(pattern))
}

This extension function on Long takes any epoch millisecond value and returns a human-readable date string. You can plug in any pattern the DateTimeFormatter supports — "dd/MM/yyyy", "MMM d", "EEE, MMM d" — making it flexible for any display need. Here is how you wire it into a dialog-based screen so the button label updates after confirmation:

import androidx.compose.material3.Button
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

fun Long.toFormattedDate(pattern: String = "MMMM dd, yyyy"): String {
    val instant = Instant.ofEpochMilli(this)
    val localDate = instant.atZone(ZoneId.of("UTC")).toLocalDate()
    return localDate.format(DateTimeFormatter.ofPattern(pattern))
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DateDisplayScreen() {
    var showPicker by remember { mutableStateOf(false) }
    var selectedLabel by remember { mutableStateOf("No date selected") }
    val datePickerState = rememberDatePickerState()

    Button(onClick = { showPicker = true }) {
        Text(selectedLabel)
    }

    if (showPicker) {
        DatePickerDialog(
            onDismissRequest = { showPicker = false },
            confirmButton = {
                TextButton(onClick = {
                    datePickerState.selectedDateMillis?.let { millis ->
                        selectedLabel = millis.toFormattedDate()
                    }
                    showPicker = false
                }) { Text("OK") }
            },
            dismissButton = {
                TextButton(onClick = { showPicker = false }) { Text("Cancel") }
            }
        ) {
            DatePicker(state = datePickerState)
        }
    }
}

When the user picks June 15, 2025 and taps OK, the button label changes from "No date selected" to "June 15, 2025". The UTC timezone is used in the conversion because the DatePicker stores its values in UTC midnight epoch time, so matching that timezone avoids off-by-one-day errors that can appear when converting to local time.

Date Range Picker in Jetpack Compose

The Material3 date picker family also includes a range picker — perfect for hotel booking flows, vacation planners, leave request forms, or any feature where a user needs to select a start and end date. Material3 ships DateRangePicker and rememberDateRangePickerState for exactly this scenario:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DateRangePicker
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDateRangePickerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

fun Long.toShortDate(): String {
    val instant = Instant.ofEpochMilli(this)
    val localDate = instant.atZone(ZoneId.of("UTC")).toLocalDate()
    return localDate.format(DateTimeFormatter.ofPattern("MMM dd, yyyy"))
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DateRangePickerScreen() {
    val rangeState = rememberDateRangePickerState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        DateRangePicker(state = rangeState)

        Spacer(modifier = Modifier.height(16.dp))

        val start = rangeState.selectedStartDateMillis?.toShortDate() ?: "Not set"
        val end = rangeState.selectedEndDateMillis?.toShortDate() ?: "Not set"
        Text(text = "Check-in: $start")
        Text(text = "Check-out: $end")
    }
}

The DateRangePickerState exposes two properties: selectedStartDateMillis and selectedEndDateMillis. The user taps the first date to mark the start, then taps a second date to define the end. Material3 automatically highlights the entire range between those two dates with a tinted background, making the selection visually clear. Both values use the same epoch millisecond format, so the same formatting helper you already wrote works for both.

Restricting Selectable Dates in the Android Date Picker

Sometimes you need to block out certain dates — prevent selecting days in the past, limit choices to weekdays only, or constrain the range to a specific window. The DatePicker composable accepts a selectableDates parameter that lets you express these rules declaratively. Here is how to allow only today and future dates:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DatePicker
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SelectableDates
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FutureDatesOnlyPicker() {
    val todayStart = System.currentTimeMillis().let {
        it - (it % 86_400_000L)
    }

    val datePickerState = rememberDatePickerState(
        selectableDates = object : SelectableDates {
            override fun isSelectableDate(utcTimeMillis: Long): Boolean {
                return utcTimeMillis >= todayStart
            }

            override fun isSelectableYear(year: Int): Boolean {
                return year >= java.util.Calendar.getInstance()
                    .get(java.util.Calendar.YEAR)
            }
        }
    )

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        DatePicker(state = datePickerState)
        Text(
            text = datePickerState.selectedDateMillis
                ?.let { "Selected: $it" } ?: "No date chosen"
        )
    }
}

The SelectableDates interface gives you two hooks: isSelectableDate for individual days and isSelectableYear for disabling entire years in the year picker view. Dates that return false from isSelectableDate are rendered grayed out and are completely non-interactive — tapping them does nothing. This is a clean, declarative approach that keeps the validation logic separate from the rendering logic.

Complete Jetpack Compose DatePicker Working Example

The following is a complete, self-contained Android app that combines everything covered in this guide — a dialog-based Jetpack Compose DatePicker, date formatting, and a live display of the confirmed selection. You can drop this directly into an Android project that has Material3 configured:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

fun Long.toReadableDate(): String {
    val instant = Instant.ofEpochMilli(this)
    val date = instant.atZone(ZoneId.of("UTC")).toLocalDate()
    return date.format(DateTimeFormatter.ofPattern("EEEE, MMMM dd yyyy"))
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    AppointmentBookingScreen()
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppointmentBookingScreen() {
    var showDatePicker by remember { mutableStateOf(false) }
    var confirmedDate by remember { mutableStateOf<String?>(null) }
    val datePickerState = rememberDatePickerState(
        initialSelectedDateMillis = System.currentTimeMillis()
    )

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = "Book an Appointment", fontSize = 24.sp)

        Spacer(modifier = Modifier.height(24.dp))

        confirmedDate?.let { date ->
            Text(text = "Appointment confirmed for:", fontSize = 14.sp)
            Spacer(modifier = Modifier.height(8.dp))
            Text(text = date, fontSize = 18.sp)
            Spacer(modifier = Modifier.height(24.dp))
        }

        Button(onClick = { showDatePicker = true }) {
            Text(if (confirmedDate == null) "Select Date" else "Change Date")
        }
    }

    if (showDatePicker) {
        DatePickerDialog(
            onDismissRequest = { showDatePicker = false },
            confirmButton = {
                TextButton(
                    onClick = {
                        datePickerState.selectedDateMillis?.let { millis ->
                            confirmedDate = millis.toReadableDate()
                        }
                        showDatePicker = false
                    }
                ) {
                    Text("Confirm")
                }
            },
            dismissButton = {
                TextButton(onClick = { showDatePicker = false }) {
                    Text("Cancel")
                }
            }
        ) {
            DatePicker(state = datePickerState)
        }
    }
}