Back to Blog
A calendar and laptop on a desk
Development
7 min read

JavaScript Calendar Scheduler: Choosing and Building One

By SimpleCalendarJS Team

SimpleCalendarJS~14 KB gzipped · Zero dependencies · Any framework
Short answer

A JavaScript calendar scheduler is a time-grid calendar where users drag, resize and create events. If you schedule people or rooms side by side in rows, pick a resource-timeline library such as EventCalendar, FullCalendar Premium, Bryntum or DHTMLX PRO. For a single shared calendar, a lightweight library plus your own conflict checks is enough.

Searches for a "JavaScript calendar scheduler" usually come from a developer whose app has grown past displaying events. Now users need to drag a booking to another slot, stretch it by 30 minutes, click an empty hour to create something, and never double-book a room. This guide sorts out which kind of scheduler you actually need, compares the main libraries by license and price as of November 2026, and then builds a working drag-and-drop scheduler with conflict checks in plain JavaScript.

What a JavaScript Calendar Scheduler Has to Do

A calendar shows events. A scheduler lets people change them. Before you compare libraries, list which of these your app needs, because each one narrows the field:

CapabilityWhat it meansWho needs it
Time-grid viewsWeek and day views with hour rowsAlmost every scheduler
Drag & drop, resizeMove an event or change its length with mouse or touchBookings, shifts, planning tools
Click-to-createClick an empty slot and open a formAny app where users add events
Conflict rulesReject overlapping bookingsRooms, appointments, equipment
Recurring events"Every Monday at 9:00"Classes, standups, rotations
Time zonesDisplay in a zone other than the browser'sDistributed teams, travel
Resource rows/columnsOne lane per person, room or machineStaff rostering, fleet, manufacturing

The first four are the scheduler basics. The last three split the market. Resource views are what decide your library choice (and often your budget), so start there.

Calendar Scheduler vs Resource Scheduler

