webcito/bs-calendar

A jQuery-based Bootstrap calendar plugin with day/week/month/agenda/year views.

Maintainers

Package info

github.com/ThomasDev-de/bs-calendar

Documentation

Language:JavaScript

pkg:composer/webcito/bs-calendar

Fund package maintenance!

Buy Me A Coffee

Statistics

Installs: 281

Dependents: 0

Suggesters: 0

Stars: 24

Open Issues: 0


README

Version jQuery Bootstrap License

bs-calendar is a jQuery plugin for Bootstrap 5 calendars with day, 4day, week, month, agenda, and year views. It supports remote appointment loading, calendar filters, search, holidays, custom formatting, drag-create, drag-move, tasks, and local appointment add/edit/delete methods.

As of version 2, Bootstrap 4 is no longer supported. Use version ^1 for Bootstrap 4 projects.

Calendar Preview Calendar Preview Calendar Preview Calendar Preview

Table of Contents

Requirements

  • jQuery ^3
  • Bootstrap ^5 CSS and JavaScript bundle
  • Bootstrap Icons ^1
  • PHP and Composer are only needed for running the local demo with the bundled vendor/ dependencies.

No Node.js build step is required for normal usage. The browser-ready files are shipped in dist/.

Installation

Use CDN/script tags:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">

<div id="calendar"></div>

<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/ThomasDev-de/bs-calendar@2.3.6/dist/bs-calendar.min.js"></script>

Or install via Composer:

composer require webcito/bs-calendar

After Composer installation, include vendor/webcito/bs-calendar/dist/bs-calendar.min.js together with jQuery, Bootstrap, and Bootstrap Icons.

Quick Start

<div id="calendar"></div>

<script>
    $(function () {
        $('#calendar').bsCalendar({
            locale: 'de-DE',
            startView: 'week',
            startWeekOnSunday: false
        });
    });
</script>

Load appointments from a function:

$('#calendar').bsCalendar({
    url(requestData) {
        return fetch('/api/appointments?' + new URLSearchParams(requestData))
            .then(response => response.json());
    }
});

Add, edit, and delete appointments locally:

const appointment = {
    title: 'New meeting',
    start: '2026-05-08 10:00:00',
    end: '2026-05-08 11:00:00'
};

$('#calendar').bsCalendar('addAppointment', appointment);
$('#calendar').bsCalendar('editAppointment', {id: appointment.id, title: 'Updated meeting'});
$('#calendar').bsCalendar('deleteAppointment', appointment.id);

Run the Demo

composer install
php -S localhost:8000 -t .

Open http://localhost:8000/demo/index.html.

The demo contains one calendar instance and shows a modal-based add/edit/delete flow using addAppointment, editAppointment, and deleteAppointment.

Core Concepts

  • url controls remote appointment loading. It can be null, a URL string, or a function returning a Promise.
  • calendars defines sidebar filters. Active calendar IDs are always sent as calendarIds in remote requests.
  • add.bs.calendar, edit.bs.calendar, and delete.bs.calendar are intent events. They tell your application what the user wants; they do not save anything.
  • addAppointment, editAppointment, and deleteAppointment mutate only the currently loaded browser-side appointment list. For backend-backed calendars, persist to your backend first or call refresh after saving.
  • refresh reloads data from url.
  • render re-renders already loaded data without calling url.
  • year view uses summary objects (date, total, optional content), not full appointment objects.
  • Recurring appointments are normal appointment objects with a recurrence object. They are expanded only for loaded appointment views.
  • Tasks are normal appointment objects with a task object.

Appointment Data

For day, 4day, week, month, agenda, and search results, appointments use this shape:

{
  "id": 123,
  "title": "Project Kickoff",
  "start": "2026-05-08 10:00:00",
  "end": "2026-05-08 11:00:00",
  "allDay": false,
  "calendarId": "work",
  "description": "Discuss goals and next steps.",
  "color": "primary",
  "icon": "bi bi-briefcase",
  "link": "https://example.com",
  "location": "Room 5A",
  "editable": true,
  "deleteable": true,
  "overlap": false,
  "recurrence": {
    "frequency": "weekly",
    "interval": 1,
    "until": "2026-12-31",
    "daysOfWeek": [5]
  },
  "task": {
    "checked": false,
    "priority": "high",
    "due": "2026-05-08 09:30:00"
  }
}

Required fields:

Field Type Description
title string Appointment title.
start string Start date/time in YYYY-MM-DD HH:mm:ss, YYYY-MM-DD, or another local date-time format accepted by parseDateInput.
end string End date/time in YYYY-MM-DD HH:mm:ss, YYYY-MM-DD, or another local date-time format accepted by parseDateInput.

Optional fields:

Field Type Default Description
id string or number generated when missing Required for later editAppointment and deleteAppointment calls. Missing IDs are generated with crypto.randomUUID() when available.
allDay boolean false Treat the appointment as an all-day item.
calendarId string or number none Useful for server-side or custom filtering by calendar.
description string none Rendered by the default info window formatter as HTML.
color string mainColor Bootstrap color, CSS color, CSS variable, or class combination.
icon string appointment/all-day icon Bootstrap icon class for this appointment. Task state icons override this for task rendering.
link string or object none Rendered by the default info window formatter.
location string, array, or null none Rendered by the default info window formatter. Arrays are joined with <br>.
editable boolean, string, or number true Controls whether the info window shows edit/duplicate controls. Boolean-like strings such as "false", "0", and "no" are treated as false.
deleteable boolean true Controls whether the info window shows a delete button.
overlap boolean, string, or number false Day/week/4day view only. Boolean-like true, "true", "1", or "yes" renders this appointment full-width and stacked instead of side-by-side.
recurrence object or null none Expands one source appointment into visible occurrences. See Recurring appointments.
task object or null none If provided, the appointment is treated as a task. See Task fields.

