What You Will Build

A monthly calendar grid with day-of-week headers, selectable date cells arranged in a 7-column grid, and a detail card showing the selected date. The current day is pre-selected on load. Tapping a date highlights it with the primary color. This is the foundation for any custom date picker or scheduling UI.

Why This Pattern Matters

The built-in Material DatePicker is a dialog, which does not work when you need an inline calendar embedded in a screen. Building a calendar from LazyVerticalGrid gives you full layout and styling control for booking apps, habit trackers, and event planners.

The Custom Calendar

@Composable
fun CustomCalendarScreen() {
    var selectedDay by remember {
        mutableIntStateOf(
            java.util.Calendar.getInstance()
                .get(java.util.Calendar.DAY_OF_MONTH)
        )
    }
    val currentMonth = remember {
        java.text.SimpleDateFormat("MMMM yyyy", java.util.Locale.getDefault())
            .format(java.util.Date())
    }
    val daysInMonth = remember {
        java.util.Calendar.getInstance()
            .getActualMaximum(java.util.Calendar.DAY_OF_MONTH)
    }

    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        Text("Custom Calendar",
            style = MaterialTheme.typography.headlineSmall,
            fontWeight = FontWeight.Bold)
        Spacer(Modifier.height(16.dp))

        Text(currentMonth,
            style = MaterialTheme.typography.titleMedium,
            modifier = Modifier.padding(bottom = 12.dp))

        // Day-of-week headers
        Row(Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly) {
            listOf("S","M","T","W","T","F","S").forEach {
                Text(it, fontWeight = FontWeight.Bold,
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                    modifier = Modifier.width(40.dp),
                    textAlign = TextAlign.Center)
            }
        }

        Spacer(Modifier.height(8.dp))

        // Date grid
        LazyVerticalGrid(
            columns = GridCells.Fixed(7),
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            items(daysInMonth) { day ->
                val d = day + 1
                val isSelected = d == selectedDay
                Box(
                    modifier = Modifier
                        .aspectRatio(1f)
                        .clip(CircleShape)
                        .background(
                            if (isSelected)
                                MaterialTheme.colorScheme.primary
                            else Color.Transparent
                        )
                        .clickable { selectedDay = d },
                    contentAlignment = Alignment.Center
                ) {
                    Text("$d",
                        color = if (isSelected) Color.White
                            else MaterialTheme.colorScheme.onSurface,
                        fontWeight = if (isSelected) FontWeight.Bold
                            else FontWeight.Normal)
                }
            }
        }

        Spacer(Modifier.height(16.dp))

        // Detail card
        Card(Modifier.fillMaxWidth()) {
            Column(Modifier.padding(16.dp)) {
                Text("Selected: $currentMonth $selectedDay",
                    style = MaterialTheme.typography.bodyLarge)
                Text("No events for this day",
                    color = MaterialTheme.colorScheme.onSurfaceVariant)
            }
        }
    }
}

Tips and Pitfalls

  • GridCells.Fixed(7) ensures exactly 7 columns matching the days of the week.
  • aspectRatio(1f) makes each cell a perfect square, which looks best for calendar grids.
  • CircleShape clip + background creates the circular highlight on the selected date.
  • For production: Account for the first day-of-week offset (e.g., if the month starts on Wednesday, add 3 empty cells before day 1). Use java.time.YearMonth for proper calendar math.

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.