Kalendar

KalendarMonth#

The full month grid — seven columns, one page per month. The header's arrow buttons and a horizontal swipe drive the same KalendarViewState, so they always stay in sync.

This is the most capable of the date-grid views: it is the only one with drag-to-reschedule.

The full month grid for March 2026. Thursday the 12th is selected and filled with a circle, the trailing days of February are dimmed in the first row, and event dots sit under the busy days — the 10th and the 13th each show three coloured dots followed by a "+1" badge for the events beyond the indicator cap.The full month grid for March 2026. Thursday the 12th is selected and filled with a circle, the trailing days of February are dimmed in the first row, and event dots sit under the busy days — the 10th and the 13th each show three coloured dots followed by a "+1" badge for the events beyond the indicator cap.

Minimal example#

kotlin
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 com.himanshoe.kalendar.KalendarMonth
import com.himanshoe.kalendar.state.rememberKalendarMonthState
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import kotlin.time.Clock

@Composable
fun MonthGrid() {
    val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) }
    val state = rememberKalendarMonthState(initialDate = today)
    var selected by remember { mutableStateOf(setOf(today)) }

    KalendarMonth(
        selectedDate = today,
        state = state,
        selectedDates = selected,
        onDateClick = { date, _ -> selected = setOf(date) },
    )
}

Parameters#

ParameterTypeDefaultDescription
selectedDateLocalDateDefault single-selection highlight, and the initially visible month when state is left at its default. Superseded by selectedDates when that is passed.
modifierModifierModifierApplied to the outermost container.
stateKalendarViewStaterememberKalendarMonthState(selectedDate)Navigation state. Hoist it to read visibleDate or navigate from your own UI.
eventsList<E>emptyList()Shown as indicator dots on day cells, and as span bars for multi-day events.
selectedDatesSet<LocalDate>setOf(selectedDate)The highlighted dates. Hoist your own set for multi-select or range.
onDateClick(date: LocalDate, events: List<E>) -> Unitno-opCalled when a non-disabled date is tapped, with that date's events.
onDayEventClick((event: E) -> Unit)?nullCalled when an individual event is tapped inside a day cell's +N overflow popover.
onDateRangeSelect((start: LocalDate, current: LocalDate) -> Unit)?nullWhen non-null, press-and-hold then drag selects a range live.
onDateRangeSelectEnd(() -> Unit)?nullCalled once when a drag started by onDateRangeSelect ends.
onEventDrop((events: List<E>, newDate: LocalDate) -> Unit)?nullWhen non-null, press-and-hold a date that has events and drag to another date to reschedule. See Drag to reschedule.
configKalendarViewConfigKalendarViewConfig()Shared visual and behavioural settings. See Configuration.
header@Composable (KalendarHeaderScope) -> UnitKalendarHeaderDefaults.HeaderReplaces the navigation header above the grid. See Customization.
dayOfWeekLabel@Composable (DayOfWeek) -> UnitKalendarHeaderDefaults.DayOfWeekLabelReplaces the content of each weekday column header.
dayContent(@Composable (scope: KalendarDayScope<E>) -> Unit)?nullReplaces the built-in day cell entirely. See Customization.

State factory#

rememberKalendarMonthState.

kotlin
val state = rememberKalendarMonthState(
    initialDate = today,
    startDayOfWeek = DayOfWeek.SUNDAY,
    minDate = LocalDate(2020, 1, 1),
    maxDate = LocalDate(2030, 12, 31),
)
ParameterTypeDefaultDescription
initialDateLocalDatetodayThe month initially visible.
startDayOfWeekDayOfWeekMONDAYThe grid's first column.
minDateLocalDate?nullTurns canScrollBackward false once the previous month would start before this date's month.
maxDateLocalDate?nullTurns canScrollForward false once the next month would start after this date's month.

Bounds are compared in page space, so a minDate that falls mid-month (say the 15th) still lets you reach the month that contains it.

Adjacent-month dates#

A month grid needs leading and trailing padding days to complete its first and last weeks. KalendarViewConfig.showAdjacentMonthDates decides what happens to them:

