Documentation

A lightweight, zero-dependency JavaScript calendar component

Installation

SimpleCalendarJs is a zero-dependency calendar component. Include the CSS and JS files in your project:

Via Script Tags

<link rel="stylesheet" href="simple-calendar-js.css" />
<script src="simple-calendar-js.js"></script>

Via ES Modules

import SimpleCalendarJs from './simple-calendar-js.js';
import './simple-calendar-js.css';

Note: Make sure to include the CSS file for proper styling.

Quick Start

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);
  }
});
View Live Demo

Options

All available options when creating a calendar instance:

View Options

OptionTypeDefaultDescription
modeString'calendar'Calendar mode: 'calendar' (standard calendar with events), 'date-picker' (single date selection), or 'range-picker' (date range selection)
defaultViewString'month'Initial view: 'month', 'week', or 'day'
defaultDateDatenullInitial date to display (defaults to today if not provided)
weekStartsOnNumber0First day of week: 0 (Sunday) or 1 (Monday)
localeString'default'Locale code (e.g., 'en-US', 'fr-FR', 'pt-PT'). Supports 34+ locales.
weekdayFormatString'short'Weekday name format: 'narrow' (1-2 letters), 'short' (abbreviated), or 'long' (full name)
use24HourBooleanfalseUse 24-hour time format instead of 12-hour with AM/PM
showTimeInItemsBooleantrueShow time in event items across all views
showGridLinesBooleantrueShow/hide grid lines (borders between cells and time slots)
showBorderBooleantrueShow/hide calendar outer border

Display Options

Control which toolbar elements are visible:

OptionTypeDefaultDescription
showToolbarBooleantrueShow/hide the entire toolbar
showTodayButtonBooleantrueShow/hide the "Today" button
showNavigationBooleantrueShow/hide navigation arrows (← →)
showTitleBooleantrueShow/hide the month/week/day title
showYearPickerBooleantrueEnable/disable year selection (clickable year)
showViewSwitcherBooleantrueShow/hide Month/Week/Day view switcher buttons
enabledViewsArray['month', 'week', 'day']Which views are available to switch to
listDaysForwardNumber30Number of days forward to display in list view (only applies when list view is active)
showTooltipsBooleantrueShow/hide event tooltips on hover
tooltipAllowHtmlBooleantrueAllow HTML in tooltips (sanitized for security). Supports: <b>, <i>, <strong>, <em>, <span>, <br>, <a> with safe attributes and URL validation
tooltipBgColorString | nullnullCustom tooltip background color (hex). If null, uses CSS variable --cal-tooltip-bg
tooltipTextColorString | nullnullCustom tooltip text color (hex). If null, uses CSS variable --cal-tooltip-text
tooltipMaxWidthNumber250Maximum tooltip width in pixels
tooltipDelayNumber400Delay in milliseconds before tooltip appears on hover
enableDragDropBooleanfalseEnable drag and drop to move events
enableResizeBooleanfalseEnable resizing events to change duration
autoContrastTextBooleanfalseAutomatically calculate contrasting text color based on event background color for better readability
contrastLevelString'high'Text contrast level when autoContrastText is enabled: 'high' (black/white), 'medium' (darker/lighter shade), 'low' (subtle variation)
allowHtmlInEventsBooleantrueAllow basic HTML tags in event titles (sanitized for security). Supports: <b>, <i>, <strong>, <em>, <span>, <br> with class attribute for icons
monthTimedEventStyleString'list'Display style for timed events in month view: 'list' (schedule format) or 'block' (traditional blocks)
monthDayNumberAlignString'left'Horizontal alignment of day numbers in month view cells: 'left', 'center', or 'right'
showEventBorderBooleanfalseDisplay 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).

Picker Modes

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.

Date Picker Mode

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();

Range Picker Mode

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();

Picker Mode Behavior

  • In picker modes, the calendar does not display events from fetchEvents
  • The onSlotClick callback is not triggered in picker modes
  • Users can navigate between months/weeks/days normally to select dates from any time period
  • The selected date(s) persist across view changes (month/week/day)

Picker API Methods

MethodReturnsDescription
getSelectedDate()Date | nullGet the currently selected date in date-picker mode
setSelectedDate(date)voidProgrammatically set the selected date in date-picker mode
getSelectedRange()Object | nullGet the selected date range in range-picker mode. Returns {start: Date, end: Date} or null
setSelectedRange(startDate, endDate)voidProgrammatically set the selected date range in range-picker mode
clearSelection()voidClear the current date or range selection in picker modes

Callbacks

Register event handlers for user interactions:

CallbackParametersDescription
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

Event Object Format

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.

Example

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);
  }
});

Timezone Handling

SimpleCalendarJs relies on JavaScript's native Date object for timezone handling, which means events are automatically displayed in the user's local timezone.

