Kalendar

kalendar.theme#

com.himanshoe.kalendar.theme

Six token sets, the composable that provides them, the object that reads them, and the built-in values. Nothing a view draws is hard-coded, and nothing reads MaterialTheme at render time.

kotlin
import com.himanshoe.kalendar.theme.KalendarTheme

Theming is the narrative version of this page, with every default value and every palette. This page is the signature-level reference.

Kalendar reads Material only to derive default token values, inside KalendarThemeDefaults. No Material widget renders the calendar. Replace the defaults and Material is out of the picture.

KalendarTheme — the composable#

kotlin
@Composable
public fun KalendarTheme(
    colors: KalendarColors = KalendarTheme.colors,
    typography: KalendarTypography = KalendarTheme.typography,
    shapes: KalendarShapes = KalendarTheme.shapes,
    dimensions: KalendarDimensions = KalendarTheme.dimensions,
    animations: KalendarAnimations = KalendarTheme.animations,
    strings: KalendarStrings = KalendarTheme.strings,
    content: @Composable () -> Unit,
)

Provides tokens to everything inside. Each parameter defaults to the ambient value, so overriding one leaves the other five as they were — including inside a nested theme.

kotlin
KalendarTheme(colors = KalendarThemeDefaults.colors().copy(selectionBackground = BrandPurple)) {
    KalendarMonth(selectedDate = today)
}

Wrapping is optional: a view used outside any KalendarTheme falls back to KalendarThemeDefaults, so reading a token is always safe.

The tokens overload#

kotlin
@Composable
public fun KalendarTheme(
    tokens: KalendarThemeTokens?,
    content: @Composable () -> Unit,
)

Applies a prepared bundle. Use it to spread a look you already hold, or to override a single call site from the ambient bundle without constructing a whole theme:

kotlin
KalendarTheme(tokens = KalendarTheme.tokens.copy(dimensions = KalendarDimensions(hourHeight = 32.dp))) {
    KalendarSchedule(state = rememberKalendarScheduleState(initialDate = today))
}
ParameterTypeNotes
tokensKalendarThemeTokens?The bundle to apply. null inherits the surrounding theme untouched, passing the ambient bundle through by identity — which is what makes it a usable default for a theme parameter on your own composable.
content@Composable () -> UnitThe views these tokens apply to.

The tokens are provided unconditionally, even for null. Wrapping conditionally would give the content a different group shape in the two cases, so a caller toggling an override on and off would discard the calendar's remembered state with it.

See Overriding one calendar.

KalendarTheme — the object#

kotlin
public object KalendarTheme

The accessors that read the ambient tokens. All are @Composable getters.

AccessorType
KalendarTheme.colorsKalendarColors
KalendarTheme.typographyKalendarTypography
KalendarTheme.shapesKalendarShapes
KalendarTheme.dimensionsKalendarDimensions
KalendarTheme.animationsKalendarAnimations
KalendarTheme.stringsKalendarStrings
KalendarTheme.tokensKalendarThemeTokens — all six at once
kotlin
@Composable
fun MyDayCell(scope: KalendarDayScope<*>) {
    Text(
        text = scope.date.day.toString(),
        color = if (scope.isToday) KalendarTheme.colors.todayContent else KalendarTheme.colors.dayContent,
        style = KalendarTheme.typography.dayNumber,
    )
}

Warning: to layer on top of what is already ambient, copy the accessor, not the defaults: KalendarTheme(dimensions = KalendarTheme.dimensions.copy(hourHeight = 96.dp)). Constructing a token set directly resets every field you did not name back to the built-in default, which inside a nested theme silently discards the outer theme's values.

KalendarThemeDefaults#

kotlin
public object KalendarThemeDefaults

The built-in token values, and the three palettes.