Reserved field:

Field Description
extras Internal render context generated by bs-calendar. Do not send or persist it as appointment data.

All-day appointments:

{
  "title": "Conference",
  "start": "2026-05-08",
  "end": "2026-05-09",
  "allDay": true
}

The plugin normalizes all-day start/end values to full-day boundaries internally.

Link object:

{
  "href": "https://example.com",
  "text": "Open details",
  "target": "_blank",
  "rel": "noopener noreferrer",
  "disabled": false,
  "html": "<strong>Open</strong>",
  "color": "primary"
}

Link object fields:

Field Type Default Description
href string none Link URL. Empty links are not rendered.
text string href Link text when html is not provided.
target string "_blank" Link target.
rel string "noopener noreferrer" Link rel attribute.
disabled boolean false If true, no link is rendered.
html string none Raw HTML content for the link body.
color string "primary" Color used by the default link formatter.

Recurring Appointments

Recurring appointments are stored as one source appointment with a recurrence object. bs-calendar expands that source appointment into renderable occurrences for the currently loaded date range. Your database can therefore keep one master record instead of one row per date. In normal appointment views the source appointment itself is not rendered in addition to its occurrences.

Core model:

Term Meaning
Source appointment The appointment object returned by your backend or added with addAppointment. It owns the recurrence rule.
Occurrence A generated render item for one matching date. It keeps the source data and adds recurrence metadata.
recurringId The source appointment ID on a generated occurrence.

Minimal weekly example:

{
  "id": "weekly-training",
  "title": "Weekly training",
  "start": "2026-06-01 18:00:00",
  "end": "2026-06-01 19:00:00",
  "recurrence": {
    "frequency": "weekly",
    "interval": 1,
    "until": "2026-12-31",
    "daysOfWeek": [1],
    "exceptions": ["2026-07-06"]
  }
}

Supported recurrence fields:

Field Type Description
frequency string daily, weekly, monthly, or yearly. Short aliases day, week, month, year and freq are accepted.
interval number Repeat every n frequency units. Missing, invalid, or smaller-than-1 values are treated as 1.
until string Optional inclusive end date. Alias end is accepted. Date-only values include the whole day.
count number Optional maximum number of series positions. Exceptions still skip their dates.
daysOfWeek array<number> Weekly weekdays using JavaScript weekday numbers, 0 Sunday through 6 Saturday. Defaults to the start weekday.
exceptions array<string> Date-only values to skip, e.g. ["2026-07-06"]. Time parts are ignored and normalized to the local date.

Occurrence fields generated by bs-calendar:

Field Example Description
id weekly-training__2026-06-08 Generated from the source ID and occurrence date.
start 2026-06-08 18:00:00 Occurrence start with the source time preserved.
end 2026-06-08 19:00:00 Occurrence end with the source duration preserved.
recurringId weekly-training Source appointment ID.
recurrenceDate 2026-06-08 Local date represented by this occurrence.
recurrenceIndex 1 Zero-based index in the generated series positions.
isOccurrence true Marks the item as a generated occurrence rather than the source record.

after-load.bs.calendar, appointment formatters, and intent events receive the expanded occurrence objects. Use appointment.recurringId or extras.recurrence.recurringId whenever you need to map an occurrence back to the source record.

Frequency behavior:

Frequency Behavior
daily Matches every interval days from the source start date.
weekly Matches selected daysOfWeek in every interval weeks from the source start week.
monthly Matches the same day of month as the source start date. Months without that day are skipped.
yearly Matches the same month and day as the source start date.

Multiple weekdays:

{
  "id": "training",
  "title": "Training",
  "start": "2026-06-01 18:00:00",
  "end": "2026-06-01 19:00:00",
  "recurrence": {
    "frequency": "weekly",
    "daysOfWeek": [1, 3],
    "until": "2026-06-30"
  }
}

This creates Monday and Wednesday occurrences from 2026-06-01 onward.

Every second week with one skipped date:

{
  "id": "team-sync",
  "title": "Team sync",
  "start": "2026-06-02 09:30:00",
  "end": "2026-06-02 10:00:00",
  "recurrence": {
    "frequency": "weekly",
    "interval": 2,
    "until": "2026-08-31",
    "exceptions": ["2026-07-14"]
  }
}

Client-side local API behavior:

$('#calendar').on('edit.bs.calendar', function (event, appointment) {
    event.preventDefault();

    const sourceId = appointment.recurringId || appointment.id;

    $('#calendar').bsCalendar('editAppointment', {
        id: sourceId,
        title: 'Updated series title'
    });
});

addAppointment, editAppointment, and deleteAppointment work on the source appointment list. If you pass a generated occurrence object to editAppointment or deleteAppointment, bs-calendar uses recurringId and updates/removes the source appointment. This first recurrence API does not create per-occurrence overrides.

Formatter example:

$('#calendar').bsCalendar({
    formatter: {
        month(appointment, extras) {
            const marker = extras.recurrence.isOccurrence ? '[recurring] ' : '';
            return `${marker}${appointment.title}`;
        }
    }
});

Remote data guidance:

  • For day, 4day, week, month, and agenda, your url receives fromDate and toDate. Return recurring source appointments that can have at least one occurrence in that range.
  • Do not filter recurring source appointments only by start date. A weekly source from January can still produce a June occurrence.
  • A practical server-side overlap check is: source start <= toDate and either no until or until >= fromDate. If you use count, your backend may need its own recurrence check for exact filtering.
  • year view uses summary rows (date, total, optional content), so recurrence expansion is not applied there. Return already counted year summary data.
  • Search is controlled by your url response. For broad search across a long-running series, return the matching source or concrete rows your application wants to show.