How It Works

  1. Automatic Conversion: JavaScript Date objects automatically convert to the user's browser timezone
  2. No Configuration Needed: The calendar has no timezone settings - it uses the browser's timezone
  3. Backend Responsibility: Your backend should send timezone-aware date strings

Best Practices

✓ Recommended - Send ISO 8601 strings with 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)
  }));
}

✗ Avoid - Sending dates without timezone info

// BAD: "2024-03-15T10:00:00" (no timezone)
// JavaScript interprets this as LOCAL time, not UTC
// This can cause issues for users in different timezones

Multi-Timezone Example

Scenario: 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 AM

Important Notes

  • Storage: Always store events in UTC in your database
  • API Format: Send dates as ISO 8601 strings with timezone information
  • Display: The calendar automatically shows events in the user's local timezone
  • No Timezone Selector: The calendar doesn't provide UI to change timezone - it always uses the browser's timezone
  • Time Formatting: Uses Intl.DateTimeFormat which respects the user's locale and timezone

Example Backend Response

{
  "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.

Tooltips

SimpleCalendarJs includes a powerful tooltip system with HTML support, smart positioning, and extensive customization options.

How Tooltips Work

Tooltips appear when you hover over an event for default 400ms delay. They display content based on the following priority:

  1. tooltip property (highest priority) - Custom tooltip content
  2. description property - Falls back if no tooltip is provided
  3. title property - Falls back if neither tooltip nor description is provided

Basic Usage

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
  }
];

Basic Text Tooltips

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'
}

Rich HTML Tooltips

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>'
}

Supported HTML Tags

  • <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)

Security Features

  • • Dangerous protocols blocked (javascript:, data:)
  • • Only safe CSS properties allowed in style
  • • All event handlers stripped (onclick, etc.)
  • • Script tags and dangerous elements removed

Configuration Options

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) => { /* ... */ }
});

Per-Event HTML Override

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
}

Smart Positioning

Tooltips use fixed positioning and automatically adjust to stay visible:

  • Fixed to viewport: Never clipped by calendar container boundaries
  • Edge detection:
    • - Near top: tooltip appears below the event
    • - Near right edge: tooltip shifts left
    • - Near left edge: tooltip shifts right
  • Dynamic arrow: Arrow always points to the hovered event center

Advanced Example

{
  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
}

Disabling Tooltips

You can disable tooltips globally using the showTooltips option:

const calendar = new SimpleCalendarJs('#calendar', {
  showTooltips: false,  // Disable all tooltips
  fetchEvents: async (start, end) => { /* ... */ }
});

Styling Tooltips

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.

Drag and Drop

SimpleCalendarJs supports drag and drop for moving events and resizing them to change their duration.

Configuration

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
      })
    });
  }
});

Moving Events (enableDragDrop)

How It Works:

  • Drag Initiation: Click and hold on an event, then move at least 5px or wait 150ms
  • Visual Feedback: The event follows your cursor while dragging
  • Snap to Grid: Events snap to 15-minute intervals in week/day views
  • Drop: Release to drop the event at the new date/time
  • Cancel: Press ESC to cancel the drag operation
  • Duration Preservation: Events maintain their duration when moved
  • Touch Support: Full support for mobile/tablet touch gestures

Cross-Boundary Conversion:

When dragging events between different sections:

Timed Event → All-Day Section (Week/Day Views):

  • • Converts to an all-day event
  • • Preserves the day span

All-Day Event → Timed Section (Week/Day Views):

  • • Converts to a timed event
  • • Default duration: 1 hour
  • • Snaps to the time slot where dropped

Month View:

  • • Events maintain their original type (all-day stays all-day, timed stays timed)
  • • Timed events preserve their original time of day

Resizing Events (enableResize)

Horizontal Resize (All-Day Events):

  • Visual Indicator: Small vertical line appears on the right edge when hovering
  • How It Works: Drag the right edge to change the number of days the event spans
  • Available In: Month view, week/day all-day sections
  • Minimum: 1 day

Vertical Resize (Timed Events):

  • Visual Indicator: Small horizontal line appears at the bottom when hovering
  • How It Works: Drag the bottom edge to change the end time
  • Available In: Week/day timed sections
  • Snap to Grid: 15-minute intervals
  • Minimum: 15 minutes
  • Live Feedback: Time display updates to show start and end times as you drag

Callback Parameters

The onEventDrop callback receives the same parameters for both move and resize operations:

ParameterTypeDescription
eventObjectThe updated event object (with new start/end)
oldStartDateOriginal start date/time
oldEndDateOriginal end date/time
newStartDateNew start date/time
newEndDateNew end date/time

Detecting Operation Type:

  • Move: oldStart !== newStart
  • Resize: oldStart === newStart and oldEnd !== newEnd