FunctionComposableReturnsDescription
colors()yesKalendarColorsDerived from the ambient MaterialTheme.colorScheme. The default.
systemColors()yesKalendarColorsKalendar's own palette, following the system light/dark setting via isSystemInDarkTheme(). No Material dependency.
lightColors()noKalendarColorsThe light palette, chosen explicitly.
darkColors()noKalendarColorsThe dark palette, chosen explicitly.
typography()yesKalendarTypographyDerived from MaterialTheme.typography.
shapes()noKalendarShapesThe built-in shapes.
dimensions()noKalendarDimensionsThe built-in sizes.
animations()noKalendarAnimationsThe built-in motion specs.
strings()noKalendarStringsThe built-in English strings.
kotlin
// An app without Material 3: MaterialTheme.colorScheme falls back to the baseline *light* scheme,
// so a calendar would stay light in dark mode. Use Kalendar's own palette instead.
KalendarTheme(colors = KalendarThemeDefaults.systemColors()) {
    KalendarMonth(selectedDate = today)
}

KalendarThemeTokens#

kotlin
@Immutable
public class KalendarThemeTokens(
    public val colors: KalendarColors,
    public val typography: KalendarTypography,
    public val shapes: KalendarShapes,
    public val dimensions: KalendarDimensions,
    public val animations: KalendarAnimations,
    public val strings: KalendarStrings,
)

All six sets bundled into one value, so a whole look can be built once, passed around, and copied.

MemberReturnsDescription
colors, typography, shapes, dimensions, animations, stringsthe six token setsThe bundled values.
copy(…)KalendarThemeTokensA duplicate with only the sets passed here replaced.
kotlin
@Composable
fun rememberBrandTokens(): KalendarThemeTokens =
    KalendarTheme.tokens.copy(
        colors = KalendarThemeDefaults.systemColors().copy(selectionBackground = BrandPurple),
        shapes = KalendarTheme.shapes.copy(dayCell = RoundedCornerShape(8.dp)),
        animations = KalendarAnimations(enabled = false),
    )

KalendarColors#

kotlin
@Immutable
public class KalendarColors(
    public val background: Color,
    public val selectionBackground: Color,
    public val onSelectionBackground: Color,
    public val todayContent: Color,
    public val dayContent: Color,
    public val dayLabelContent: Color,
    public val headerContent: Color,
    public val eventIndicator: Color,
    public val nowIndicator: Color,
    public val gridLine: Color,
    public val dragGhostBackground: Color,
    public val popupBackground: Color,
    public val popupBorder: Color,
    public val agendaCard: Color,
    public val headerContentDisabled: Color = headerContent.copy(alpha = 0.38f),
    public val focusIndicator: Color = todayContent,
    public val hoverBackground: Color = dayContent.copy(alpha = 0.08f),
    public val disabledContentAlpha: Float = 0.38f,
    public val eventBlockAlpha: Float = 0.18f,
    public val eventBlockDraggingAlpha: Float = 0.45f,
    public val dragGhostAlpha: Float = 0.6f,
)

Fourteen required colours, three derived colours and four alphas.

PropertyTypeUsed for
backgroundColorFill behind the whole calendar.
selectionBackgroundColorThe selected day's fill and the sliding indicator.
onSelectionBackgroundColorContent on top of selectionBackground.
todayContentColorToday's day number when it is not selected.
dayContentColorOrdinary day numbers.
dayLabelContentColorDay-of-week column labels and hour-gutter labels.
headerContentColorThe navigation header's title and icons.
eventIndicatorColorEvent dots and blocks when KalendarEvent.eventColor is null.
nowIndicatorColorThe current-time line on the hour grids.
gridLineColorHour lines and separators.
dragGhostBackgroundColorThe drop-target ghost while dragging an event.
popupBackgroundColorFill of the month/year jump picker.
popupBorderColorOutline around the jump picker.
agendaCardColorFill of one KalendarAgenda event row.
headerContentDisabledColorA header arrow blocked by minDate/maxDate.
focusIndicatorColorThe ring around a keyboard-focused day cell.
hoverBackgroundColorThe wash over a day cell under the pointer.
disabledContentAlphaFloatDates rejected by disabledDates, and adjacent-month dates.
eventBlockAlphaFloatAn event block's fill over its accent colour. Stripe and text stay opaque.
eventBlockDraggingAlphaFloatAn event block's fill while being dragged.
dragGhostAlphaFloatApplied to dragGhostBackground.
copy(…)KalendarColorsA duplicate with only the values passed here replaced.

