Skip to content

Event presentation and templates

Calendar guide

Keep scheduling events small and store product records by event ID. CalendarOptions<TMetadata> accepts eventPresentation, eventRenderer and eventTheme. All are optional; existing events continue to work.

Structured presentation

ts
import { createCalendar } from '@xsigns/chronosketch'
import type { CalendarOptions } from '@xsigns/chronosketch'

interface BookingMetadata { bookingId: string; customerId: string }

const options: CalendarOptions<BookingMetadata> = {
    date: '2026-09-06',
    view: 'week',
    events: [{
        id: 'booking-1', kind: 'timed', title: 'Consultation',
        start: '2026-09-06T14:00', end: '2026-09-06T15:30',
    }],
    eventTheme: 'system',
    eventPresentation: event => ({
        title: event.title,
        person: 'Ada Lovelace',
        resources: ['Room 1', 'Projector'],
        details: ['Bring the project brief'],
        icon: 'material:check',
        status: { label: 'Active', icon: 'material:check' },
        colors: { background: '#def4ec', text: '#154c3c', accent: '#24765e' },
        darkColors: { background: '#214c40', text: '#dbfff1', accent: '#7fdcb9' },
        metadata: { bookingId: event.id, customerId: 'customer-42' },
    }),
}
const calendar = createCalendar(options)
calendar.mount(document.querySelector<HTMLElement>('#calendar')!)

The resolver runs for each visible event segment. Keep it synchronous and pure. Read business data from your own ID-indexed map. No fields are injected into the scheduling event, and metadata is not serialized or copied by the store.

Presentation fieldType / meaning
titleOptional display title; defaults to event.title
personSeparate text line
resourcesReadonly array of resource labels
detailsReadonly array of supplementary text lines
iconCalendarIconName from the bundled catalog
statusRequired label, optional icon, opacity (clamped to 0–1), strikethrough
colorsIndependently optional background, text, accent CSS colors
darkColorsDark overrides; each missing value falls back to colors
metadataOptional TMetadata, available in custom templates

All default text is rendered as text, never HTML. Status is shown as a readable badge and included in the event's accessible name. For cancellation use, for example, { label: 'Cancelled', opacity: 0.6, strikethrough: true }. Do not communicate status only through a color; custom templates should retain a visible label or another understandable non-color cue.

Month and weekly all-day bars are compact single-line contents. Day/Week timed cards and Agenda use stacked fields. Space-limited cards can clip visible content; the accessible name includes the structured fields and status. Custom templates must fit the allocated event geometry and do not enlarge a timed interval or month lane.

eventTheme: 'light' | 'dark' | 'system' selects event palettes only. System (default) responds to the browser's color preference. Synchronize it with your application's theme switch; it does not restyle the entire calendar. Unspecified colors inherit the normal calendar theme. Icons use currentColor.

Vanilla renderer

ts
calendar.setEventRenderer((context, host) => {
    // context.presentation.metadata is BookingMetadata | undefined.
    const text = document.createElement('span')
    text.textContent = context.presentation.person + ' — ' + context.event.title
    host.append(text)
    // Optional: return () => disposeSubscriptions()
})

CalendarEventRenderContext<TMetadata> contains:

  • event: the full original CalendarEvent, not a clipped or display-only object.
  • view: month, week, day or agenda.
  • segment: visible start/end, exclusive end. Month/all-day/Agenda use civil dates; timed raster cards use clipped floating date-times.
  • presentation: resolved display fields and typed metadata.

The renderer is called after attachment with an empty, public content host. Use context.view to select a view-specific template. The same event can have multiple hosts when it spans days or month rows. Return optional cleanup for subscriptions or third-party widgets; it runs on replacement or destroy.

The host lives inside the library's event button. Use non-interactive content: no nested buttons, inputs, links or independently focusable controls. Keep product actions in the existing click/context callbacks. Templates own content; ChronoSketch owns focus, geometry, time labels, editing grips, click and secondary actions. Do not replace the host itself or inspect private DOM structures.