Current boundaries:

  • Supported rules are intentionally simple: daily, weekly, monthly, yearly, interval, until/end, count, weekly days, and date exceptions.
  • RRULE strings, nth weekday rules, timezone-specific recurrence sets, moved occurrences, and edited single occurrences are not supported yet.
  • Occurrences inherit the source appointment fields. Store exceptions or future per-occurrence overrides in your own backend model.

Task Fields

An appointment is treated as a task when it contains a truthy task object.

Field Type Default Description
checked boolean false Whether the task is completed. Completed tasks render muted/struck through.
priority string "normal" Supported values are "low", "normal", and "high". Missing, empty, or unsupported values are normalized to "normal".
due string or null null Optional due date/time. If it is in the past and the task is not checked, task.isOverdue is generated internally.

Task behavior:

  • Task icons use icons.task, icons.taskDone, and icons.taskOverdue.
  • Clicking a task icon toggles task.checked locally and fires task-status-changed.bs.calendar.
  • The global task sidebar control is shown when showTasks is true.
  • Task visibility state is sent to normal view and search requests as showTasks.
  • task.isOverdue is internal render state. You may read it in callbacks, but you should not persist it as source data.

Remote Data with url

url is the appointment data source. It accepts three value types:

Value Behavior
null No remote appointment request is made. The current appointment list is cleared and after-load.bs.calendar fires with an empty array. Holidays can still be loaded.
string The plugin sends a jQuery AJAX GET request to that URL with requestData as query data. The response must match the view/search response contract below.
function The function is called as url(requestData) and must return a Promise/thenable resolving to the response data.

String URL example:

$('#calendar').bsCalendar({
    url: '/api/appointments'
});

Function URL example:

$('#calendar').bsCalendar({
    url(requestData) {
        return fetch('/api/appointments?' + new URLSearchParams(requestData))
            .then(response => response.json());
    }
});

Request data in normal appointment views:

View Request fields
day, 4day, week, month, agenda fromDate, toDate, view, showTasks, calendarIds
year year, view, showTasks, calendarIds

Request data in search mode:

Field Description
search Search string from the search input. Empty searches are skipped and return an empty local list.
limit Page size from options.search.limit.
offset Current search offset.
showTasks Current task visibility state.
calendarIds Active calendar IDs, always an array.

Normal response for day, 4day, week, month, and agenda:

[
  {
    "id": 1,
    "title": "Meeting",
    "start": "2026-05-08 10:00:00",
    "end": "2026-05-08 11:00:00",
    "color": "primary"
  }
]

Search response:

{
  "rows": [
    {
      "id": 1,
      "title": "Meeting",
      "start": "2026-05-08 10:00:00",
      "end": "2026-05-08 11:00:00"
    }
  ],
  "total": 42
}

Year-view response:

[
  {
    "date": "2026-05-08",
    "total": 3,
    "content": "3 appointments"
  }
]

Year summary fields:

Field Type Required Description
date string Yes Day in YYYY-MM-DD format.
total number Yes Badge number shown in year view. Must be greater than 0.
content string No Popover body. HTML rendering is enabled. Defaults to total.

Use queryParams to append custom request values:

$('#calendar').bsCalendar({
    url: '/api/appointments',
    queryParams(requestData) {
        return {
            userId: $('#user').val(),
            showTasks: requestData.showTasks
        };
    }
});

queryParams receives the generated requestData and should return an object. The returned object is merged into the request. Protected keys fromDate, toDate, year, and view cannot be overridden.

You can also change remote loading at runtime:

$('#calendar').bsCalendar('refresh', {
    url: '/api/other-appointments',
    queryParams(requestData) {
        return {teamId: 5};
    }
});

Add, Edit, and Delete Workflow

add.bs.calendar, edit.bs.calendar, and delete.bs.calendar are intent events. They let your app open a modal, confirm destructive actions, validate input, save to a backend, and then update the calendar.

Callback options receive the same payloads:

Event Callback Payload
add.bs.calendar onAdd(data, dragExtras) Proposed start/end for a new appointment.
edit.bs.calendar onEdit(appointment, extras, dragExtras) Current appointment plus render context.
delete.bs.calendar onDelete(appointment, extras) Appointment selected for deletion.

After a local mutation method has succeeded, the calendar fires completion events:

Event Callback Payload
added.bs.calendar onAdded(appointment, extras) Appointment that was added.
edited.bs.calendar onEdited(appointment, extras) Appointment after the local update.
deleted.bs.calendar onDeleted(appointment, extras) Appointment that was removed.

When drag-create is used, dragExtras contains the proposed start, end, hour-slot rule availability, and appointment duration rule availability. When drag-move or drag-resize is used, appointment still contains the original appointment and dragExtras contains the proposed new range.

If hourSlots.rules[].mode is blocked or exclusive, interactive creation, drag-moving, and drag-resizing respect those rules. Day/week/4day drag-create, drag-move, and drag-resize clamp to the nearest valid rule edge while dragging; invalid click-create and invalid drop targets do not fire add.bs.calendar or edit.bs.calendar.

For drag-create, drag-move, and drag-resize, the allowed interval starts as every exclusive range for that weekday. If no exclusive range exists, the whole visible hourSlots.start to hourSlots.end range is allowed. blocked ranges are then subtracted from those intervals. preferred ranges do not block dragging.

For backend-backed calendars, save to your backend first and then either call refresh so the updated data is loaded from url, or call addAppointment, editAppointment, or deleteAppointment for an immediate local update and ensure the backend returns the same data on the next refresh.