The fourteen colours at the top have no defaults, so the compiler tells you when a release adds one rather than letting a new surface silently fall back to something arbitrary.

kotlin
KalendarTheme(colors = KalendarTheme.colors.copy(nowIndicator = BrandRed)) {
    KalendarSchedule(events = events)
}

KalendarTypography#

kotlin
@Immutable
public class KalendarTypography(
    public val headerTitle: TextStyle,
    public val monthLabel: TextStyle,
    public val dayOfWeekLabel: TextStyle,
    public val dayNumber: TextStyle,
    public val dayNumberEmphasized: TextStyle,
    public val weekDayNumber: TextStyle,
    public val resourceTitle: TextStyle,
    public val eventLabel: TextStyle,
    public val eventOverflowLabel: TextStyle,
    public val hourLabel: TextStyle,
    public val agendaEventTitle: TextStyle,
    public val agendaEventSubtitle: TextStyle,
)
PropertyUsed for
headerTitleThe navigation header's title, the Timeline's month titles, the jump picker's year.
monthLabelMonth names inside KalendarYear's grid.
dayOfWeekLabelThe day-of-week column headers.
dayNumberThe day number in an ordinary cell.
dayNumberEmphasizedThe day number when the cell is selected or is today.
weekDayNumberThe date number in KalendarScheduleWeek's day-column headers.
resourceTitleA lane's name in KalendarResourceView's column headers.
eventLabelEvent block titles and all-day chip titles.
eventOverflowLabelThe compact +N label.
hourLabelThe hour-gutter labels.
agendaEventTitleAn event's name in KalendarAgenda's rows, and its empty-state message.
agendaEventSubtitleAn event's description and time range in KalendarAgenda's rows.
copy(…)A duplicate with only the styles passed here replaced.

Every field is required. To drop the selected/today emphasis entirely, set dayNumberEmphasized equal to dayNumber:

kotlin
val type = KalendarTheme.typography
KalendarTheme(typography = type.copy(dayNumberEmphasized = type.dayNumber)) {
    KalendarMonth(selectedDate = today)
}

KalendarShapes#

kotlin
@Immutable
public class KalendarShapes(
    public val dayCell: Shape,
    public val eventIndicator: Shape,
    public val eventBlock: Shape,
    public val popup: Shape,
    public val agendaCard: Shape,
)
PropertyUsed forDefault
dayCellThe day cell's selection fill, the sliding indicator, and the drag ghost.CircleShape
eventIndicatorThe per-event dots below a day number.CircleShape
eventBlockEvent blocks and all-day chips.RoundedCornerShape(4.dp)
popupThe month/year jump picker surface.RoundedCornerShape(12.dp)
agendaCardOne KalendarAgenda event row.RoundedCornerShape(12.dp)
copy(…)A duplicate with only the shapes passed here replaced.

dayCell is one token shared by three separate composables that have to agree visually — which is exactly why it lives on the theme rather than on each composable.

kotlin
KalendarTheme(shapes = KalendarTheme.shapes.copy(dayCell = RoundedCornerShape(8.dp))) {
    KalendarMonth(selectedDate = today)
}

KalendarDimensions#

kotlin
@Immutable
public class KalendarDimensions(…)

Every field has a default, so this is the one token set you can construct from scratch as a one-liner.