Vue slots

Use the default Calendar for unknown metadata, or create a typed component once at module scope:

vue
<script setup lang="ts">
import { createCalendarComponent } from '@xsigns/chronosketch/vue'
import type { CalendarOptions } from '@xsigns/chronosketch'
interface BookingMetadata { bookingId: string }
const Calendar = createCalendarComponent<BookingMetadata>()
const props = defineProps<{ options: CalendarOptions<BookingMetadata> }>()
</script>

<template>
  <Calendar :options="props.options">
    <template #event="{ event, view, presentation }">
      <span>{{ event.title }}</span>
      <span v-if="view !== 'month'">{{ presentation.person }}</span>
      <span>{{ presentation.status?.label }}</span>
      <span>{{ presentation.metadata?.bookingId }}</span>
    </template>
  </Calendar>
</template>

The event slot takes precedence over options.eventRenderer. Teleports preserve Vue context, provide/inject and reactive slot content. Replace eventPresentation when its external lookup data changes, or call the Vanilla setter when using a manually mounted instance. Updating the event array also resolves presentation.

React renderer

tsx
import { Calendar } from '@xsigns/chronosketch/react'
import type { CalendarOptions } from '@xsigns/chronosketch'
interface BookingMetadata { bookingId: string }

export function BookingCalendar({ options }: { options: CalendarOptions<BookingMetadata> }) {
    return <Calendar<BookingMetadata>
        {...options}
        renderEvent={({ event, view, presentation }) => (
            <span>
                {event.title} · {presentation.status?.label}
                {view !== 'month' && presentation.metadata?.bookingId}
            </span>
        )}
    />
}

React renderEvent takes precedence over eventRenderer. Portals preserve context and normal React component lifetimes. Updated props re-render template contents. Replace eventPresentation (for example with useCallback dependencies) when its external lookup data changes.

Reactive instance configuration

MethodBehavior
setEventPresentation(resolver?)Replace resolver and refresh visible content; omission restores defaults
setEventRenderer(renderer?)Replace template, clean up old instances; omission restores default content
setEventTheme(theme = 'system')Switch palettes without replacing event elements
setEvents(events)Existing event update; also refreshes presentation/templates

React props and Vue options synchronize these settings. Navigation and same-period scroll remain intact when presentation changes. Full subtree replacement is still the rendering strategy; custom component local state is recreated when the calendar subtree refreshes. Put persistent draft state in the product.

Click and context callbacks continue to receive CalendarEvent. Look up the product record by ID. Presentation status does not change permissions or disable editing; use the existing interaction options and action availability.

Overlapping timed events

Day and Week calculate deterministic side-by-side columns per visible day. Connected overlap groups share their maximum column count. Exactly touching exclusive ends do not overlap and may reuse a column; an isolated event uses the full width. Multi-day intervals are clipped before layout. No event is hidden behind a simultaneous event. Very dense groups may produce narrow cards; there is no minimum-width expansion or overflow menu yet.

Future zoned intervals

Current timed events remain floating local values. They cannot distinguish the two occurrences of the autumn hour. Zoned intervals are not implemented.

The planned domain extension needs unambiguous instants plus an IANA display zone, explicit ambiguity/gap resolution, and a variable-length time axis. Repeated hours must have distinct offset labels; moving/resizing must explicitly define elapsed-time versus wall-clock semantics. Existing civil and floating events must retain their meaning. This requires its own implementation and DST tests, rather than a locale/formatting option.

Avatars and logos

Return image: { src: '/guests/ada.jpg' } from eventPresentation for a round avatar, or set variant: 'logo' for a contained logo. EventImage and EventMedia are exported types. Images are decorative; keep the guest name in person or the event title. An optional icon is the fallback on image failure; without it the broken image is removed. Consumers own the URLs and image hosting.

ChronoSketch · Calendar and scheduling components