Options

All options can be passed during initialization:

$('#calendar').bsCalendar({
    locale: 'de-DE',
    startView: 'week'
});

Options may also be supplied through jQuery data-* attributes. JavaScript options override data attributes. Some options can be changed later with updateOptions.

Option Type Default Description
showAbout boolean true Shows the About dropdown.
locale string "en-GB" Locale for labels and date formatting. Underscores are normalized to hyphens.
title string or null null HTML/string title in the toolbar.
startWeekOnSunday boolean true If false, weeks start on Monday.
navigateOnWheel boolean true Enables mouse-wheel navigation over the calendar.
rounded number 5 Bootstrap rounded level 0 to 5. Invalid values fall back to 5.
border string "border border-0 rounded-0 shadow" Bootstrap classes used by bordered calendar UI elements.
search object or null {limit: 10, offset: 0} Search config. Set null to disable search UI.
search.limit number 10 Number of search results per page.
search.offset number 0 Initial search offset.
startDate Date or string new Date() Initial reference date. String values are parsed during initialization.
startView string "month" Initial view. Allowed values: day, 4day, week, month, agenda, year. Must be enabled in views.
mainColor string "primary" Default color used by highlights, controls, and appointments.
views array or comma-separated string ["year", "month", "agenda", "week", "4day", "day"] Enabled views. Invalid entries are removed; duplicates are removed; empty result falls back to all possible views.
holidays object or null null OpenHolidays configuration. See Holidays.
showAddButton boolean true Shows the toolbar add button.
draggable boolean false Enables drag-create in day/week/4day view, drag-move in day/week/4day/month view, and drag-resize from timed appointment edges in day/week/4day view. Touch locks native scrolling while a drag gesture is pending or active.
draggableSnapMinutes number 5 Snap interval in minutes for drag-create/move/resize in day/week/4day view. Minimum is 1.
translations object {search, searchNoResult} merged with locale translations Custom UI translations. See Localization and Translations.
icons object see Icons Bootstrap icon classes.
url null, string, or function null Appointment data source. See Remote Data with url.
queryParams function or null null Adds custom request params before loading appointments.
topbarAddons selector, element, jQuery object, or null null Element(s) inserted after the top toolbar.
sidebarAddons selector, element, jQuery object, or null null Element(s) appended to the sidebar.
formatter object see Formatters Custom render functions.
hourSlots object {height: 30, start: 0, end: 24} Day/week/4day hour grid configuration.
hourSlots.height number 30 Height in pixels for one hour. Minimum normalized value is 1.
hourSlots.start number or string 0 First visible hour. Normalized to 0 to 23. Supports decimals and HH:mm strings.
hourSlots.end number or string 24 Last visible hour boundary. Normalized to 1 to 24 and kept greater than start. Supports decimals and HH:mm strings.
hourSlots.rules object, array, or null null Highlight and availability rules for specific time slots. Accepts one object or an array of objects.
hourSlots.rules.startTime string '08:00' Start time for each rule range (format HH:mm).
hourSlots.rules.endTime string '17:00' End time for each rule range (format HH:mm).
hourSlots.rules.daysOfWeek array [1,2,3,4,5] Days of the week (0-6, Sun-Sat) for each rule range.
hourSlots.rules.mode string 'highlight' exclusive allows creation/move only inside the range, blocked prevents overlapping creation/move, preferred marks preferred work time, omitted mode only highlights.
hourSlots.rules.color string rgba(0,0,0,0.05) Color/styling for each rule range, normalized with getColors.
appointmentRules object {durationMinutes: null, durationStepMinutes: null, minDurationMinutes: null, maxDurationMinutes: null} Timed appointment duration rules for click-create, drag-create, drag-move, and drag-resize.
appointmentRules.durationMinutes number or null null Exact required duration in minutes. If set, resize handles are hidden for timed appointments.
appointmentRules.durationStepMinutes number or null null Allows only durations divisible by this value, e.g. 45 allows 45/90/135-minute appointments.
appointmentRules.minDurationMinutes number or null null Minimum allowed timed appointment duration.
appointmentRules.maxDurationMinutes number or null null Maximum allowed timed appointment duration.
calendars array or null null Sidebar calendar filters.
onAll function, function-name string, or null null Receives every event name and payload.
onInit function, function-name string, or null null Same payload as init.bs.calendar.
onAdd function, function-name string, or null null Same payload as add.bs.calendar.
onAdded function, function-name string, or null null Same payload as added.bs.calendar.
onEdit function, function-name string, or null null Same payload as edit.bs.calendar.
onEdited function, function-name string, or null null Same payload as edited.bs.calendar.
onDuplicate function, function-name string, or null null Same payload as duplicate.bs.calendar.
onDelete function, function-name string, or null null Same payload as delete.bs.calendar.
onDeleted function, function-name string, or null null Same payload as deleted.bs.calendar.
onView function, function-name string, or null null Same payload as view.bs.calendar.
onBeforeLoad function, function-name string, or null null Same payload as before-load.bs.calendar.
onAfterLoad function, function-name string, or null null Same payload as after-load.bs.calendar.
onTaskStatusChanged function, function-name string, or null null Same payload as task-status-changed.bs.calendar.
onShowInfoWindow function, function-name string, or null null Same payload as show-info-window.bs.calendar.
onHideInfoWindow function, function-name string, or null null Same payload as hide-info-window.bs.calendar.
onNavigateForward function, function-name string, or null null Same payload as navigate-forward.bs.calendar.
onNavigateBack function, function-name string, or null null Same payload as navigate-back.bs.calendar.
storeState boolean false Persists selected view, active calendars, and task visibility in localStorage.
showTasks boolean true Enables task UI and the global task toggle in the sidebar.
debug boolean false Enables debug logging.