PropertyDefaultControls
dayCellPadding2.dpInset around each day cell, the selection indicator, and the drag ghost.
dayCellAspectRatio1fWidth-to-height ratio of a day cell. Above 1f gives short, wide cells.
eventIndicatorSize4.dpDiameter of one event dot.
eventIndicatorSpacing2.dpGap between adjacent event dots.
eventIndicatorBottomPadding2.dpGap below the dot row, between it and the cell's bottom edge.
eventIndicatorStripHeight14.dpHeight reserved for the dot/overflow strip, so a cell with events and one without agree on where the day number sits.
spanBarHeight4.dpThickness of one multi-day event's span bar — the continuous bar drawn across every day an event with an endDate occupies.
spanBarSpacing2.dpGap below each span bar, separating it from the next bar down and the lowest bar from the dot row. One lane is spanBarHeight + spanBarSpacing tall.
spanBarCornerRadius2.dpCorner radius applied to a bar's real ends only — the day the event starts and the day it ends. Where a bar is cut by the end of a week it stays square and runs flush to the cell's edge.
spanBarEndInset3.dpHow far a bar is pulled back from the cell's edge at a real end. The other half of the same distinction; 0.dp relies on the corner radius alone.
spanBarMaxLanes2How many span bars may be stacked in one day cell. A budget for vertical space, not a count of events. 0 disables span bars; the constructor requires >= 0.
headerPaddingPaddingValues(horizontal = 4.dp, vertical = 8.dp)Padding around the navigation header's contents.
headerIconSlotWidth48.dpWidth reserved on each side of the header title for icons.
headerContentSpacing8.dpGap between the header's title and the controls beside it.
hourHeight64.dpVertical space for one hour on the hour grids.
hourGutterWidth48.dpWidth of the hour-label gutter, and of the header spacer above it.
gridLineThickness1.dpThickness of hour lines and separators.
nowIndicatorThickness2.dpThickness of the current-time line.
eventBlockGap1.dpHorizontal gap between side-by-side event blocks.
eventBlockAccentWidth3.dpWidth of an event block's leading accent stripe. 0.dp removes it.
eventBlockPaddingPaddingValues(horizontal = 4.dp, vertical = 2.dp)Padding around an event block's label.
eventBlockResizeHandleHeight12.dpHeight of the grab strip at each edge of a block that starts a resize rather than a move.
allDayRowPaddingPaddingValues(horizontal = 16.dp, vertical = 4.dp)Padding around the all-day chip row.
allDayChipSpacing4.dpGap between all-day chips.
allDayChipPaddingPaddingValues(horizontal = 6.dp, vertical = 2.dp)Padding inside one all-day chip.
resourceColumnMinWidth96.dpNarrowest a KalendarResourceView lane may become before the lanes start scrolling horizontally.
resourceHeaderPadding4.dpPadding inside a resource column header.
resourceHeaderAccentHeight2.dpThickness of a lane header's accent underline.
monthSpacing16.dpVertical gap between month grids in KalendarYear.
sectionPaddingPaddingValues(horizontal = 16.dp, vertical = 8.dp)Padding around section titles.
agendaRowPaddingPaddingValues(horizontal = 12.dp, vertical = 10.dp)Padding inside one agenda row.
agendaRowSpacing4.dpVertical gap between agenda rows.
agendaRowGap12.dpGap between an agenda row's accent dot and its text.
agendaDotSize10.dpDiameter of an agenda row's accent dot.
copy(…)A duplicate with only the values passed here replaced.
kotlin
KalendarTheme(
    dimensions = KalendarTheme.dimensions.copy(hourHeight = 96.dp, eventBlockAccentWidth = 0.dp),
) {
    KalendarSchedule(state = rememberKalendarScheduleState(initialDate = today))
}

Note: KalendarSchedule, KalendarScheduleWeek and KalendarResourceView also take an hourHeight: Dp parameter, which defaults from this token and overrides it for that one calendar. Use the parameter for a single view, the token for every view.

KalendarAnimations#

kotlin
@Immutable
public class KalendarAnimations(
    public val enabled: Boolean = true,
    public val selectionColorSpec: AnimationSpec<Color> = spring(
        dampingRatio = Spring.DampingRatioNoBouncy,
        stiffness = Spring.StiffnessMediumLow,
    ),
    public val selectionIndicatorSpec: AnimationSpec<Offset> = spring(
        dampingRatio = Spring.DampingRatioNoBouncy,
        stiffness = Spring.StiffnessMedium,
    ),
    public val pressScale: Float = 0.92f,
    public val pressScaleSpec: AnimationSpec<Float> = spring(
        dampingRatio = Spring.DampingRatioLowBouncy,
        stiffness = Spring.StiffnessMedium,
    ),
    public val eventIndicatorFadeSpec: FiniteAnimationSpec<Float> = spring(
        dampingRatio = Spring.DampingRatioNoBouncy,
        stiffness = Spring.StiffnessMediumLow,
    ),
    public val titleTransitionMillis: Int = 200,
    public val titleFadeOutEasing: Easing = FastOutLinearInEasing,
    public val titleFadeInEasing: Easing = LinearOutSlowInEasing,
)

