Kalendar

kalendar.component#

com.himanshoe.kalendar.component

The two default-rendering objects behind the date-grid views' day cell and header, and the modifier that gives a custom cell its accessibility and click handling back.

kotlin
import com.himanshoe.kalendar.component.KalendarDayCellDefaults

Everything else in this package — KalendarGrid and KalendarStaticGrid, KalendarBlockLayout, KalendarNavigationHeader, the hour-grid and lane-grid parts, the keyboard-navigation plumbing — is internal. It is where measurement, positioning, paging and gestures live, and it is deliberately not part of the compatible API: those are the parts a view keeps so a slot never has to reimplement them.

Modifier.kalendarDaySemantics#

kotlin
@Composable
public fun Modifier.kalendarDaySemantics(
    scope: KalendarDayScope<*>,
    onClick: () -> Unit,
    monthNameFormatter: (Month) -> String = KalendarFormatters.monthName,
    interactionSource: MutableInteractionSource? = null,
): Modifier

A custom dayContent replaces the built-in cell — including its accessibility and its click handling. This modifier gives both back.

ParameterTypeDefaultDescription
scopeKalendarDayScope<*>The cell's state, as handed to the slot. Star-projected: this modifier reads the interface only.
onClick() -> UnitCalled when the cell is tapped and the date is not disabled.
monthNameFormatter(Month) -> StringKalendarFormatters.monthNameFormats the month name inside the announcement. Pass config.monthNameFormatter to match a localized calendar.
interactionSourceMutableInteractionSource?nullSupply one to observe press, focus and hover state yourself — which is how you rebuild the focus ring and hover wash a custom cell also loses.

Applied to your cell's outermost modifier, it adds:

  • a screen-reader contentDescription built from the date, plus KalendarStrings.todaySuffix when the date is today and KalendarStrings.hasEventsSuffix when it has events — e.g. "August 12, 2026, today, has events";
  • the selected semantic state, from scope.isSelected;
  • Role.Button, so assistive technology announces the cell correctly;
  • a click that is ignored when scope.isDisabled is true.
kotlin
KalendarMonth(
    selectedDate = today,
    onDateClick = { date, _ -> select(date) },
    dayContent = { scope ->
        MyDayCell(
            day = scope.date.day,
            modifier = Modifier.kalendarDaySemantics(scope = scope, onClick = { select(scope.date) }),
        )
    },
)

Because the announcements come from KalendarStrings, a cell using this stays localizable through the same theme mechanism as the built-in one. A hand-rolled contentDescription does not.

Rebuilding the focus ring and hover wash:

kotlin
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
val isHovered by interactionSource.collectIsHoveredAsState()

MyDayCell(
    modifier = Modifier
        .background(if (isHovered) KalendarTheme.colors.hoverBackground else Color.Transparent)
        .border(
            width = 2.dp,
            color = if (isFocused) KalendarTheme.colors.focusIndicator else Color.Transparent,
            shape = KalendarTheme.shapes.dayCell,
        )
        .kalendarDaySemantics(
            scope = scope,
            onClick = { select(scope.date) },
            interactionSource = interactionSource,
        ),
)

KalendarDayCellDefaults#

kotlin
public object KalendarDayCellDefaults

The exact composables the date-grid views fall back to, so a custom slot can wrap the built-in cell rather than reimplement it.

Cell#

kotlin
@Composable
public fun <E : KalendarEvent> Cell(
    scope: KalendarDayScope<E>,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    showSelectionBackground: Boolean = true,
    monthNameFormatter: (Month) -> String = KalendarFormatters.monthName,
    maxEventIndicators: Int = 3,
    onEventClick: (E) -> Unit = {},
    showOverflowPopup: Boolean = true,
    overflowPopup: @Composable (KalendarDayOverflowScope<E>) -> Unit = { OverflowPopup(scope = it) },
)
ParameterTypeDefaultDescription
scopeKalendarDayScope<E>The cell's state.
onClick() -> UnitCalled when the cell is tapped and the date is not disabled.
modifierModifierModifierApplied to the cell.
showSelectionBackgroundBooleantrueWhether this cell draws its own selection fill. The views pass false when the sliding indicator is already drawing it.
monthNameFormatter(Month) -> StringEnglish month namesUsed for the accessibility description.
maxEventIndicatorsInt3Dots drawn before the remainder collapses into +N. Counts only the events drawn as dots — a multi-day event that got a span bar lane is not one of them. The views pass KalendarViewConfig.eventIndicatorCap.
onEventClick(E) -> Unit{}Called when an event is tapped inside the overflow popover — the calendar's own event type.
showOverflowPopupBooleantrueWhether the +N label opens a popover at all. false leaves it as a plain count.
overflowPopup@Composable (KalendarDayOverflowScope<E>) -> UnitOverflowPopupReplaces the popover. Last, so trailing-lambda syntax lands on it.
kotlin
KalendarMonth(
    selectedDate = today,
    dayContent = { scope ->
        Box {
            KalendarDayCellDefaults.Cell(scope = scope, onClick = { select(scope.date) })
            if (scope.date in holidays) HolidayBadge(modifier = Modifier.align(Alignment.TopEnd))
        }
    },
)

Note: span-bar lanes are assigned by the surrounding grid from the whole event list, because a lane cannot be decided from one day's events alone. A Cell composed outside any grid therefore has no lane assignment to read and draws its multi-day events as dots. Inside a dayContent slot — as above — it is still inside the grid, so the bars are drawn normally.

