A universal calendar web component — works in React, Vue, Svelte, Angular, Solid.js, and plain HTML with no framework dependency.
<kal-calendar> custom element, no framework requirednpm install kalendly
<link
rel="stylesheet"
href="https://unpkg.com/kalendly/dist/styles/calendar.css"
/>
<script src="https://unpkg.com/kalendly/dist/index.umd.js"></script>
<kal-calendar
id="cal"
title="My Calendar"
initial-date="2025-01-15"
></kal-calendar>
<script>
const cal = document.getElementById('cal');
// Set events (JS property — not an attribute)
cal.events = [
{ id: 1, name: 'Team Meeting', date: new Date(2025, 0, 15) },
{ id: 2, name: 'Project Deadline', date: new Date(2025, 0, 20) },
];
// Listen to custom events
cal.addEventListener('cal-date-select', e => {
console.log('Selected:', e.detail.date, e.detail.events);
});
cal.addEventListener('cal-month-change', e => {
console.log('Month:', e.detail.year, e.detail.month);
});
</script>
import 'kalendly';
import 'kalendly/styles';
// <kal-calendar> is now registered and ready
React 19 has full custom element support — pass objects/arrays as props and listen to custom events directly.
import 'kalendly';
import 'kalendly/styles';
function App() {
return (
<kal-calendar
title="My Calendar"
events={events}
oncal-date-select={e => console.log(e.detail.date)}
oncal-month-change={e => console.log(e.detail.year, e.detail.month)}
/>
);
}
React 18 users: React 18 does not forward object/array props or custom events to custom elements. You need to wire these up via a
ref:import { useRef, useEffect } from 'react'; import 'kalendly'; function Calendar({ events, onDateSelect, onMonthChange, ...attrs }) { const ref = useRef(null); useEffect(() => { if (ref.current) ref.current.events = events; }, [events]); useEffect(() => { const el = ref.current; if (!el) return; const onSelect = e => onDateSelect?.(e.detail.date, e.detail.events); const onChange = e => onMonthChange?.(e.detail.year, e.detail.month); el.addEventListener('cal-date-select', onSelect); el.addEventListener('cal-month-change', onChange); return () => { el.removeEventListener('cal-date-select', onSelect); el.removeEventListener('cal-month-change', onChange); }; }, [onDateSelect, onMonthChange]); return <kal-calendar ref={ref} {...attrs} />; }
Vue 3 supports custom elements natively — bind props with : and listen to events with @:
<template>
<kal-calendar
title="My Calendar"
:events="events"
@cal-date-select="onDateSelect"
@cal-month-change="onMonthChange"
/>
</template>
<script setup>
import 'kalendly';
import 'kalendly/styles';
const events = [{ id: 1, name: 'Team Meeting', date: new Date(2025, 0, 15) }];
function onDateSelect(e) {
console.log('Selected:', e.detail.date);
}
function onMonthChange(e) {
console.log('Month:', e.detail.year, e.detail.month);
}
</script>
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
// app.component.ts
import 'kalendly';
import 'kalendly/styles';
<!-- app.component.html -->
<kal-calendar
title="My Calendar"
[events]="events"
(cal-date-select)="onDateSelect($event)"
(cal-month-change)="onMonthChange($event)"
></kal-calendar>
<script>
import 'kalendly';
import 'kalendly/styles';
let { events = [] } = $props();
</script>
<kal-calendar {events} oncal-date-select={e => console.log(e.detail.date)} />
<script>
import { onMount } from 'svelte';
import 'kalendly';
import 'kalendly/styles';
export let events = [];
let calEl;
onMount(() => { calEl.events = events; });
$: if (calEl) calEl.events = events;
</script>
<kal-calendar bind:this={calEl} on:cal-date-select on:cal-month-change />
import 'kalendly';
import 'kalendly/styles';
function App() {
return (
<kal-calendar
title="My Calendar"
prop:events={events}
on:cal-date-select={e => console.log(e.detail.date)}
/>
);
}
v0.2.0 replaces the four separate framework packages with a single web component.
| Before (v0.1.x) | After (v0.2.0+) |
|---|---|
import { Calendar } from 'kalendly/react' |
import 'kalendly' |
import { Calendar } from 'kalendly/vue' |
import 'kalendly' |
import { Calendar } from 'kalendly/react-native' |
Not supported |
import { createCalendar } from 'kalendly/vanilla' |
import 'kalendly' |
<Calendar events={events} /> |
<kal-calendar events={events} /> |
React Native is out of scope and not replaced.
// Bundler (Vite, webpack) — add once in your app entry (e.g. main.tsx)
import 'kalendly/styles';
// Plain HTML
// <link rel="stylesheet" href="/node_modules/kalendly/dist/styles/calendar.css">
// Angular — add to angular.json → projects → architect → build → styles
// "node_modules/kalendly/dist/styles/calendar.css"
kalendly uses Light DOM — all standard CSS techniques work:
/* 1. CSS custom properties (recommended) */
:root {
--calendar-primary-color: #6366f1;
--calendar-background: #1e1e2e;
--calendar-border-color: #334155;
}
/* 2. Direct class overrides */
.kalendly-calendar .calendar--card {
border-radius: 12px;
}
// 3. JS theme property
document.querySelector('kal-calendar').theme = {
primary: '#6366f1',
background: '#1e1e2e',
};
Primitives are set as HTML attributes:
| Attribute | Type | Default | Description |
|---|---|---|---|
title |
string |
— | Calendar title |
initial-date |
string |
today | ISO date string for initial view |
min-year |
string |
currentYear - 30 | Minimum year in picker |
max-year |
string |
currentYear + 10 | Maximum year in picker |
week-starts-on |
"0"|"1" |
"0" |
Week start: 0 = Sunday, 1 = Monday |
use-short-month-names |
string |
— | Present = use abbreviated month names |
Rich objects are set as JS properties (not attributes):
| Property | Type | Description |
|---|---|---|
events |
CalendarEvent[] |
Events to display |
theme |
CalendarTheme |
Custom theme colors |
categoryColors |
CategoryColorMap |
Per-category color overrides |
renderEvent |
(event: CalendarEvent) => string |
Custom event HTML renderer |
renderNoEvents |
() => string |
Custom empty-state HTML renderer |
| Event | detail shape |
Description |
|---|---|---|
cal-date-select |
{ date: Date, events: CalendarEvent[] } |
User clicked a date |
cal-month-change |
{ year: number, month: number } |
Month navigation occurred |
Both events bubble and are composed (cross Shadow DOM boundaries).
const cal = document.querySelector('kal-calendar');
cal.updateEvents(newEvents); // Re-render with new events
cal.updateTheme(newTheme); // Apply new theme
cal.goToDate(new Date(2025, 5, 1)); // Navigate to date
cal.getCurrentDate(); // Returns currently selected Date
cal.getEngine(); // Access CalendarEngine directly
interface CalendarEvent {
id: string | number;
name: string;
date: string | Date;
startTime?: string; // e.g. "09:00"
endTime?: string; // e.g. "10:00"
allDay?: boolean;
description?: string;
color?: string;
category?:
| 'work'
| 'personal'
| 'meeting'
| 'deadline'
| 'appointment'
| 'other';
location?: string;
url?: string;
status?: 'scheduled' | 'completed' | 'cancelled' | 'tentative';
priority?: 'low' | 'medium' | 'high';
attendees?: string[];
organizer?: string;
reminders?: number[]; // minutes before event
recurring?: {
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
interval?: number;
endDate?: string | Date;
daysOfWeek?: number[];
};
notes?: string;
tags?: string[];
[key: string]: unknown;
}
:root {
--calendar-primary-color: #fc8917;
--calendar-secondary-color: #fca045;
--calendar-tertiary-color: #fdb873;
--calendar-text-color: #2c3e50;
--calendar-text-light: #6b7280;
--calendar-border-color: #dee2e6;
--calendar-today-outline: #f7db04;
--calendar-event-indicator: #1890ff;
--calendar-background: #fff;
--calendar-cell-hover: #f3f4f6;
--calendar-selected-bg: #eff6ff;
}
cal.theme = {
primary: '#3b82f6',
secondary: '#60a5fa',
tertiary: '#93c5fd',
textColor: '#111827',
textLight: '#6b7280',
background: '#ffffff',
cellHover: '#f3f4f6',
borderColor: '#e5e7eb',
todayOutline: '#fbbf24',
selectedBg: '#eff6ff',
eventIndicator: '#10b981',
};
cal.theme = {
primary: '#6366f1',
secondary: '#818cf8',
textColor: '#f9fafb',
textLight: '#d1d5db',
background: '#1f2937',
cellHover: '#374151',
borderColor: '#4b5563',
todayOutline: '#fbbf24',
selectedBg: '#312e81',
eventIndicator: '#34d399',
};
import { CalendarEngine } from 'kalendly/core';
const engine = new CalendarEngine({ events, initialDate: new Date() });
const unsubscribe = engine.subscribe(() => {
const viewModel = engine.getViewModel();
// re-render
});
engine.getActions().next();
engine.getActions().previous();
engine.getActions().jump(2025, 5);
engine.getActions().goToToday();
unsubscribe();
engine.destroy();
Custom Elements v1 — Chrome 67+, Firefox 63+, Safari 12.1+, Edge 79+.
See CONTRIBUTING.md.
git clone https://github.com/callezenwaka/kalendly.git
cd kalendly
npm install
npm test
npm run dev:examples
MIT © Callis Ezenwaka
See CHANGELOG.md.
<kal-calendar> web component — works natively in React, Vue, Angular, and plain HTML with no framework dependencytitle prop