A lightweight, zero-dependency JavaScript calendar component
SimpleCalendarJs is a zero-dependency calendar component. Include the CSS and JS files in your project:
<link rel="stylesheet" href="simple-calendar-js.css" />
<script src="simple-calendar-js.js"></script>import SimpleCalendarJs from './simple-calendar-js.js';
import './simple-calendar-js.css';Note: Make sure to include the CSS file for proper styling.
Create a calendar instance by passing a container element and options:
// Create a container in your HTML
<div id="calendar"></div>
// Initialize the calendar
const calendar = new SimpleCalendarJs('#calendar', {
defaultView: 'month',
weekStartsOn: 1, // 0 = Sunday, 1 = Monday
locale: 'en-US',
use24Hour: false,
fetchEvents: async (start, end) => {
// Fetch events from your API
const response = await fetch(`/api/events?start=${start}&end=${end}`);
return response.json();
},
onEventClick: (event) => {
console.log('Event clicked:', event);
},
onSlotClick: (date) => {
console.log('Empty slot clicked:', date);
}
});All available options when creating a calendar instance:
| Option | Type | Default | Description |
|---|---|---|---|
mode | String | 'calendar' | Calendar mode: 'calendar' (standard calendar with events), 'date-picker' (single date selection), or 'range-picker' (date range selection) |
defaultView | String | 'month' | Initial view: 'month', 'week', or 'day' |
defaultDate | Date | null | Initial date to display (defaults to today if not provided) |
weekStartsOn | Number | 0 | First day of week: 0 (Sunday) or 1 (Monday) |
locale | String | 'default' | Locale code (e.g., 'en-US', 'fr-FR', 'pt-PT'). Supports 34+ locales. |
weekdayFormat | String | 'short' | Weekday name format: 'narrow' (1-2 letters), 'short' (abbreviated), or 'long' (full name) |
use24Hour | Boolean | false | Use 24-hour time format instead of 12-hour with AM/PM |
showTimeInItems | Boolean | true | Show time in event items across all views |
showGridLines | Boolean | true | Show/hide grid lines (borders between cells and time slots) |
showBorder | Boolean | true | Show/hide calendar outer border |
Control which toolbar elements are visible:
| Option | Type | Default | Description |
|---|---|---|---|
showToolbar | Boolean | true | Show/hide the entire toolbar |
showTodayButton | Boolean | true | Show/hide the "Today" button |
showNavigation | Boolean | true | Show/hide navigation arrows (← →) |
showTitle | Boolean | true | Show/hide the month/week/day title |
showYearPicker | Boolean | true | Enable/disable year selection (clickable year) |
showViewSwitcher | Boolean | true | Show/hide Month/Week/Day view switcher buttons |
enabledViews | Array | ['month', 'week', 'day'] | Which views are available to switch to |
listDaysForward | Number | 30 | Number of days forward to display in list view (only applies when list view is active) |
showTooltips | Boolean | true | Show/hide event tooltips on hover |
tooltipAllowHtml | Boolean | true | Allow HTML in tooltips (sanitized for security). Supports: <b>, <i>, <strong>, <em>, <span>, <br>, <a> with safe attributes and URL validation |
tooltipBgColor | String | null | null | Custom tooltip background color (hex). If null, uses CSS variable --cal-tooltip-bg |
tooltipTextColor | String | null | null | Custom tooltip text color (hex). If null, uses CSS variable --cal-tooltip-text |
tooltipMaxWidth | Number | 250 | Maximum tooltip width in pixels |
tooltipDelay | Number | 400 | Delay in milliseconds before tooltip appears on hover |
enableDragDrop | Boolean | false | Enable drag and drop to move events |
enableResize | Boolean | false | Enable resizing events to change duration |
autoContrastText | Boolean | false | Automatically calculate contrasting text color based on event background color for better readability |
contrastLevel | String | 'high' | Text contrast level when autoContrastText is enabled: 'high' (black/white), 'medium' (darker/lighter shade), 'low' (subtle variation) |
allowHtmlInEvents | Boolean | true | Allow basic HTML tags in event titles (sanitized for security). Supports: <b>, <i>, <strong>, <em>, <span>, <br> with class attribute for icons |
monthTimedEventStyle | String | 'list' | Display style for timed events in month view: 'list' (schedule format) or 'block' (traditional blocks) |
monthDayNumberAlign | String | 'left' | Horizontal alignment of day numbers in month view cells: 'left', 'center', or 'right' |
showEventBorder | Boolean | false | Display borders around events. Border color adaptively darkens from event background (light colors darken more for visibility), or use event borderColor property |
Example: Start calendar on a specific date:
new SimpleCalendarJs('#calendar', {
defaultView: 'month',
defaultDate: new Date(2024, 5, 15) // June 15, 2024
});Example: Hide grid lines for a cleaner look:
new SimpleCalendarJs('#calendar', {
showGridLines: false
});Example: Hide toolbar and only allow month view:
new SimpleCalendarJs('#calendar', {
showToolbar: false,
enabledViews: ['month']
});Example: Customize weekday name format:
// Portuguese with full day names
new SimpleCalendarJs('#calendar', {
locale: 'pt-PT',
weekdayFormat: 'long' // Domingo, Segunda-feira, Terça-feira...
});
// English with narrow format (single letters)
new SimpleCalendarJs('#calendar', {
locale: 'en-US',
weekdayFormat: 'narrow' // S, M, T, W, T, F, S
});Note: Different locales format the same option differently. Use weekdayFormat to control the style: 'narrow' (1-2 letters), 'short' (abbreviated), or 'long' (full name).
In addition to the standard calendar mode with events, SimpleCalendarJs provides two specialized picker modes for date selection: date-picker for selecting a single date, and range-picker for selecting a date range.
Use date-picker mode to allow users to select a single date. The selected date is highlighted with a distinct visual style.
const calendar = new SimpleCalendarJs('#calendar', {
mode: 'date-picker',
onDateSelect: (date) => {
console.log('Selected date:', date);
}
});
// Get the selected date
const selectedDate = calendar.getSelectedDate();
// Set a date programmatically
calendar.setSelectedDate(new Date(2024, 5, 15));
// Clear selection
calendar.clearSelection();Use range-picker mode to allow users to select a date range. Click once to set the start date, and click again to set the end date. The selected range is visually highlighted.
const calendar = new SimpleCalendarJs('#calendar', {
mode: 'range-picker',
onRangeSelect: (startDate, endDate) => {
console.log('Selected range:', startDate, 'to', endDate);
}
});
// Get the selected range
const range = calendar.getSelectedRange();
// Returns: { start: Date, end: Date } or null
// Set a range programmatically
calendar.setSelectedRange(
new Date(2024, 5, 10),
new Date(2024, 5, 20)
);
// Clear selection
calendar.clearSelection();fetchEventsonSlotClick callback is not triggered in picker modes| Method | Returns | Description |
|---|---|---|
getSelectedDate() | Date | null | Get the currently selected date in date-picker mode |
setSelectedDate(date) | void | Programmatically set the selected date in date-picker mode |
getSelectedRange() | Object | null | Get the selected date range in range-picker mode. Returns {start: Date, end: Date} or null |
setSelectedRange(startDate, endDate) | void | Programmatically set the selected date range in range-picker mode |
clearSelection() | void | Clear the current date or range selection in picker modes |
Register event handlers for user interactions:
| Callback | Parameters | Description |
|---|---|---|
fetchEvents | (start: Date, end: Date) | Async function to fetch events for the visible date range. Should return an array of event objects. |
onEventClick | (event: Object) | Called when an event is clicked |
onSlotClick | (date: Date) | Called when an empty time slot is clicked |
onDateSelect | (date: Date) | Called when a date is selected in date-picker mode |
onRangeSelect | (startDate: Date, endDate: Date) | Called when a date range is selected in range-picker mode |
onViewChange | (view: String) | Called when the view changes (month/week/day) |
onNavigate | (start: Date, end: Date) | Called when navigating to a different date range |
onEventDrop | (event: Object, oldStart: Date, oldEnd: Date, newStart: Date, newEnd: Date) | Called when an event is dropped after being dragged |
Events returned from fetchEvents should have the following structure:
{
id: 'unique-id', // Required: Unique identifier
title: 'Event Title', // Required: Event name (can contain HTML if allowHtmlInEvents is enabled)
start: Date, // Required: Start date/time
end: Date, // Required: End date/time
allDay: false, // Optional: All-day event flag
color: '#4f46e5', // Optional: Custom background color (hex)
textColor: '#ffffff', // Optional: Custom text color (hex) - overrides autoContrastText
borderColor: '#3730a3', // Optional: Custom border color (hex) - used when showEventBorder is true
allowHtml: true, // Optional: Override global allowHtmlInEvents setting for this event
description: 'Details', // Optional: Event description (also used for tooltip)
tooltip: 'Custom tooltip', // Optional: Custom tooltip text (overrides description)
tooltipAllowHtml: true // Optional: Override global tooltipAllowHtml setting for this event
}Minute Precision: The calendar supports minute-level precision for event times. Events are positioned exactly according to their start/end times in week and day views. For example, an event starting at 9:15 AM will be positioned 15 minutes (25%) below the 9:00 AM hour line.
const calendar = new SimpleCalendarJs('#calendar', {
fetchEvents: async (start, end) => {
return [
{
id: '1',
title: 'Team Meeting',
start: new Date(2024, 0, 15, 9, 15), // 9:15 AM
end: new Date(2024, 0, 15, 10, 45), // 10:45 AM
color: '#4f46e5'
},
{
id: '2',
title: 'Lunch Break',
start: new Date(2024, 0, 15, 12, 30), // 12:30 PM
end: new Date(2024, 0, 15, 13, 30), // 1:30 PM
allDay: false,
color: '#059669'
}
];
},
onEventClick: (event) => {
alert(`Clicked: ${event.title}`);
},
onSlotClick: (date) => {
console.log('Create event at:', date);
}
});SimpleCalendarJs relies on JavaScript's native Date object for timezone handling, which means events are automatically displayed in the user's local timezone.
fetchEvents: async (start, end) => {
const response = await fetch(`/api/events?start=${start}&end=${end}`);
const events = await response.json();
// Backend returns ISO 8601 strings with timezone info
// Example: "2024-03-15T10:00:00Z" (UTC)
// or: "2024-03-15T10:00:00-05:00" (EST)
return events.map(event => ({
...event,
start: new Date(event.start), // Automatically converts to local timezone
end: new Date(event.end)
}));
}// BAD: "2024-03-15T10:00:00" (no timezone)
// JavaScript interprets this as LOCAL time, not UTC
// This can cause issues for users in different timezonesScenario: Your server stores events in UTC, users are in different timezones
// Server returns (stored in UTC):
{
"title": "Team Meeting",
"start": "2024-03-15T14:00:00Z", // 2:00 PM UTC
"end": "2024-03-15T15:00:00Z" // 3:00 PM UTC
}
// User in New York (EST, UTC-5):
// Calendar displays: 9:00 AM - 10:00 AM
// User in London (GMT, UTC+0):
// Calendar displays: 2:00 PM - 3:00 PM
// User in Tokyo (JST, UTC+9):
// Calendar displays: 11:00 PM - 12:00 AMIntl.DateTimeFormat which respects the user's locale and timezone{
"events": [
{
"id": 1,
"title": "Global Team Standup",
"start": "2024-03-15T14:00:00Z",
"end": "2024-03-15T14:30:00Z",
"description": "Daily standup - all timezones welcome"
}
]
}Summary: As long as your backend sends properly formatted ISO 8601 date strings with timezone information (e.g., with 'Z' for UTC or timezone offset like '-05:00'), the calendar will automatically handle timezone conversion for each user's location.
SimpleCalendarJs includes a powerful tooltip system with HTML support, smart positioning, and extensive customization options.
Tooltips appear when you hover over an event for default 400ms delay. They display content based on the following priority:
const events = [
{
id: 1,
title: 'Team Meeting',
start: new Date(2024, 2, 15, 10, 0),
end: new Date(2024, 2, 15, 11, 0),
tooltip: 'Weekly team sync\nDiscuss Q1 roadmap' // Custom tooltip
},
{
id: 2,
title: 'Client Call',
start: new Date(2024, 2, 15, 14, 0),
end: new Date(2024, 2, 15, 15, 0),
description: 'Requirements gathering\nPrepare demo' // Used as tooltip
},
{
id: 3,
title: 'Code Review',
start: new Date(2024, 2, 15, 16, 0),
end: new Date(2024, 2, 15, 17, 0)
// No tooltip/description - will show title
}
];Use \n (newline character) in your tooltip or description text to create multiple lines:
{
id: 1,
title: 'Project Kickoff',
start: new Date(2024, 2, 20, 9, 0),
end: new Date(2024, 2, 20, 10, 30),
tooltip: 'Project Kickoff Meeting\n\nAgenda:\n- Introductions\n- Timeline review\n- Q&A session\n\nLocation: Conference Room A'
}By default, tooltips support HTML formatting with secure sanitization:
{
id: 1,
title: 'Design Review',
start: new Date(2024, 2, 20, 14, 0),
end: new Date(2024, 2, 20, 15, 30),
tooltip: '<b>Product Design Review</b><br><br><i>New feature mockups:</i><br>• Dashboard v2.0<br>• Mobile redesign<br><br><a href="https://figma.com/example">View in Figma</a>'
}<b>, <strong> - Bold text<i>, <em> - Italic text<br> - Line breaks<span style="color: #xxx;"> - Colored text<a href="..."> - Links (auto open in new tab)javascript:, data:)onclick, etc.)Control tooltip behavior globally:
const calendar = new SimpleCalendarJs('#calendar', {
showTooltips: true, // Enable/disable tooltips (default: true)
tooltipAllowHtml: true, // Allow HTML in tooltips (default: true)
tooltipBgColor: '#1f2937', // Custom background color (default: null)
tooltipTextColor: '#f9fafb', // Custom text color (default: null)
tooltipMaxWidth: 250, // Maximum width in pixels (default: 250)
tooltipDelay: 400, // Hover delay in milliseconds (default: 400)
fetchEvents: async (start, end) => { /* ... */ }
});Override the global tooltipAllowHtml setting for specific events:
{
id: 1,
title: 'Secure Event',
tooltip: '<script>alert("xss")</script>', // This will be sanitized/removed
tooltipAllowHtml: false // Force plain text for this event only
}Tooltips use fixed positioning and automatically adjust to stay visible:
{
id: 1,
title: 'Client Demo',
start: new Date(2024, 2, 25, 10, 0),
end: new Date(2024, 2, 25, 11, 0),
tooltip: `
<span style="color: #ef4444;"><b>⚡ HIGH PRIORITY</b></span><br><br>
<b>Q4 Business Review</b><br>
<i>Executive presentation</i><br><br>
<b>Must prepare:</b><br>
• Financial projections<br>
• ROI analysis<br>
• Growth charts<br><br>
<a href="https://example.com/deck">View presentation deck</a>
`,
tooltipAllowHtml: true
}You can disable tooltips globally using the showTooltips option:
const calendar = new SimpleCalendarJs('#calendar', {
showTooltips: false, // Disable all tooltips
fetchEvents: async (start, end) => { /* ... */ }
});Tooltips use CSS custom properties and can be customized:
:root {
--cal-tooltip-bg: #1f2937; /* Background color */
--cal-tooltip-text: #f9fafb; /* Text color */
--cal-tooltip-border: #374151; /* Border color */
--cal-tooltip-max-width: 250px; /* Maximum width */
--cal-tooltip-padding: 8px 12px; /* Inner padding */
--cal-tooltip-radius: 6px; /* Border radius */
--cal-tooltip-font-size: 12px; /* Font size */
--cal-tooltip-offset: 8px; /* Distance from event */
--cal-tooltip-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* Shadow */
}Or use configuration options for colors:
const calendar = new SimpleCalendarJs('#calendar', {
tooltipBgColor: '#dc2626', // Red background
tooltipTextColor: '#ffffff', // White text
});Note: HTML is enabled by default for tooltips. To disable, set tooltipAllowHtml: false globally or per-event. All HTML is sanitized for security with comprehensive XSS protection.
SimpleCalendarJs supports drag and drop for moving events and resizing them to change their duration.
const calendar = new SimpleCalendarJs('#calendar', {
enableDragDrop: true, // Enable moving events
enableResize: true, // Enable resizing events
onEventDrop: (event, oldStart, oldEnd, newStart, newEnd) => {
// Detect if this is a move or resize
const isMoved = oldStart.getTime() !== newStart.getTime();
const isResized = oldEnd.getTime() !== newEnd.getTime() && !isMoved;
if (isMoved) {
console.log(`Event "${event.title}" moved to ${newStart}`);
} else if (isResized) {
console.log(`Event "${event.title}" resized to end at ${newEnd}`);
}
// Update your backend
await fetch(`/api/events/${event.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
start: newStart.toISOString(),
end: newEnd.toISOString(),
allDay: event.allDay
})
});
}
});enableDragDrop)How It Works:
Cross-Boundary Conversion:
When dragging events between different sections:
Timed Event → All-Day Section (Week/Day Views):
All-Day Event → Timed Section (Week/Day Views):
Month View:
enableResize)Horizontal Resize (All-Day Events):
Vertical Resize (Timed Events):
The onEventDrop callback receives the same parameters for both move and resize operations:
| Parameter | Type | Description |
|---|---|---|
event | Object | The updated event object (with new start/end) |
oldStart | Date | Original start date/time |
oldEnd | Date | Original end date/time |
newStart | Date | New start date/time |
newEnd | Date | New end date/time |
Detecting Operation Type:
oldStart !== newStartoldStart === newStart and oldEnd !== newEndImportant Notes:
onEventDrop callbackcalendar.refresh() to revert to the previous stateThe monthTimedEventStyle option controls how timed events are displayed in month view:
'list')Schedule-style display with horizontal layout:
showTimeInItems is enabled)const calendar = new SimpleCalendarJs('#calendar', {
monthTimedEventStyle: 'list', // Default
showTimeInItems: true // Shows time next to dot
});'block')Traditional calendar block display:
const calendar = new SimpleCalendarJs('#calendar', {
monthTimedEventStyle: 'block'
});Note: This option only affects timed events in month view. All-day events always display as blocks, and week/day views always use block style with duration-based heights.
Month view automatically adapts row heights based on your container's height and event content:
When you set an explicit height on the calendar container (e.g., height: 600px), the calendar calculates the maximum number of events that can fit in each cell and displays a "+N more" indicator for overflow events.
// HTML with fixed height
<div id="calendar" style="height: 600px;"></div>
// Calendar automatically limits events to fit the available space
const calendar = new SimpleCalendarJs('#calendar', {
defaultView: 'month'
});When the container has no explicit height (or height: auto), the calendar displays all events with variable row heights:
// HTML with natural height
<div id="calendar" style="height: auto;"></div>
// or
<div id="calendar"></div>
// All events displayed, rows adapt to content
const calendar = new SimpleCalendarJs('#calendar', {
defaultView: 'month'
});Recommendation: Use fixed height containers when you need predictable layouts, and natural height when you want to show all events with minimal scrolling.
SimpleCalendarJs can automatically calculate the best text color for event text based on the background color, ensuring optimal readability with configurable contrast levels.
Choose from three contrast levels to match your design preferences:
'high')Pure black or white text for maximum readability:
'medium')Darker or lighter shade of the background color for a more integrated look:
'low')Subtle variation that maintains the color family:
const calendar = new SimpleCalendarJs('#calendar', {
autoContrastText: true, // Enable automatic text color calculation
contrastLevel: 'medium', // Choose: 'high', 'medium', or 'low'
fetchEvents: async (start, end) => {
return [
{
id: 1,
title: 'Team Meeting',
start: new Date('2024-03-15T10:00:00'),
color: '#ffeb3b' // Yellow background → auto-calculated dark yellow text
},
{
id: 2,
title: 'Project Review',
start: new Date('2024-03-16T14:00:00'),
color: '#1a237e' // Dark blue background → auto-calculated light blue text
}
];
}
});Individual events can override the auto-calculated color with a custom textColor:
{
id: 1,
title: 'Special Event',
start: new Date('2024-03-15T10:00:00'),
color: '#4caf50', // Green background
textColor: '#ffeb3b' // Force yellow text (overrides auto-contrast)
}Text color is determined in this order:
event.textColor (highest priority) - Manual overrideautoContrastText: true - Automatic calculation based on background color--cal-event-text (default) - Fallback to theme defaultWhen to use: Enable autoContrastText when events have diverse background colors from user preferences or categories. Disable it (default) when all events use similar colors with known good contrast.
SimpleCalendarJs allows basic HTML formatting in event titles for icons, bold/italic text, and simple styling while maintaining security through automatic sanitization.
All HTML is automatically sanitized to prevent XSS attacks:
<b>, <i>, <strong>, <em>, <span>, <br>class (for icon libraries like Font Awesome)<script>, event handlers (onclick, onerror), dangerous attributesconst calendar = new SimpleCalendarJs('#calendar', {
allowHtmlInEvents: true, // Enable HTML rendering
fetchEvents: async (start, end) => {
return [
{
id: 1,
title: '<b>Important</b> Meeting',
start: new Date('2024-03-15T10:00:00')
},
{
id: 2,
title: 'Design <i>Review</i>',
start: new Date('2024-03-16T14:00:00')
}
];
}
});Perfect for adding visual indicators:
{
id: 1,
title: '<i class="fas fa-video"></i> Video Call',
start: new Date('2024-03-15T10:00:00')
}
{
id: 2,
title: '<i class="fas fa-plane"></i> Flight to NYC',
start: new Date('2024-03-16T14:00:00'),
allDay: true
}
{
id: 3,
title: '<i class="fas fa-birthday-cake"></i> <b>Sarah</b> Birthday',
start: new Date('2024-03-20T00:00:00'),
allDay: true
}Override the global setting for specific events:
const calendar = new SimpleCalendarJs('#calendar', {
allowHtmlInEvents: false, // Disabled globally
fetchEvents: async (start, end) => {
return [
{
id: 1,
title: 'Regular Meeting', // Plain text
start: new Date('2024-03-15T10:00:00')
},
{
id: 2,
title: '<i class="fas fa-star"></i> <b>VIP</b> Client Call',
start: new Date('2024-03-16T14:00:00'),
allowHtml: true // Enable HTML only for this event
}
];
}
});Default behavior: HTML in event titles is disabled by default (allowHtmlInEvents: false). However, tooltips support HTML by default (tooltipAllowHtml: true). All HTML is sanitized - dangerous tags and attributes are stripped for security.
Display upcoming events in a chronological list format. Perfect for mobile devices and quick overview of upcoming events.
const calendar = new SimpleCalendarJs('#calendar', {
defaultView: 'list', // Start in list view
enabledViews: ['month', 'week', 'day', 'list'], // Include list in views
listDaysForward: 30 // Show next 30 days (default)
});listDaysForward optionList view always displays events from the current date/time forward, unlike other views which are navigable:
// Show next 90 days in list view
new SimpleCalendarJs('#calendar', {
defaultView: 'list',
enabledViews: ['month', 'list'], // Only month and list views
listDaysForward: 90,
fetchEvents: async (start, end) => {
// Your event fetching logic
}
});Note: List view is opt-in. Add 'list' to the enabledViews array to enable it.
Programmatically control the calendar:
| Method | Parameters | Description |
|---|---|---|
setView(view) | 'month' | 'week' | 'day' | 'list' | Switch to a different view |
navigate(direction) | number | Navigate by offset: 1 = next, -1 = previous |
goToDate(date) | Date | Jump to a specific date |
goToToday() | - | Jump to today's date |
refresh() | - | Clear event cache and re-fetch events |
destroy() | - | Remove calendar and cleanup event listeners |
const calendar = new SimpleCalendarJs('#calendar', { /* options */ });
// Switch to week view
calendar.setView('week');
// Navigate to next week
calendar.navigate(1);
// Jump to a specific date
calendar.goToDate(new Date(2024, 5, 15));
// Go to today
calendar.goToToday();
// Refresh calendar (clear cache and re-fetch)
calendar.refresh();
// Cleanup when done
calendar.destroy();SimpleCalendarJs intelligently caches fetched events to minimize unnecessary network requests:
calendar.refresh() when events change externally (added, updated, or deleted)How it works:
listDaysForward)// Month view Feb 2026: Fetches Jan 28 - Mar 1 (full month grid)
calendar.setView('month'); // ✓ Fetch: Jan 28 - Mar 1
// Switch to week view (Feb 20-26): Already in cache, no fetch
calendar.setView('week'); // ✗ No fetch (instant)
// Switch to day view (Feb 22): Already in cache, no fetch
calendar.setView('day'); // ✗ No fetch (instant)
// Navigate next week (Feb 27 - Mar 5): Extends beyond cache
// Fetches March's full month grid (Mar 1 - Apr 5)
calendar.navigate(1); // ✓ Fetch: Mar 1 - Apr 5
// Navigate next week (Mar 6-12): Already in March's cache
calendar.navigate(1); // ✗ No fetch (instant)
// Navigate previous week (Feb 27 - Mar 5): Partially outside cache
// Fetches February's full month grid (Jan 28 - Mar 1)
calendar.navigate(-1); // ✓ Fetch: Jan 28 - Mar 1
// After external event changes, refresh to clear cache
addEventToDatabase(newEvent);
calendar.refresh(); // ✓ Fetch (clears cache)The calendar doesn't include built-in persistence - this is intentionally left to your application so you have full control over how and where preferences are stored.
// Restore saved preferences or use defaults
const savedView = localStorage.getItem('calendarView') || 'month';
const savedDate = localStorage.getItem('calendarDate');
const calendar = new SimpleCalendarJs('#calendar', {
defaultView: savedView,
defaultDate: savedDate ? new Date(savedDate) : null,
// Save view changes
onViewChange: (view) => {
localStorage.setItem('calendarView', view);
},
// Save navigation (current date)
onNavigate: (startDate) => {
localStorage.setItem('calendarDate', startDate.toISOString());
}
});// Same as localStorage but survives only for the current tab/session
const calendar = new SimpleCalendarJs('#calendar', {
defaultView: sessionStorage.getItem('calendarView') || 'month',
onViewChange: (view) => sessionStorage.setItem('calendarView', view),
onNavigate: (start) => sessionStorage.setItem('calendarDate', start.toISOString())
});// Read from URL
const params = new URLSearchParams(window.location.search);
const view = params.get('view') || 'month';
const date = params.get('date') ? new Date(params.get('date')) : null;
const calendar = new SimpleCalendarJs('#calendar', {
defaultView: view,
defaultDate: date,
onViewChange: (newView) => {
const url = new URL(window.location);
url.searchParams.set('view', newView);
window.history.pushState({}, '', url);
},
onNavigate: (startDate) => {
const url = new URL(window.location);
url.searchParams.set('date', startDate.toISOString().split('T')[0]);
window.history.pushState({}, '', url);
}
});function MyCalendar() {
const [view, setView] = useState(
() => localStorage.getItem('calendarView') || 'month'
);
const [currentDate, setCurrentDate] = useState(
() => {
const saved = localStorage.getItem('calendarDate');
return saved ? new Date(saved) : new Date();
}
);
const handleViewChange = (newView) => {
setView(newView);
localStorage.setItem('calendarView', newView);
};
const handleNavigate = (startDate) => {
setCurrentDate(startDate);
localStorage.setItem('calendarDate', startDate.toISOString());
};
return (
<SimpleCalendarJsReact
defaultView={view}
defaultDate={currentDate}
onViewChange={handleViewChange}
onNavigate={handleNavigate}
fetchEvents={fetchEvents}
/>
);
}// composables/useCalendarPreferences.js
import { ref, watch } from 'vue';
export function useCalendarPreferences() {
const view = ref(localStorage.getItem('calendarView') || 'month');
const currentDate = ref(
localStorage.getItem('calendarDate')
? new Date(localStorage.getItem('calendarDate'))
: new Date()
);
watch(view, (newView) => {
localStorage.setItem('calendarView', newView);
});
watch(currentDate, (newDate) => {
localStorage.setItem('calendarDate', newDate.toISOString());
});
return { view, currentDate };
}
// In component
<script setup>
import { useCalendarPreferences } from './composables/useCalendarPreferences';
const { view, currentDate } = useCalendarPreferences();
const handleViewChange = (newView) => {
view.value = newView;
};
const handleNavigate = (startDate) => {
currentDate.value = startDate;
};
</script>
<template>
<SimpleCalendarJsVue
:defaultView="view"
:defaultDate="currentDate"
@viewChange="handleViewChange"
@navigate="handleNavigate"
/>
</template>// Load preferences from your API
const preferences = await fetch('/api/user/calendar-preferences').then(r => r.json());
const calendar = new SimpleCalendarJs('#calendar', {
defaultView: preferences.view || 'month',
defaultDate: preferences.lastViewedDate ? new Date(preferences.lastViewedDate) : null,
onViewChange: async (view) => {
// Debounce to avoid too many API calls
clearTimeout(window.savePreferencesTimeout);
window.savePreferencesTimeout = setTimeout(() => {
fetch('/api/user/calendar-preferences', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ view })
});
}, 1000);
}
});Common preferences to save:
'month', 'week', 'day', or 'list'Note: The calendar is designed to be stateless - all state management is your responsibility, giving you full flexibility over storage method, persistence duration, and privacy controls.
SimpleCalendarJs supports dark mode and color customization via CSS custom properties.
Toggle dark mode by adding the uc-dark class to the calendar container:
// Enable dark mode
calendar._root.classList.add('uc-dark');
// Disable dark mode
calendar._root.classList.remove('uc-dark');
// Toggle dark mode
calendar._root.classList.toggle('uc-dark', isDark);Target the .uc-calendar class to customize light mode:
.uc-calendar {
/* Primary color */
--cal-primary: #10b981; /* Green theme */
--cal-primary-dark: #059669;
--cal-primary-light: #d1fae5;
/* Today indicator */
--cal-today-bg: #d1fae5;
--cal-today-text: #059669;
}Target .uc-calendar.uc-dark to customize dark mode:
.uc-calendar.uc-dark {
/* Dark mode colors */
--cal-bg: #1f2937;
--cal-bg-secondary: #111827;
--cal-text: #f9fafb;
--cal-border: #374151;
/* Dark mode today/selected indicators */
--cal-today-bg: #065f46; /* Custom dark green */
--cal-selected-bg: #047857;
}Example with a complete brand color scheme:
/* Light mode - Yellow theme */
.uc-calendar {
--cal-primary: #eab308; /* yellow-500 */
--cal-primary-dark: #ca8a04; /* yellow-600 */
--cal-primary-light: #fef9c3; /* yellow-100 */
--cal-today-bg: #fef9c3;
--cal-today-text: #854d0e;
}
/* Dark mode - Yellow theme */
.uc-calendar.uc-dark {
--cal-primary: #eab308;
--cal-primary-dark: #facc15;
--cal-primary-light: #713f12;
--cal-today-bg: #713f12; /* yellow-900 */
--cal-selected-bg: #854d0e; /* yellow-800 */
}| Variable | Category | Description |
|---|---|---|
| --cal-primary | Colors | Primary theme color |
| --cal-primary-dark | Colors | Darker primary (hover states) |
| --cal-primary-light | Colors | Lighter primary (backgrounds) |
| --cal-bg | Colors | Background color |
| --cal-bg-secondary | Colors | Secondary background |
| --cal-text | Colors | Primary text color |
| --cal-text-subtle | Colors | Subtle text color |
| --cal-border | Colors | Border color |
| --cal-today-bg | Colors | Today indicator background |
| --cal-selected-bg | Colors | Selected date background |
| --cal-font-size | Sizing | Base font size (default: 13px) |
| --cal-hour-height | Sizing | Hour row height (default: 60px) |
| --cal-event-height | Sizing | Event bar height (default: 22px) |
| --cal-tooltip-* | Tooltips | Tooltip colors, sizing, typography |
| --cal-loading-bg | Loading | Loading spinner overlay background (default: rgba(255, 255, 255, 0.7) in light mode, rgba(31, 41, 55, 0.7) in dark mode) |
Tip: Use .uc-calendar.uc-dark to override dark mode colors separately from light mode. This allows you to create cohesive themes that work in both modes.
Use the React wrapper for seamless integration with React applications.
import SimpleCalendarJsReact from './simple-calendar-js-react.jsx';
import './simple-calendar-js.css';import { useState } from 'react';
import SimpleCalendarJsReact from './simple-calendar-js-react.jsx';
function App() {
const [isDark, setIsDark] = useState(false);
const fetchEvents = async (start, end) => {
const response = await fetch(`/api/events?start=${start}&end=${end}`);
return response.json();
};
const handleEventClick = (event) => {
console.log('Event clicked:', event);
};
return (
<>
<button onClick={() => setIsDark(!isDark)}>
Toggle Theme
</button>
<SimpleCalendarJsReact
defaultView="month"
defaultDate={new Date(2024, 5, 15)}
weekStartsOn={1}
darkMode={isDark}
fetchEvents={fetchEvents}
onEventClick={handleEventClick}
onSlotClick={(date) => console.log('Slot clicked:', date)}
onViewChange={(view) => console.log('View changed:', view)}
style={{ height: '600px' }}
/>
</>
);
}import { useRef } from 'react';
function App() {
const calRef = useRef();
const handleNextWeek = () => {
calRef.current.setView('week');
calRef.current.navigate(1);
};
return (
<>
<button onClick={handleNextWeek}>Next Week</button>
<SimpleCalendarJsReact ref={calRef} />
</>
);
}darkMode - Boolean prop for theme controlonEventClick, onSlotClick, onViewChange, onNavigate - Event handlersstyle, className - Apply to wrapper divUse the Vue 3 wrapper with Composition API.
import SimpleCalendarJsVue from './simple-calendar-js-vue.js';
import './simple-calendar-js.css';<script setup>
import { ref } from 'vue';
import SimpleCalendarJsVue from './simple-calendar-js-vue.js';
const isDark = ref(false);
const fetchEvents = async (start, end) => {
const response = await fetch(`/api/events?start=${start}&end=${end}`);
return response.json();
};
const handleEventClick = (event) => {
console.log('Event clicked:', event);
};
</script>
<template>
<button @click="isDark = !isDark">Toggle Theme</button>
<SimpleCalendarJsVue
:defaultView="'month'"
:defaultDate="new Date(2024, 5, 15)"
:weekStartsOn="1"
:darkMode="isDark"
:fetchEvents="fetchEvents"
@eventClick="handleEventClick"
@slotClick="(date) => console.log('Slot clicked:', date)"
@viewChange="(view) => console.log('View changed:', view)"
:style="{ height: '600px' }"
/>
</template><script setup>
import { ref } from 'vue';
const calendarRef = ref();
const handleNextWeek = () => {
calendarRef.value.setView('week');
calendarRef.value.navigate(1);
};
</script>
<template>
<button @click="handleNextWeek">Next Week</button>
<SimpleCalendarJsVue ref="calendarRef" />
</template>:darkMode - Boolean prop for theme control@eventClick, @slotClick, @viewChange, @navigate - Vue events:customClass - Apply CSS class to wrapperUse the Angular standalone component (or NgModule).
import { SimpleCalendarJsComponent } from './simple-calendar-js-angular.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [SimpleCalendarJsComponent],
// ...
})import { Component } from '@angular/core';
import { SimpleCalendarJsComponent } from './simple-calendar-js-angular.component';
@Component({
selector: 'app-calendar-demo',
standalone: true,
imports: [SimpleCalendarJsComponent],
template: `
<button (click)="isDark = !isDark">Toggle Theme</button>
<simple-calendar-js
[defaultView]="'month'"
[defaultDate]="new Date(2024, 5, 15)"
[weekStartsOn]="1"
[darkMode]="isDark"
[fetchEvents]="fetchEvents"
(eventClick)="handleEventClick($event)"
(slotClick)="handleSlotClick($event)"
(viewChange)="handleViewChange($event)"
[customStyle]="{ height: '600px' }"
></simple-calendar-js>
`
})
export class CalendarDemoComponent {
isDark = false;
fetchEvents = async (start: Date, end: Date) => {
const response = await fetch(`/api/events?start=${start}&end=${end}`);
return response.json();
};
handleEventClick(event: any) {
console.log('Event clicked:', event);
}
handleSlotClick(date: Date) {
console.log('Slot clicked:', date);
}
handleViewChange(view: string) {
console.log('View changed:', view);
}
}import { Component, ViewChild } from '@angular/core';
import { SimpleCalendarJsComponent } from './simple-calendar-js-angular.component';
@Component({
// ...
})
export class CalendarDemoComponent {
@ViewChild(SimpleCalendarJsComponent) calendar!: SimpleCalendarJsComponent;
handleNextWeek() {
this.calendar.setView('week');
this.calendar.navigate(1);
}
}@Input properties[darkMode] - Boolean input for theme control(eventClick), (slotClick), (viewChange), (navigate) - Angular outputs[customClass], [customStyle] - Apply to wrapper divCustomize the calendar appearance with CSS.
.my-green-calendar {
--cal-primary: #10b981;
--cal-primary-dark: #059669;
--cal-primary-light: #d1fae5;
}.my-calendar {
--cal-font-size: 14px;
--cal-event-height: 26px;
}/* Target all events */
.uc-calendar .uc-event {
border-radius: 6px;
font-weight: 600;
}
/* Target timed events in week/day view */
.uc-calendar .uc-timed-event {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}| Class | Target |
|---|---|
.uc-calendar | Calendar root container |
.uc-dark | Dark mode modifier |
.uc-toolbar | Top toolbar |
.uc-event | Event elements |
.uc-timed-event | Timed events in week/day view |
.uc-today | Today's date cell |