Every spec is stated in full rather than left to spring()'s defaults, so retuning one means changing a number here rather than first discovering what the default was.

PropertyDefaultControls
enabledtrueMaster switch. When false, every animation snaps to its target and pressScale is ignored.
selectionColorSpeca fully damped, medium-low-stiffness spring()The day cell's selection fill fading in and out.
selectionIndicatorSpeca fully damped, medium-stiffness spring()The indicator sliding between selected day cells.
pressScale0.92fThe scale a day cell shrinks to while pressed. 1f disables the effect.
pressScaleSpeca low-bounce, medium-stiffness spring()The pressScale animation — the one spec that overshoots.
eventIndicatorFadeSpeca fully damped, medium-low-stiffness spring()The row of event dots fading in and out as a day gains or loses events.
titleTransitionMillis200Total duration of the header and sticky-title fade, split evenly between out and in.
titleFadeOutEasingFastOutLinearInEasingThe half that takes the old title away: accelerating, so it leaves promptly.
titleFadeInEasingLinearOutSlowInEasingThe half that brings the new title back: decelerating, so it settles.
copy(…)A duplicate with only the values passed here replaced.

enabled = false is the correct response to a platform reduced-motion preference, and what screenshot tests want for determinism:

kotlin
KalendarTheme(animations = KalendarAnimations(enabled = !prefersReducedMotion)) {
    KalendarMonth(selectedDate = today)
}

KalendarStrings#

kotlin
@Immutable
public class KalendarStrings(…)

Every fixed word and accessibility announcement, as a token set — so translating the calendar is a theme concern like any other. Fully defaulted, so overriding one field is a one-liner.

PropertyTypeDefaultUsed for
previousPageString"Previous"Label for the header's previous-page arrow.
nextPageString"Next"Label for the next-page arrow.
todayString"Today"Label for the today button, and the date picker's Today action.
previousYearString"Previous year"Label for the jump picker's previous-year button.
nextYearString"Next year"Label for its next-year button.
openJumpPickerString"Choose month and year"Label for the header title when tapping it opens the jump picker.
todaySuffixString", today"Appended to a day cell's announcement when the date is today. Include the leading separator.
hasEventsSuffixString", has events"Appended when the date has at least one event. Include the leading separator.
agendaEmptyStateString"No events"Shown when KalendarAgenda is given no events.
resourceEmptyStateString"No resources"Shown when KalendarResourceView is given no lanes.
showAllDayEventsString"Show all events"Label for the control that expands a truncated all-day row.
eventOverflowLabel(hiddenEventCount: Int) -> String{ "+$it" }The compact label past eventIndicatorCap.
dayAccessibilityLabel(date: LocalDate, monthName: String) -> String"August 12, 2026"A day cell's base announcement. The two suffixes are appended to whatever this returns.
dayOfWeekAccessibilityLabel(DayOfWeek) -> String"Monday"The full day name announced for a column header, which only shows "M".
eventAccessibilityLabel(event, start: LocalDateTime, end: LocalDateTime) -> String"Design review, 09:30 to 11:00"A timed event block.
resourceEventAccessibilityLabel(event, start, end, resourceTitle: String) -> Stringthe above, plus the laneA block in KalendarResourceView.
allDayEventAccessibilityLabel(event) -> String"Release day, all day"An all-day chip.
copy(…)A duplicate with only the values passed here replaced.
kotlin
KalendarTheme(strings = KalendarTheme.strings.copy(today = stringResource(R.string.today))) {
    KalendarMonth(selectedDate = today)
}

The event labels are applied by KalendarScheduleDefaults.EventBlock and .AllDayChip — a replacement eventContent or allDayEventContent slot draws its own semantics, and should. See Localization.