OverflowPopup#

kotlin
@Composable
public fun <E : KalendarEvent> OverflowPopup(
    scope: KalendarDayOverflowScope<E>,
    modifier: Modifier = Modifier,
)

The built-in +N popover: the date's whole event list, dismissed by tapping outside or pressing escape.

Generic in E rather than taking a star projection, because KalendarDayOverflowScope is invariant: a KalendarDayOverflowScope<*> can read events but cannot report a tap back through onEventClick. Write your own shared popover the same way — as a generic composable — and pass it as overflowPopup = { MyPopup(scope = it) }.

ParameterTypeDefaultDescription
E: KalendarEventinferred from scopeThe calendar's event type.
scopeKalendarDayOverflowScope<E>The date, its events, and the hidden count.
modifierModifierModifierApplied to the popover surface.
kotlin
overflowPopup = { scope -> KalendarDayCellDefaults.OverflowPopup(scope = scope) }

KalendarHeaderDefaults#

kotlin
public object KalendarHeaderDefaults

The chrome around a date grid.

kotlin
@Composable
public fun Header(
    scope: KalendarHeaderScope,
    modifier: Modifier = Modifier,
    showNavigationArrows: Boolean = true,
    showTodayButton: Boolean = true,
    showJumpPicker: Boolean = true,
    monthNameFormatter: (Month) -> String = KalendarFormatters.monthName,
    shortMonthNameFormatter: (Month) -> String = KalendarFormatters.shortMonthName,
    previousIcon: @Composable () -> Unit = { … },
    nextIcon: @Composable () -> Unit = { … },
    todayIcon: @Composable () -> Unit = { … },
    actions: @Composable RowScope.() -> Unit = {},
)

The default header on KalendarWeek, KalendarMonth and KalendarYear — and usable in KalendarTimeline's slot too, since every control drives the scope rather than any particular state type.

ParameterTypeDefaultDescription
scopeKalendarHeaderScopeInside a header slot, the value the slot was handed.
modifierModifierModifierApplied to the header's outermost container.
showNavigationArrowsBooleantrueWhether to draw the previous/next buttons. Each disables itself when the matching canScroll* is false.
showTodayButtonBooleantrueWhether to draw the today button.
showJumpPickerBooleantrueWhether tapping the title opens the month/year jump picker.
monthNameFormatter(Month) -> StringEnglish month namesThe title's month name.
shortMonthNameFormatter(Month) -> Stringfirst three lettersThe jump picker's fixed-width month buttons, and the week/day titles.
previousIcon@Composable () -> Unita Foundation-drawn chevronThe glyph only inside the previous-page button.
nextIcon@Composable () -> Unitthe same chevron, reversedThe glyph inside the next-page button.
todayIcon@Composable () -> Unita Foundation-drawn calendar pageThe glyph inside the today button.
actions@Composable RowScope.() -> Unit{}Extra controls appended to the header's trailing edge, inside its Row.
kotlin
// Keep the built-in header, but without the jump picker.
header = { scope -> KalendarHeaderDefaults.Header(scope = scope, showJumpPicker = false) }

Note: the icon slots draw the glyph only. The button around it — its touch target, its click handler, and its accessibility label from KalendarStrings — belongs to the header. Pass contentDescription = null on your icon, or a screen reader announces the control twice.

Two consequences: the default icons are private, so an icon slot is all-or-nothing per icon; and the default chevrons mirror under RTL automatically, whereas a replacement is your composable and mirroring it is yours.

TimelineHeader#

kotlin
@Composable
public fun TimelineHeader(
    scope: KalendarHeaderScope,
    modifier: Modifier = Modifier,
    showTodayButton: Boolean = true,
    background: Brush? = null,
)

KalendarTimeline's separate default: a leading-aligned title, no arrows, an optional today button, and a background brush for the opaque fill the overlay needs — without one, the months scroll visibly underneath it.

ParameterTypeDefaultDescription
scopeKalendarHeaderScopeThe value the slot was handed. On a timeline its page vocabulary is mapped onto months.
modifierModifierModifierApplied to the sticky container.
showTodayButtonBooleantrueWhether to draw the today button.
backgroundBrush?nullThe overlay's fill. null means KalendarColors.background.
kotlin
header = { scope ->
    KalendarHeaderDefaults.TimelineHeader(scope = scope, background = config.background)
}

DayOfWeekLabel#

kotlin
@Composable
public fun DayOfWeekLabel(
    dayOfWeek: DayOfWeek,
    modifier: Modifier = Modifier,
    formatter: (DayOfWeek) -> String = KalendarFormatters.dayOfWeekLabel,
)

One weekday column header's content, styled with KalendarTypography.dayOfWeekLabel and announced with KalendarStrings.dayOfWeekAccessibilityLabel. Shared by every view that has columns, including KalendarDatePicker — a column initial is a column initial.

ParameterTypeDefaultDescription
dayOfWeekDayOfWeekThe weekday this column is.
modifierModifierModifierApplied to the label. The grid still measures and places one per column.
formatter(DayOfWeek) -> Stringfirst letterThe text to draw.
kotlin
dayOfWeekLabel = { day ->
    KalendarHeaderDefaults.DayOfWeekLabel(dayOfWeek = day, formatter = { it.name.take(2) })
}

For a locale-aware label, set KalendarViewConfig.dayOfWeekLabelFormatter instead — no slot needed.