webcito / bs-calendar
A jQuery-based Bootstrap calendar plugin with day/week/month/agenda/year views.
Package info
github.com/ThomasDev-de/bs-calendar
Language:JavaScript
pkg:composer/webcito/bs-calendar
Fund package maintenance!
Requires
Requires (Dev)
- roave/security-advisories: dev-latest
This package is auto-updated.
Last update: 2026-06-17 17:27:42 UTC
README
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.
![]() |
![]() |
![]() |
![]() |
Table of Contents
- Requirements
- Installation
- Quick Start
- Run the Demo
- Core Concepts
- Appointment Data
- Recurring Appointments
- Remote Data with
url - Add, Edit, and Delete Workflow
- Options
- Events and Callbacks
- Methods
- Formatters
- Extras Object
- Colors
- Holidays
- Localization and Translations
- Utilities
- Repository Notes
- Completeness Check
Requirements
- jQuery
^3 - Bootstrap
^5CSS 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
urlcontrols remote appointment loading. It can benull, a URL string, or a function returning a Promise.calendarsdefines sidebar filters. Active calendar IDs are always sent ascalendarIdsin remote requests.add.bs.calendar,edit.bs.calendar, anddelete.bs.calendarare intent events. They tell your application what the user wants; they do not save anything.addAppointment,editAppointment, anddeleteAppointmentmutate only the currently loaded browser-side appointment list. For backend-backed calendars, persist to your backend first or callrefreshafter saving.refreshreloads data fromurl.renderre-renders already loaded data without callingurl.yearview uses summary objects (date,total, optionalcontent), not full appointment objects.- Recurring appointments are normal appointment objects with a
recurrenceobject. They are expanded only for loaded appointment views. - Tasks are normal appointment objects with a
taskobject.
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, andagenda, yoururlreceivesfromDateandtoDate. Return recurring source appointments that can have at least one occurrence in that range. - Do not filter recurring source appointments only by
startdate. A weekly source from January can still produce a June occurrence. - A practical server-side overlap check is: source
start <= toDateand either nountiloruntil >= fromDate. If you usecount, your backend may need its own recurrence check for exact filtering. yearview uses summary rows (date,total, optionalcontent), so recurrence expansion is not applied there. Return already counted year summary data.- Search is controlled by your
urlresponse. 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, andicons.taskOverdue. - Clicking a task icon toggles
task.checkedlocally and firestask-status-changed.bs.calendar. - The global task sidebar control is shown when
showTasksistrue. - Task visibility state is sent to normal view and search requests as
showTasks. task.isOverdueis 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:
blockedwins whenever the requested time range overlaps a blocked range.exclusiveapplies next. If anyexclusiverule exists for the weekday, work is allowed only when the requested time range is fully contained in anexclusiverange.preferredallows work and setsisPreferred.highlightor an omittedmodeis visual only and does not block work.- 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, andde_CHall use thedetranslations 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.jsis the unminified browser source.dist/bs-calendar.min.jsshould be regenerated after changes todist/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
DEFAULTSoptions fromdist/bs-calendar.js - All public plugin methods in the method switch
- All jQuery events emitted through
trigger()and matchingon*callback options - Appointment object fields, including task fields and generated
idbehavior urlvalue types, request data, and response contracts- Formatter signatures
extrasfields used by callbacks and formatters- Global
$.bsCalendarAPI and utility helpers - Localization and custom translation handling



