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.YearMonthfor proper calendar math.