Hour Slot Rule Priority

hourSlots.rules affect availability in this order:

  1. blocked wins whenever the requested time range overlaps a blocked range.
  2. exclusive applies next. If any exclusive rule exists for the weekday, work is allowed only when the requested time range is fully contained in an exclusive range.
  3. preferred allows work and sets isPreferred.
  4. highlight or an omitted mode is visual only and does not block work.
  5. If no rule matches, work is allowed.

This means overlapping blocked and exclusive rules are treated as blocked. Overlapping blocked and preferred rules are also treated as blocked.

Slot background colors use the same mode-aware priority. For overlapping colors, the winning availability rule provides the color.

Appointment Duration Rules

appointmentRules validates timed appointment durations during interactive creation, moving, and resizing. It is separate from hourSlots.rules: hourSlots.rules describes when work is allowed, while appointmentRules describes how long a timed appointment may be.

Fixed 60-minute appointments:

$('#calendar').bsCalendar({
    appointmentRules: {
        durationMinutes: 60
    }
});

With durationMinutes, click-create and drag-create propose exactly that duration. Resize handles are not rendered for timed appointments because the duration is fixed.

45-minute coaching blocks:

$('#calendar').bsCalendar({
    draggableSnapMinutes: 15,
    appointmentRules: {
        durationStepMinutes: 45,
        minDurationMinutes: 45
    }
});

This allows 45, 90, 135 minutes, and so on. draggableSnapMinutes still controls the pointer grid for start/end times; the duration rule then snaps the appointment length to the nearest valid duration.

Flexible 30-minute blocks between 30 and 120 minutes:

$('#calendar').bsCalendar({
    appointmentRules: {
        durationStepMinutes: 30,
        minDurationMinutes: 30,
        maxDurationMinutes: 120
    }
});

If durationMinutes is set, it wins over step/min/max rules. Otherwise, min/max are applied first and durationStepMinutes restricts the result to valid multiples. Drag event payloads expose the validation result as dragExtras.appointmentRules with canWork, durationMinutes, rules, and violations.

Calendar filters:

$('#calendar').bsCalendar({
    calendars: [
        {id: 'personal', title: 'Personal', color: 'primary', active: true},
        {id: 'work', title: 'Work', color: 'danger', active: true}
    ]
});

Calendar fields:

Field Type Required Default Description
id string or number Yes none Sent in calendarIds. Entries without an ID are removed.
title string No Calendar {i} Sidebar label.
color string No mainColor Sidebar color, normalized with getColors.
active boolean No true Initial filter state.

Translations

Key English default Description
today "Today" Text for the Today button.
day "Day" Label for the day view.
4day "4 Days" Label for the 4-day view.
week "Week" Label for the week view.
month "Month" Label for the month view.
year "Year" Label for the year view.
agenda "Agenda" Label for the agenda list view.
allDay "All day" Label for all-day appointments.
search "Type and press Enter" Search placeholder.
searchNoResult "No appointment found" Empty search message.
tasks "Tasks" Label for the task toggle and task badge.
taskPriorityHigh "High" Label for high-priority task badge.
taskPriorityNormal "Medium" Label for normal-priority task badge.
taskPriorityLow "Low" Label for low-priority task badge.
duplicate "Duplicate" Info-window duplicate action label.

Icons

Key Default
day "bi bi-calendar-day"
4day "bi bi-calendar-range"
week "bi bi-kanban"
month "bi bi-calendar-month"
year "bi bi-calendar4"
agenda "bi bi-list-ul"
about "bi bi-info-circle"
add "bi bi-plus-lg"
menu "bi bi-layout-sidebar-inset"
search "bi bi-search"
prev "bi bi-chevron-left"
next "bi bi-chevron-right"
link "bi bi-box-arrow-up-right"
appointment "bi bi-clock"
appointmentAllDay "bi bi-brightness-high"
task "bi bi-circle"
taskDone "bi bi-check2-circle"
taskOverdue "bi bi-exclamation-circle"

Events and Callbacks

Events use the .bs.calendar namespace:

$('#calendar').on('view.bs.calendar', function (event, view) {
    console.log(view);
});

Callback options receive the same payload as their matching event, without the jQuery event object. Callback options may be functions or global function-name strings.

Event Callback option jQuery handler payload Description
all.bs.calendar onAll(eventName, ...params) (event, eventName, ...params) Fired before every specific event except all itself. eventName includes .bs.calendar.
init.bs.calendar onInit() (event) Calendar initialized.
add.bs.calendar onAdd(data, dragExtras) (event, data, dragExtras) Add intent from toolbar, day/hour click, date click, or drag-create.
added.bs.calendar onAdded(appointment, extras) (event, appointment, extras) Appointment added with addAppointment.
edit.bs.calendar onEdit(appointment, extras, dragExtras) (event, appointment, extras, dragExtras) Edit intent from info window or drag-move.
edited.bs.calendar onEdited(appointment, extras) (event, appointment, extras) Appointment updated with editAppointment.
duplicate.bs.calendar onDuplicate(appointment, extras) (event, appointment, extras) Duplicate action clicked in the info window.
delete.bs.calendar onDelete(appointment, extras) (event, appointment, extras) Delete intent from info window.
deleted.bs.calendar onDeleted(appointment, extras) (event, appointment, extras) Appointment removed with deleteAppointment.
view.bs.calendar onView(view) (event, view) View rendered or changed.
navigate-forward.bs.calendar onNavigateForward(view, from, to) (event, view, from, to) Forward navigation completed. from and to are Date objects.
navigate-back.bs.calendar onNavigateBack(view, from, to) (event, view, from, to) Backward navigation completed. from and to are Date objects.
show-info-window.bs.calendar onShowInfoWindow(appointment, extras) (event, appointment, extras) Info window is about to be shown for a newly created info modal.
hide-info-window.bs.calendar onHideInfoWindow() (event) Info window closed by outside click.
before-load.bs.calendar onBeforeLoad(requestData) (event, requestData) Fired after requestData is built and before remote loading starts.
after-load.bs.calendar onAfterLoad(appointments) (event, appointments) Fired after appointment data has been normalized and stored.
task-status-changed.bs.calendar onTaskStatusChanged(appointment) (event, appointment) A task checkbox icon was toggled locally.