ValueResult
true (default)The neighbouring month's dates are drawn dimmed, at KalendarColors.disabledContentAlpha.
falseThose cells are blank placeholders that still hold the grid's alignment.

Either way, adjacent-month dates are not tappable — they count as disabled.

Selection#

selectedDates is a plain hoisted set, so a tap does whatever you say it does. For single, multiple, and range behaviour, rememberKalendarSelectionState implements the tap logic and survives configuration changes and process death:

kotlin
val selection = rememberKalendarSelectionState(mode = KalendarSelectionMode.Range)

KalendarMonth(
    selectedDate = today,
    selectedDates = selection.selectedDates,
    onDateClick = { date, _ -> selection.onDateClick(date) },
)

For a hand-rolled range, LocalDate.datesUntil(other) returns every date between the two, inclusive, in either order:

kotlin
import com.himanshoe.kalendar.foundation.datetime.datesUntil

var rangeStart by remember { mutableStateOf<LocalDate?>(null) }
var rangeEnd by remember { mutableStateOf<LocalDate?>(null) }
val start = rangeStart
val end = rangeEnd

KalendarMonth(
    selectedDate = today,
    selectedDates = if (start != null && end != null) start.datesUntil(end) else emptySet(),
    onDateClick = { date, _ ->
        if (rangeStart == null || rangeEnd != null) {
            rangeStart = date
            rangeEnd = null
        } else {
            rangeEnd = date
        }
    },
)

Sliding selection indicator#

When exactly one date is selected, a single shared indicator slides between cells rather than each cell fading independently. On by default; turn it off with KalendarViewConfig(showSelectionIndicator = false). It is drawn behind the cell, so a custom dayContent keeps the animation.

Drag to select a range#

kotlin
val selection = rememberKalendarSelectionState(mode = KalendarSelectionMode.Range)

KalendarMonth(
    selectedDate = today,
    selectedDates = selection.selectedDates,
    onDateClick = { date, _ -> selection.onDateClick(date) },
    onDateRangeSelect = selection::onRangeDrag,
    onDateRangeSelectEnd = selection::onRangeDragEnd,
)

A long press is required before the drag begins, so a quick tap still reaches onDateClick.

Drag to reschedule#

With onEventDrop set, press and hold a date that has events, then drag: a ghost highlight follows the cell under the pointer, and releasing calls the callback with the pressed date's events and the drop date.

The calendar never mutates your events — update your own source to complete the move:

kotlin
var events by remember { mutableStateOf(initialEvents) }

KalendarMonth(
    selectedDate = today,
    events = events,
    onEventDrop = { dropped, newDate ->
        events = events.map { event ->
            if (event in dropped && event is BasicKalendarEvent) {
                event.copy(date = newDate, endDate = null)
            } else {
                event
            }
        }
    },
)

Warning: KalendarEvent is an interface — it has no copy. Only the concrete implementation does. Smart-cast to BasicKalendarEvent (as above) or rebuild the event from your own domain model. And copying a multi-day event onto a new start date without also moving its endDate will silently change the event's length, so decide explicitly what should happen to the span.

Dates without events fall through to onDateRangeSelect's range behaviour, so drag-to-reschedule and drag-to-select can both be enabled at once.

Events on the grid#

Each day cell draws one dot per single-day event, coloured by KalendarEvent.eventColor and falling back to KalendarColors.eventIndicator. Past KalendarViewConfig.eventIndicatorCap (3 by default) the remainder collapses into a compact +N label.

A multi-day event (KalendarEvent.endDate) draws as a continuous bar across the days it covers, not as a dot on each of them. The bar runs edge to edge so adjacent cells join, and it breaks at the week boundary — a Thursday-to-Tuesday event is two bars, one per grid row. Its real start and end are inset and rounded; an end that is only the row running out stays square and flush, which is what distinguishes an event that finished on Saturday from one that continues into Sunday.

Overlapping spans pack into lanes, capped at KalendarDimensions.spanBarMaxLanes (2 by default). A span that does not fit the cap is not hidden — it falls back to the indicator dot and still reaches the +N overflow. Bar height, spacing, corner radius, end inset, and the lane cap are all dimension tokens; spanBarMaxLanes = 0 turns bars off entirely.

See Events for the full rules.