Important Notes:

  • • The calendar updates the event internally before firing the callback
  • • You must update your backend in the onEventDrop callback
  • • If the backend update fails, call calendar.refresh() to revert to the previous state
  • • Both features are disabled in list view (read-only)
  • • You can enable one, both, or neither feature independently

Month View Timed Event Display Style

The monthTimedEventStyle option controls how timed events are displayed in month view:

List Style (Default: 'list')

Schedule-style display with horizontal layout:

  • Colored dot: Shows event color as a small circle
  • Time: Displays start time (if showTimeInItems is enabled)
  • Title: Event title truncated with ellipsis if too long
  • Compact: Clean, minimal appearance similar to schedule apps
const calendar = new SimpleCalendarJs('#calendar', {
  monthTimedEventStyle: 'list',  // Default
  showTimeInItems: true           // Shows time next to dot
});

Block Style ('block')

Traditional calendar block display:

  • Colored Background: Full event background in event color
  • Time Display: Start time shown inside block (if enabled)
  • Classic Look: Traditional calendar appearance
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 Row Heights

Month view automatically adapts row heights based on your container's height and event content:

Fixed Height Container

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'
});

Natural Height Container (Auto/Unlimited Mode)

When the container has no explicit height (or height: auto), the calendar displays all events with variable row heights:

  • Rows with many events → Automatically expand to fit all events
  • Rows with few events → Use minimal vertical space
  • Empty rows → Collapse to minimum height (80px)
  • Each row is independent → Heights calculated individually per row
// 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'
});

Benefits of Variable Row Heights

  • Eliminates wasted space: Empty rows don't take up unnecessary vertical space
  • Optimal density: Each week row is exactly as tall as it needs to be
  • No uniform padding: Weeks with 20 events don't force empty weeks to be equally tall
  • Better UX: Users see all events without clicking "+N more" when there's room

Recommendation: Use fixed height containers when you need predictable layouts, and natural height when you want to show all events with minimal scrolling.

Auto-Contrast Text Color

SimpleCalendarJs can automatically calculate the best text color for event text based on the background color, ensuring optimal readability with configurable contrast levels.

Contrast Levels

Choose from three contrast levels to match your design preferences:

High Contrast (default: 'high')

Pure black or white text for maximum readability:

  • Light backgrounds (luminance > 0.5) → Black text (#000000)
  • Dark backgrounds (luminance ≤ 0.5) → White text (#ffffff)

Medium Contrast ('medium')

Darker or lighter shade of the background color for a more integrated look:

  • Light backgrounds → Darkened version of the background color
  • Dark backgrounds → Lightened version of the background color

Low Contrast ('low')

Subtle variation that maintains the color family:

  • Light backgrounds → Slightly darker shade
  • Dark backgrounds → Slightly lighter shade

Basic Usage

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
      }
    ];
  }
});

Manual Override

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)
}

Priority Order

Text color is determined in this order:

  1. event.textColor (highest priority) - Manual override
  2. autoContrastText: true - Automatic calculation based on background color
  3. CSS variable --cal-event-text (default) - Fallback to theme default

When 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.

HTML in Event Titles

SimpleCalendarJs allows basic HTML formatting in event titles for icons, bold/italic text, and simple styling while maintaining security through automatic sanitization.

Security & Sanitization

All HTML is automatically sanitized to prevent XSS attacks:

  • Allowed tags: <b>, <i>, <strong>, <em>, <span>, <br>
  • Allowed attributes: class (for icon libraries like Font Awesome)
  • Blocked: <script>, event handlers (onclick, onerror), dangerous attributes
  • Character limit: Class names are sanitized to alphanumeric, dash, underscore, and space only

Basic Usage

const 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')
      }
    ];
  }
});

Font Awesome Icons

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
}

Per-Event Control

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.

List View

Display upcoming events in a chronological list format. Perfect for mobile devices and quick overview of upcoming events.

Basic Usage

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)
});

Features

  • • Shows events from current date/time forward
  • • Groups events by date with sticky headers
  • • Color-coded event indicators
  • • Mobile-friendly design
  • • Configurable range with listDaysForward option

Behavior

Upcoming Events View

List view always displays events from the current date/time forward, unlike other views which are navigable:

  • No navigation buttons: List view doesn't have previous/next navigation
  • No "Today" button: Already showing upcoming events from now
  • Date range title: Shows the range being displayed (e.g., "Feb 22 - Mar 24, 2026")
  • Multi-day events: Active multi-day events appear under today's date, not their start date

Configuration Example

// 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.

Methods

Programmatically control the calendar:

MethodParametersDescription
setView(view)'month' | 'week' | 'day' | 'list'Switch to a different view
navigate(direction)numberNavigate by offset: 1 = next, -1 = previous
goToDate(date)DateJump 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