Methods

Call methods with the jQuery plugin method syntax:

$('#calendar').bsCalendar('refresh');
Method Params Description
refresh optional {url, view, queryParams} Reloads and renders. Can update settings.url, switch to an enabled view, and replace queryParams before loading.
render none Re-renders current loaded data without fetching.
clear none Clears rendered appointments and local appointment data. Ignored in search mode.
updateOptions object Deep-merges runtime options, normalizes settings, rebuilds affected UI, and fetches data.
addAppointment appointment object Adds one local appointment, generates an ID if missing, normalizes it, renders, and fires added.bs.calendar. Ignored in search mode and year view.
editAppointment appointment object with id, or {id, appointment} / {id, data} Deep-merges changes into the currently loaded appointment with the same ID, normalizes it, renders, and fires edited.bs.calendar. Ignored in search mode and year view.
editApointment same as editAppointment Backward-compatible misspelled alias.
deleteAppointment appointment id or object with id Deletes one currently loaded appointment by ID, renders, and fires deleted.bs.calendar. Ignored in search mode and year view.
destroy none Removes generated markup/events, aborts outstanding appointment requests, removes the info modal, and restores the original element state.
setDate date string, Date, or {date, view} Sets the visible reference date and optionally switches to an enabled view. Ignored in search mode.
setToday optional view string Sets the reference date to today and optionally switches to an enabled view. Ignored in search mode.
setView view string Switches to an enabled view and reloads/renders. Ignored in search mode.
setHourSlotRules object, array, or null Updates hourSlots.rules and refreshes the grid.
setLocale locale string Normalizes the locale and applies it through updateOptions. Ignored in search mode.

Examples:

$('#calendar').bsCalendar('refresh', {url: '/api/appointments'});
$('#calendar').bsCalendar('render');
$('#calendar').bsCalendar('clear');
$('#calendar').bsCalendar('updateOptions', {locale: 'fr-FR'});
$('#calendar').bsCalendar('addAppointment', {title: 'Call', start: '2026-05-08 10:00:00', end: '2026-05-08 10:30:00'});
$('#calendar').bsCalendar('editAppointment', {id: 123, title: 'Updated'});
$('#calendar').bsCalendar('deleteAppointment', 123);
$('#calendar').bsCalendar('setDate', {date: '2026-05-08', view: 'day'});
$('#calendar').bsCalendar('setToday', 'week');
$('#calendar').bsCalendar('setView', 'month');
$('#calendar').bsCalendar('setHourSlotRules', [
    {
        daysOfWeek: [1, 2, 3, 4, 5],
        startTime: '09:00',
        endTime: '17:00',
        mode: 'exclusive',
        color: 'rgba(25, 135, 84, 0.055)'
    },
    {
        daysOfWeek: [6],
        startTime: '10:00',
        endTime: '14:00',
        mode: 'preferred',
        color: 'rgba(13, 110, 253, 0.045)'
    },
    {
        daysOfWeek: [0],
        startTime: '00:00',
        endTime: '23:59',
        mode: 'blocked',
        color: 'rgba(220, 53, 69, 0.06)'
    }
]);
$('#calendar').bsCalendar('setLocale', 'de-DE');
$('#calendar').bsCalendar('destroy');

There is no public getAppointment method. The plugin only stores the currently loaded view/search appointment slice, so ID lookup would not be a reliable global data access API.

Formatters

Formatters customize appointment, search, holiday, info-window, and duration rendering.

$('#calendar').bsCalendar({
    formatter: {
        day(appointment, extras) {
            return appointment.title;
        },
        week(appointment, extras) {
            return appointment.title;
        },
        allDay(appointment, extras, view) {
            return appointment.title;
        },
        month(appointment, extras) {
            return appointment.title;
        },
        agenda(appointment, extras) {
            return appointment.title;
        },
        search(appointment, extras) {
            return appointment.title;
        },
        holiday(holiday, view) {
            return holiday.name?.[0]?.text || holiday.title;
        },
        window(appointment, extras) {
            return Promise.resolve(`<h3>${appointment.title}</h3>`);
        },
        duration(duration) {
            return `${duration.totalMinutes} min`;
        }
    }
});

Formatter signatures:

Formatter Signature Return
day (appointment, extras) HTML/string
week (appointment, extras) HTML/string
allDay (appointment, extras, view) HTML/string
month (appointment, extras) HTML/string
agenda (appointment, extras) HTML/string
search (appointment, extras) HTML/string
holiday (holiday, view) HTML/string
window (appointment, extras) Promise resolving to HTML/string
duration (duration) string

In month view, day cells automatically show a small expand button when their appointment list overflows. The expanded month-day overlay uses formatter.monthExpanded(appointment, extras) so the compact formatter.month output can stay single-line.

Extras Object

extras is generated for each appointment after loading/normalization.