Picture your main screen. If it shows one shared calendar (a clinic's appointments, a team's meetings, a club's fixtures), a standard week and day calendar with drag and drop does the job. You can colour-code events by person or room and filter by them.

If it shows many parallel lanes (twelve nurses as rows across a week, or five meeting rooms as columns for one day), you need a resource view: a resource time grid (columns per resource) or a resource timeline (rows per resource, time running horizontally). These are much harder to build and are often the part of a library you pay for.

Don't force a resource layout onto a plain calendar with CSS hacks. If lanes are the core of the product, pick a library that ships them. If they aren't, don't pay for them.

Need a calendar in your app?

SimpleCalendarJS: ~14 KB gzipped, zero dependencies, works in any framework. Free for personal use.

JavaScript Calendar Scheduler Libraries Compared

Licensing and pricing below come from each vendor's own pages as of November 2026. Prices change, so check the linked sources before you budget.

LibraryLicenseResource / timeline viewsCommercial cost for scheduler features
FullCalendar (v7.1.5)Standard: MIT. Premium: commercialPremium onlyPremium from $480 (1–10 developers)
EventCalendar (vkurko/calendar)MITYes: resource time grid and resource timeline (day to year)Free
DHTMLX SchedulerStandard: GPL v2. PRO: commercialTimeline and Units views are PRO onlyPRO from $599 (Individual: 1 developer, 1 project)
Bryntum SchedulerCommunity: MIT. Core / Enterprise: commercial EULResource scheduling is the product's focusCore $850 per developer, Enterprise $1,300
Schedule-XCore: MIT. Premium: commercialResource view is Premium€479/year or €999 lifetime (2–3 developers, per project)
SimpleCalendarJSFree for personal, educational and open source use. Commercial use needs a licenseNoneSee pricing

Notes on each

  • FullCalendar. The most widely used option. Standard is MIT-licensed and free for commercial use. Timeline and vertical resource views, plus printer-friendly rendering, are in Premium. If you stop renewing, you keep the last version released before your license expired. If you already use it and it feels heavy, see why FullCalendar slows apps down.
  • EventCalendar. The strongest free option if you need resource lanes. It is MIT-licensed, offers resourceTimeGrid* and resourceTimeline* views, and its README puts the standalone bundle at about 35 kB Brotli-compressed with zero dependencies. The API is modelled on FullCalendar's.
  • DHTMLX Scheduler. Feature-rich, with recurring events, an agenda view, a map view and an export module for PDF, PNG, Excel and iCal. The Standard edition is GPL v2, so it only fits projects whose code is GPL-compatible. Timeline, Units (multiple resources), Grid view, Week Agenda and multiple schedulers on one page require PRO.
  • Bryntum Scheduler. Enterprise-grade resource scheduling. The store page lists a free Community tier under MIT. Recurring events, time zones and resource time ranges are in the $850-per-developer Core tier, and PDF export and nested events are in Enterprise. Redistribution in SaaS needs an OEM quote.
  • Schedule-X. A modern, MIT-licensed calendar core. Its resource view, drag-to-create, event modal and Gantt are paid Premium components, licensed per project.
  • SimpleCalendarJS. Month, week, day and list views with drag & drop and resize, zero dependencies, about 14 KB gzipped (about 19 KB with CSS). No resource views, no recurrence engine and no timezone option. More on where it fits below.

For a broader size comparison, including non-scheduler calendars, see the best JavaScript calendar libraries compared by bundle size.

Building a JavaScript Calendar Scheduler With SimpleCalendarJS

This example covers the single-calendar case: a week view where users drag and resize bookings, click an empty slot to create one, and click a booking to delete it. Overlaps are rejected, and a weekly standup is expanded by hand because the library has no RRULE support.

The key pattern: your data store is the source of truth. The calendar reads from it through fetchEvents. After any change, you update the store and call refresh(). To reject a drop, leave the store unchanged and call refresh(), and the event snaps back.

1. Markup

Give the container an inline height so the week view gets a fixed layout. The create form uses a native dialog element. A form with method="dialog" closes the dialog and sets its returnValue to the value of the button that was clicked.

<div id="calendar" style="height: 700px"></div> <dialog id="slot-dialog"> <form method="dialog"> <p><label>Title <input name="title" required></label></p> <p> <label>Duration <select name="minutes"> <option value="30">30 min</option> <option value="60" selected>1 hour</option> <option value="90">90 min</option> </select> </label> </p> <button value="cancel" formnovalidate>Cancel</button> <button value="save">Save</button> </form> </dialog> <script type="module" src="./scheduler.js"></script>

2. The scheduler

// scheduler.js (bundled with Vite or similar) import SimpleCalendarJs from 'simple-calendar-js'; import 'simple-calendar-js/dist/simple-calendar-js.min.css'; // --- Data store (replace with your API) --- let events = [ { id: 1, title: 'Client call', start: new Date(2026, 10, 16, 10, 0), end: new Date(2026, 10, 16, 11, 0), color: '#4f46e5' }, { id: 2, title: 'Design review', start: new Date(2026, 10, 17, 14, 0), end: new Date(2026, 10, 17, 15, 30), color: '#0891b2' }, ]; let nextId = 3; // A weekly recurring block, expanded manually (no RRULE support) const standup = { title: 'Team standup', weekday: 1, hour: 9, minute: 0, minutes: 15, color: '#64748b' }; function expandWeekly(rule, rangeStart, rangeEnd) { const out = []; const day = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), rangeStart.getDate()); while (day < rangeEnd) { if (day.getDay() === rule.weekday) { const start = new Date(day.getFullYear(), day.getMonth(), day.getDate(), rule.hour, rule.minute); const end = new Date(start.getTime() + rule.minutes * 60000); out.push({ id: `standup-${start.getTime()}`, title: rule.title, start, end, color: rule.color, recurring: true }); } day.setDate(day.getDate() + 1); } return out; } const overlaps = (aStart, aEnd, bStart, bEnd) => aStart < bEnd && bStart < aEnd; function eventsInRange(start, end) { return [ ...events.filter((e) => overlaps(e.start, e.end, start, end)), ...expandWeekly(standup, start, end), ]; } function hasConflict(start, end, ignoreId = null) { return eventsInRange(start, end).some( (e) => e.id !== ignoreId && overlaps(start, end, e.start, e.end) ); } // Stub: swap for fetch('/api/bookings', { method: 'POST', ... }) etc. async function persist(action, payload) { console.log('persist', action, payload); } // --- Calendar --- const calendar = new SimpleCalendarJs('#calendar', { defaultView: 'week', defaultDate: new Date(2026, 10, 16), enabledViews: ['month', 'week', 'day', 'list'], weekStartsOn: 1, use24Hour: true, enableDragDrop: true, enableResize: true, fetchEvents: async (start, end) => eventsInRange(start, end), onSlotClick: (date) => openCreateDialog(date), onEventClick: async (event) => { if (event.recurring) { alert('Recurring blocks are managed in settings.'); return; } if (!confirm(`Delete "${event.title}"?`)) return; events = events.filter((e) => e.id !== event.id); await persist('delete', { id: event.id }); calendar.refresh(); }, // Called after a drag (move) or a resize onEventDrop: async (event, oldStart, oldEnd, newStart, newEnd) => { const stored = events.find((e) => e.id === event.id); if (!stored) { alert('Recurring blocks cannot be moved.'); calendar.refresh(); // store unchanged, so the event snaps back return; } if (hasConflict(newStart, newEnd, event.id)) { alert('That time overlaps another booking.'); calendar.refresh(); return; } stored.start = newStart; stored.end = newEnd; try { await persist('update', { id: stored.id, start: newStart, end: newEnd }); } catch (err) { stored.start = oldStart; // roll back if the server refuses stored.end = oldEnd; alert('Could not save the change.'); } calendar.refresh(); }, }); // --- Click-to-create dialog --- const dialog = document.getElementById('slot-dialog'); const form = dialog.querySelector('form'); let pendingStart = null; function openCreateDialog(date) { pendingStart = new Date(date); // A click in month view gives a whole day; default to 09:00 if (pendingStart.getHours() === 0 && pendingStart.getMinutes() === 0) { pendingStart.setHours(9); } form.reset(); dialog.returnValue = ''; dialog.showModal(); } dialog.addEventListener('close', async () => { if (dialog.returnValue !== 'save' || !pendingStart) return; const data = new FormData(form); const start = pendingStart; const end = new Date(start.getTime() + Number(data.get('minutes')) * 60000); if (hasConflict(start, end)) { alert('That time overlaps another booking.'); return; } const booking = { id: nextId++, title: data.get('title'), start, end, color: '#16a34a' }; events.push(booking); await persist('create', booking); calendar.refresh(); });

A few details worth noticing:

  1. Every event has an id. Drag & drop needs one, and your callbacks use it to find the record in your store.
  2. Extra fields survive. The recurring: true flag isn't a calendar field, but it's passed back in onEventClick and onEventDrop. You can use the same trick for roomId, customerId or a URL.
  3. Rejecting a change is just a refresh(). The calendar re-reads your unchanged store, so you never need a separate revert API.
  4. Dates are built with new Date(y, m, d, h, min). A date-only string like '2026-11-16' is parsed as UTC midnight and can land on the previous day west of UTC.

3. Loading from a real API

fetchEvents receives the visible range, so the server only returns what's on screen. Results are cached per month grid, and refresh() clears that cache.

fetchEvents: async (start, end) => { const params = new URLSearchParams({ start: start.toISOString(), end: end.toISOString() }); const res = await fetch(`/api/bookings?${params}`); if (!res.ok) return []; const rows = await res.json(); return rows.map((r) => ({ id: r.id, title: r.title, start: new Date(r.start), // ISO string with offset, e.g. "2026-11-16T10:00:00Z" end: new Date(r.end), color: r.color, roomId: r.room_id, })); },

For the full week-view setup (hour heights, the current-time line, styling), see the JavaScript weekly calendar guide. For a scheduler with staff and roles, the shift scheduling calendar tutorial goes further.

Scheduling Logic the Library Won't Do for You

Whichever library you pick, a few rules belong in your code, not the UI.

Enforce conflicts on the server

The browser check above gives instant feedback, but two users can grab the same slot within the same second. The server has to have the final say. PostgreSQL handles this well with an exclusion constraint on a range column. The official docs show this pattern for rooms, using the btree_gist extension:

CREATE EXTENSION btree_gist; CREATE TABLE room_reservation ( room text, during tsrange, EXCLUDE USING GIST (room WITH =, during WITH &&) );

An overlapping insert for the same room fails with a constraint error. Catch it, return a 409, and the onEventDrop code above rolls the change back.

Expand recurrence where the data lives

If the calendar has no recurrence engine, expand rules into instances for the requested range, as expandWeekly does. In production, do it on the server, so conflict checks and reports see the same instances. Store exceptions ("skip 24 December") alongside the rule.

Decide your time zone policy early

Store UTC and send ISO strings with an offset. A browser calendar shows the viewer's local time by default. If users need to see a location's local time regardless of where they are, convert on the server or before returning events.

Keep the form yours

Most libraries, SimpleCalendarJS included, give you the click and leave the form to you. That's a feature: your booking form can validate against your own business rules (opening hours, minimum notice, customer lookup). The booking system tutorial shows a fuller version.

Where SimpleCalendarJS Fits

SimpleCalendarJS is a good fit when your scheduler is one calendar with time-grid views: appointments, team meetings, classes, content slots, equipment bookings shown by colour. You get month, week, day and list views, drag & drop and resize with mouse and touch, slot and event click callbacks, 12/24-hour time, a configurable week start, locale-aware dates through Intl.DateTimeFormat, and theming through CSS custom properties. It has zero dependencies and is about 14 KB gzipped, and the same core class works in vanilla JS, React, Vue, Svelte or Angular.

Be clear about what it doesn't do. It has no resource or timeline views, so a roster of 20 staff in rows needs EventCalendar (free, MIT), FullCalendar Premium, Bryntum or DHTMLX PRO instead. It has no RRULE support and no timezone option, so you expand recurrence and convert zones yourself, as shown above. It has no built-in event forms, iCal export or Google Calendar sync, and no TypeScript definitions.

On licensing, it's free for personal, educational and open source projects. Commercial use (client sites, SaaS, internal company tools) needs a paid license per project. See pricing. You can try the drag & drop behaviour in the sandbox and check every option in the docs.

Frequently Asked Questions

Is FullCalendar free to use as a scheduler?

FullCalendar Standard is MIT-licensed and free, including for commercial use, and covers month, week, day and list views. As of November 2026, its timeline and resource views are in FullCalendar Premium, which starts at $480 for 1–10 developers. Renewing before your license expires costs 50% of the base price.

How do I prevent double bookings in a JavaScript scheduler?

Check for overlaps in the browser so users get instant feedback, but enforce the rule on the server too, because two users can book the same slot at the same moment. In PostgreSQL, an exclusion constraint on a tsrange column with the && operator rejects overlapping rows. Add btree_gist to scope that rule to each room or person.

How do I show recurring events in a calendar that has no RRULE support?

Store the rule (for example, every Monday at 09:00 for 15 minutes) and expand it into individual events for the requested date range before you return them. With SimpleCalendarJS, do this inside fetchEvents(start, end), which gets the visible range. Give each generated instance a stable, unique id.

How should a JavaScript scheduler handle time zones?

Store times in UTC, or as ISO strings with an offset, and send them to the browser in that form. new Date('2026-11-16T10:00:00Z') turns into the viewer's local time. SimpleCalendarJS has no timezone option and shows dates in the browser's local time, so convert to another zone yourself before returning events.

Can I use a vanilla JavaScript scheduler inside React or Vue?

Yes. Create the calendar in the framework's lifecycle hook (useRef plus useEffect in React, onMounted in Vue) and keep the instance in a ref so you can call refresh() later. Call destroy() in the effect cleanup or in onBeforeUnmount. In SSR frameworks, create it on the client only.

Should I build a calendar scheduler from scratch without a library?

Only if your needs are very narrow. A usable scheduler needs a time grid, side-by-side layout for overlapping events, drag and resize with mouse and touch, locale-aware formatting and keyboard-friendly markup. That's weeks of edge cases. A library handles the UI, and you keep the business rules.

Add a calendar to your app today

Free for personal projects. $49/year or $199 lifetime per commercial project.

Keep Reading