Example

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();

Event Caching

SimpleCalendarJs intelligently caches fetched events to minimize unnecessary network requests:

  • Smart fetch strategy: Always fetches the full month grid (including leading/trailing days), regardless of current view
  • Automatic caching: Events are cached by date range after each fetch
  • Efficient reuse: View switches and navigation within the cached range use cached data (no spinner, instant display)
  • Fetch only when needed: New data is fetched only when the required range extends beyond the cache
  • Manual refresh: Call calendar.refresh() when events change externally (added, updated, or deleted)

How it works:

  • • When in month/week/day views: Fetches the full month grid for the current date context
  • • When in list view: Fetches the specific forward range (configurable with listDaysForward)
  • • Subsequent navigation within the same month uses cached data
  • • Navigation to a different month triggers a new fetch for that month's full grid
// 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)

Persisting User Preferences

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.

Using LocalStorage (Persistent Across Sessions)

// 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());
  }
});

Using SessionStorage (Per-Tab)

// 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())
});

Using URL Query Parameters (Shareable State)

// 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);
  }
});

React Example with State Hook

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}
    />
  );
}

Vue Example with Composable

// 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>

Server-Side Preferences (Synced Across Devices)

// 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);
  }
});

What to Persist

Common preferences to save:

  • View mode: 'month', 'week', 'day', or 'list'
  • Current date: Last viewed date for returning to the same position
  • Enabled views: Which view modes the user prefers to have available
  • UI preferences: Dark mode, show/hide options

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.

Theming

SimpleCalendarJs supports dark mode and color customization via CSS custom properties.

Dark Mode

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);

Override Light Mode Colors

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;
}

Override Dark Mode Colors

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;
}

Customize Both Light and Dark Modes

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 */
}

Available CSS Variables

VariableCategoryDescription
--cal-primaryColorsPrimary theme color
--cal-primary-darkColorsDarker primary (hover states)
--cal-primary-lightColorsLighter primary (backgrounds)
--cal-bgColorsBackground color
--cal-bg-secondaryColorsSecondary background
--cal-textColorsPrimary text color
--cal-text-subtleColorsSubtle text color
--cal-borderColorsBorder color
--cal-today-bgColorsToday indicator background
--cal-selected-bgColorsSelected date background
--cal-font-sizeSizingBase font size (default: 13px)
--cal-hour-heightSizingHour row height (default: 60px)
--cal-event-heightSizingEvent bar height (default: 22px)
--cal-tooltip-*TooltipsTooltip colors, sizing, typography
--cal-loading-bgLoadingLoading 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.

React

Use the React wrapper for seamless integration with React applications.

Installation

import SimpleCalendarJsReact from './simple-calendar-js-react.jsx';
import './simple-calendar-js.css';

Basic Usage

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' }}
      />
    </>
  );
}

Using Imperative Methods

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} />
    </>
  );
}

Props

  • • All calendar options are available as props
  • darkMode - Boolean prop for theme control
  • onEventClick, onSlotClick, onViewChange, onNavigate - Event handlers
  • style, className - Apply to wrapper div

Vue

Use the Vue 3 wrapper with Composition API.

Installation

import SimpleCalendarJsVue from './simple-calendar-js-vue.js';
import './simple-calendar-js.css';

Basic Usage

<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>

Using Imperative Methods

<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>

Props & Events

  • • All calendar options are available as props (use kebab-case or camelCase)
  • :darkMode - Boolean prop for theme control
  • @eventClick, @slotClick, @viewChange, @navigate - Vue events
  • :customClass - Apply CSS class to wrapper

Angular

Use the Angular standalone component (or NgModule).

Installation

import { SimpleCalendarJsComponent } from './simple-calendar-js-angular.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [SimpleCalendarJsComponent],
  // ...
})

Basic Usage

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);
  }
}

Using Methods via ViewChild

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);
  }
}

Inputs & Outputs

  • • All calendar options are available as @Input properties
  • [darkMode] - Boolean input for theme control
  • (eventClick), (slotClick), (viewChange), (navigate) - Angular outputs
  • [customClass], [customStyle] - Apply to wrapper div

Custom Styling

Customize the calendar appearance with CSS.

Example: Green Theme

.my-green-calendar {
  --cal-primary: #10b981;
  --cal-primary-dark: #059669;
  --cal-primary-light: #d1fae5;
}

Example: Larger Text

.my-calendar {
  --cal-font-size: 14px;
  --cal-event-height: 26px;
}

Example: Custom Event Styling

/* 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);
}

Important CSS Classes

ClassTarget
.uc-calendarCalendar root container
.uc-darkDark mode modifier
.uc-toolbarTop toolbar
.uc-eventEvent elements
.uc-timed-eventTimed events in week/day view
.uc-todayToday's date cell