Field Description
locale Locale used for formatting.
icon Appointment or task icon class used for rendering.
colors.origin Original color value.
colors.backgroundColor Computed background color.
colors.backgroundImage Computed background image/gradient.
colors.color Computed text color.
colors.classList Computed Bootstrap classes, if applicable.
colors.hex Computed hexadecimal color (#rrggbb) when resolvable, otherwise null.
start.date Start date in YYYY-MM-DD.
start.time Start time in HH:MM:SS.
end.date End date in YYYY-MM-DD.
end.time End time in HH:MM:SS.
duration.days Full days.
duration.hours Remaining hours.
duration.minutes Remaining minutes.
duration.seconds Remaining seconds.
duration.totalMinutes Total minutes.
duration.totalSeconds Total seconds.
duration.formatted Formatter output from formatter.duration.
hourSlotRules Mode-aware availability object derived from hourSlots.rules.
displayDates Per-day display data used by month/week/day rendering.
allDay Whether the appointment is all-day.
inADay Whether it stays within one calendar day.
isToday Whether the start date is today.
isNow Whether the current time is between start and end.
recurrence Recurrence metadata for source appointments and generated occurrences.

extras.recurrence contains:

Field Description
isRecurring Whether this appointment has a recurrence rule.
isOccurrence Whether this item is a generated occurrence.
recurringId Source appointment ID for generated occurrences.
occurrenceId Current rendered appointment ID.
occurrenceDate Date represented by this occurrence.
occurrenceIndex Zero-based occurrence index when available.
frequency Normalized frequency value.
interval Normalized interval value.

extras.hourSlotRules and drag dragExtras.hourSlotRules contain:

Field Description
canWork false for blocked ranges and outside exclusive ranges, otherwise true.
mode Matching mode: exclusive, preferred, blocked, highlight, or null.
reason Availability reason: available, blocked, exclusive, outsideExclusive, preferred, or highlighted.
range The matching hourSlots.rules object, or null.
inRange Whether the appointment is fully contained in the matching range.
isBlocked Whether the range blocks work.
isPreferred Whether the range marks preferred work time.
isExclusive Whether exclusive mode affects this appointment.

displayDates[] entries contain:

Field Description
date Display date.
day Weekday index.
times.start Visible start time for that day.
times.end Visible end time for that day.
visibleInWeek Whether this date is visible in week/4day view.
visibleInMonth Whether this date is visible in month view.

Year-view summary objects get a smaller extras object with colors, isToday, and isNow.

Colors

Supported color inputs:

  • Bootstrap theme names or class combinations, e.g. primary, danger opacity-75 gradient
  • Hex colors, e.g. #ff5733
  • RGB/RGBA values
  • CSS variables, e.g. var(--bs-primary)
  • Named CSS colors, e.g. steelblue

Use the public color helper:

const colors = $.bsCalendar.utils.getColors('#ff5733', 'primary');

// {
//   origin: '#ff5733',
//   backgroundColor: '#ff5733',
//   backgroundImage: 'none',
//   color: '#000000' or '#FFFFFF',
//   hex: '#ff5733'
// }

Holidays

holidays uses the OpenHolidays API. If country or language is missing, bs-calendar derives it from locale.

$('#calendar').bsCalendar({
    holidays: {
        country: 'DE',
        federalState: 'BE',
        language: 'DE'
    }
});
Key Type Default Description
country string or null locale country ISO 3166-1 alpha-2 country code.
federalState string or null null Subdivision/state code. Required for school holidays.
language string or null locale language ISO 639-1 language code.

If url is null, holidays can still be loaded and rendered.

Localization and Translations

The locale option has two responsibilities:

  • It controls date/time formatting through Intl.DateTimeFormat.
  • Its language part selects the built-in translation object. For example, de-DE, de-AT, and de_CH all use the de translations after locale normalization.

If no matching language exists, bs-calendar falls back to English.

Built-In Languages

Code Language
ar Arabic
he Hebrew
zh Chinese Simplified
en English
de German
es Spanish
fr French
it Italian
pt Portuguese
nl Dutch
pl Polish
ru Russian
uk Ukrainian
tr Turkish
ja Japanese
ko Korean
hi Hindi
id Indonesian
vi Vietnamese
th Thai
cs Czech
sv Swedish
da Danish
no Norwegian
fi Finnish
ro Romanian
el Greek

Translation Keys

All built-in translation objects currently use these keys:

Key English default Used for
today "Today" Today toolbar button.
day "Day" Day view label.
4day "4 Days" 4-day view label.
week "Week" Week view label.
month "Month" Month view label.
year "Year" Year view label.
agenda "Agenda" Agenda list view label.
allDay "All day" All-day appointment label.
search "Type and press Enter" Search input placeholder.
searchNoResult "No appointment found" Empty search result message.
tasks "Tasks" Task sidebar toggle and task badge label.
taskPriorityHigh "High" High-priority task badge.
taskPriorityNormal "Medium" Normal-priority task badge.
taskPriorityLow "Low" Low-priority task badge.
duplicate "Duplicate" Duplicate action in the info-window dropdown.

Selecting A Locale

$('#calendar').bsCalendar({
    locale: 'de-DE'
});

Underscores are normalized, so de_DE is treated as de-DE.

Overriding Strings Per Instance

You can override individual strings without redefining the whole language. Custom translations are merged with the selected built-in language.

$('#calendar').bsCalendar({
    locale: 'en-GB',
    translations: {
        today: 'Now',
        allDay: 'Full day',
        search: 'Find appointments...',
        taskPriorityNormal: 'Normal',
        duplicate: 'Copy'
    }
});

Registering Or Replacing A Language

Use $.bsCalendar.addTranslation(locale, translation) before initialization. Only the language code before the hyphen is used as the registry key. For example, de-CH registers or replaces de, not a separate Swiss-German variant.

$.bsCalendar.addTranslation('eo', {
    today: 'Hodiau',
    day: 'Tago',
    '4day': '4 Tagoj',
    week: 'Semajno',
    month: 'Monato',
    year: 'Jaro',
    agenda: 'Agendo',
    allDay: 'Tuttaga',
    search: 'Tajpu kaj premu Enter',
    searchNoResult: 'Neniu rendevuo trovita',
    tasks: 'Taskoj',
    taskPriorityHigh: 'Alta',
    taskPriorityNormal: 'Normala',
    taskPriorityLow: 'Malalta',
    duplicate: 'Duobligi'
});

$('#calendar').bsCalendar({
    locale: 'eo'
});

For regional wording differences, keep the regional locale for date formatting and override strings per instance:

$('#calendar').bsCalendar({
    locale: 'de-CH',
    translations: {
        allDay: 'Ganztägig',
        taskPriorityNormal: 'Normal',
        duplicate: 'Kopieren'
    }
});

Reading Translations Programmatically

These helpers use the language key. If you pass a regional locale like de-DE, only de is used internally.

const de = $.bsCalendar.getTranslations('de');
const today = $.bsCalendar.getTranslation('de', 'today');
const fallback = $.bsCalendar.getTranslation('xx', 'duplicate'); // English fallback

Changing Locale At Runtime

Use setLocale when you only want to change the locale. Use updateOptions when you also want to override translation keys at the same time.

$('#calendar').bsCalendar('setLocale', 'es-ES');

$('#calendar').bsCalendar('updateOptions', {
    locale: 'fr-FR',
    translations: {
        duplicate: 'Copier'
    }
});

Runtime locale changes are ignored while the calendar is in search mode.

Utilities

Global API:

$.bsCalendar.version;
$.bsCalendar.about;
$.bsCalendar.possibleViews;
$.bsCalendar.setDefaults({locale: 'de-DE'});
$.bsCalendar.getDefaults();
$.bsCalendar.addTranslation('es', {today: 'Hoy'});
$.bsCalendar.getTranslations('de');
$.bsCalendar.getTranslation('de', 'today');

Appointment and date helpers:

const hourNumber = $.bsCalendar.utils.parseTimeToDecimal('08:30'); // output -> 8.5
const appointments = $.bsCalendar.utils.convertIcsToAppointments(icsString);
const date = $.bsCalendar.utils.parseDateInput('2026-05-08 10:00:00');
const normalized = $.bsCalendar.utils.normalizeDateTime('2026-05-08 10:00');
const time = $.bsCalendar.utils.formatTime(date);
const dateString = $.bsCalendar.utils.formatDateToDateString(date);
const localizedDate = $.bsCalendar.utils.formatDateByLocale(date, 'de-DE');
const week = $.bsCalendar.utils.getCalendarWeek(date);
const weekdays = $.bsCalendar.utils.getShortWeekDayNames('de-DE', false);
const sameDay = $.bsCalendar.utils.datesAreEqual(new Date(), date);
const label = $.bsCalendar.utils.getAppointmentTimespanBeautify(extras, true);

Lower-level utility helpers:

const computed = $.bsCalendar.utils.computeColor('primary');
const styles = $.bsCalendar.utils.getComputedStyles('danger opacity-75 gradient');
const direct = $.bsCalendar.utils.isDirectColorValid('#ff5733');
const resolved = $.bsCalendar.utils.resolveColor('steelblue');
const dark = $.bsCalendar.utils.isDarkColor('#000000');
const hex = $.bsCalendar.utils.toHex('rgb(255, 87, 51)');
const colors = $.bsCalendar.utils.getColors('primary', 'secondary');
const id = $.bsCalendar.utils.generateRandomString(8);
const localeParts = $.bsCalendar.utils.getLanguageAndCountry('de-DE');
const empty = $.bsCalendar.utils.isValueEmpty('');
const namedHex = $.bsCalendar.utils.colorNameToHex.steelblue;

OpenHolidays helpers:

$.bsCalendar.utils.openHolidayApi.getCountries('DE');
$.bsCalendar.utils.openHolidayApi.getLanguages('DE');
$.bsCalendar.utils.openHolidayApi.getSubdivisions('DE', 'DE');
$.bsCalendar.utils.openHolidayApi.getSchoolHolidays('DE', 'BE', '2026-01-01', '2026-12-31');
$.bsCalendar.utils.openHolidayApi.getPublicHolidays('DE', 'BE', 'DE', '2026-01-01', '2026-12-31');

Repository Notes

Key files:

.
├── README.md
├── changelog.md
├── composer.json
├── dist/
│   ├── bs-calendar.js
│   └── bs-calendar.min.js
└── demo/
    ├── index.html
    └── img/

Development notes:

  • No npm build is required.
  • dist/bs-calendar.js is the unminified browser source.
  • dist/bs-calendar.min.js should be regenerated after changes to dist/bs-calendar.js.
  • The demo expects Composer dependencies in vendor/.
  • No automated test suite is currently included.

Changelog and support:

Completeness Check

This README is intended to cover the public surface of version 2.3.6:

  • All DEFAULTS options from dist/bs-calendar.js
  • All public plugin methods in the method switch
  • All jQuery events emitted through trigger() and matching on* callback options
  • Appointment object fields, including task fields and generated id behavior
  • url value types, request data, and response contracts
  • Formatter signatures
  • extras fields used by callbacks and formatters
  • Global $.bsCalendar API and utility helpers
  • Localization and custom translation handling