kalendly
v0.2.0
Published
A universal calendar web component — works in React, Vue, Angular, svelte, and plain HTML with no framework dependency
Maintainers
Readme
kalendly
A universal calendar web component — works in React, Vue, Svelte, Angular, Solid.js, and plain HTML with no framework dependency.
Features
- Framework-agnostic: Single
<kal-calendar>custom element, no framework required - Responsive: Mobile-friendly, matches your existing UI
- Themeable: CSS variables + JS property API
- Type Safe: Full TypeScript support
- Event-rich: Categories, priorities, time ranges, attendees, and more
- Accessible: Built with accessibility in mind
- Tree-shakeable: Import only what you need
Live Demo
Installation
npm install kalendlyUsage
Vanilla HTML / CDN
<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>ES Modules
import 'kalendly';
import 'kalendly/styles';
// <kal-calendar> is now registered and readyReact 19
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
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>Angular
// 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>Svelte 5
<script>
import 'kalendly';
import 'kalendly/styles';
let { events = [] } = $props();
</script>
<kal-calendar {events} oncal-date-select={e => console.log(e.detail.date)} />Svelte 4
<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 />Solid.js
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)}
/>
);
}Migration from v0.1.x
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.
Styling
Loading styles
// 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"Overriding styles
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',
};Attributes
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 |
Properties
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 |
Custom Events
| 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).
JavaScript API
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 directlyCalendarEvent Interface
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;
}Theming
All CSS variables
: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;
}JS theme property (full reference)
cal.theme = {
primary: '#3b82f6',
secondary: '#60a5fa',
tertiary: '#93c5fd',
textColor: '#111827',
textLight: '#6b7280',
background: '#ffffff',
cellHover: '#f3f4f6',
borderColor: '#e5e7eb',
todayOutline: '#fbbf24',
selectedBg: '#eff6ff',
eventIndicator: '#10b981',
};Dark theme example
cal.theme = {
primary: '#6366f1',
secondary: '#818cf8',
textColor: '#f9fafb',
textLight: '#d1d5db',
background: '#1f2937',
cellHover: '#374151',
borderColor: '#4b5563',
todayOutline: '#fbbf24',
selectedBg: '#312e81',
eventIndicator: '#34d399',
};Core Engine (advanced)
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();Browser Support
Custom Elements v1 — Chrome 67+, Firefox 63+, Safari 12.1+, Edge 79+.
Contributing
See CONTRIBUTING.md.
git clone https://github.com/callezenwaka/kalendly.git
cd kalendly
npm install
npm test
npm run dev:examplesLicense
MIT © Callis Ezenwaka
Changelog
See CHANGELOG.md.
Recent Updates
- v0.2.0: Migrated to a single
<kal-calendar>web component — works natively in React, Vue, Angular, and plain HTML with no framework dependency - v0.1.7: Vanilla calendar performance optimization with event delegation
- v0.1.6: Navigation enhancements — Today button, month/year picker, optional
titleprop - v0.1.5: Universal theming system, TypeScript improvements
- v0.1.0: Initial release with React, Vue, React Native, and Vanilla JavaScript support

