How to Build a Meeting Scheduler with Vanilla JS (No Framework Required)
By SimpleCalendarJS Team
Every team app eventually needs a meeting scheduler. Users pick a time, invite attendees, and the system checks that nobody is double-booked. Simple in theory — but search for "meeting scheduler JavaScript" and you'll find commercial widgets quoting enterprise pricing, full-stack platforms like Cal.com that want to own your entire scheduling flow, and calendar libraries that need three framework adapters before they render a week view. You don't need any of that. A vanilla JavaScript meeting scheduler is three pieces: a calendar UI, time slot logic, and overlap detection. Here's how to build one.
Meeting scheduler vs. booking system — different problems
Before writing code, understand what makes a meeting scheduler different from a booking system. A booking system handles one-to-one reservations: one customer picks one provider's available slot. A meeting scheduler handles many-to-many coordination: you're finding a time window where multiple attendees are all free.
This distinction matters because the overlap detection logic is fundamentally different:
| Feature | Booking System | Meeting Scheduler |
|---|---|---|
| Participants | 1 customer → 1 provider | N attendees, all must be free |
| Conflict check | One calendar | Multiple calendars |
| Slot source | Provider's availability | Intersection of all attendees' free time |
| Typical use | Clinics, salons, rentals | Team standups, client calls, interviews |
The calendar UI component is the same in both cases. The logic layer is where they diverge.
The commercial landscape
If you've researched meeting scheduler libraries, you've seen these options:
| Solution | Pricing | Bundle Size | Overlap Detection |
|---|---|---|---|
| Bryntum Scheduler | Custom (enterprise) | ~200 KB+ | Built-in |
| Syncfusion Scheduler | $995+/developer | ~150 KB+ | actionBegin event |
| Mobiscroll | $395+/license | ~80 KB+ | eventOverlap option |
| DHTMLX Scheduler | $599+ (PRO) | ~100 KB+ | Server-side |
| Schedule-X | Free (open source) | ~40 KB+ | Manual |
| SimpleCalendarJS | Free | ~14 KB | Manual (you control it) |
Bryntum and Syncfusion are enterprise-grade. They bundle conflict detection, resource views, and drag-and-drop — but their pricing reflects that. Mobiscroll has a clean eventOverlap option that denies dragging events onto occupied slots, but licenses start at $395 per project with 80% annual renewal fees.
On the free side, Schedule-X (~94K weekly npm downloads) is a modern FullCalendar alternative with good framework support. But its core package plus theme plus temporal polyfill adds up. Cal.com (41K+ GitHub stars) is a full scheduling platform — not a library you embed. It's a Next.js app with its own database, authentication, and UI. Great if you want to replace Calendly; overkill if you need a scheduler inside your existing app.
The lightweight path: use SimpleCalendarJS for the calendar UI and build the meeting logic yourself. You own every line of scheduling code, and the calendar stays swappable.
Building the scheduler with SimpleCalendarJS
Step 1: Set up the calendar
npm install simple-calendar-js
import SimpleCalendarJs from 'simple-calendar-js'; import 'simple-calendar-js/dist/simple-calendar-js.min.css'; const calendar = new SimpleCalendarJs('#scheduler', { defaultView: 'week', locale: 'en-US', enabledViews: ['week', 'day'], // Load existing meetings from your API fetchEvents: async (start, end) => { const res = await fetch( `/api/meetings?from=${start.toISOString()}&to=${end.toISOString()}` ); const meetings = await res.json(); return meetings.map((m) => ({ id: m.id, title: m.title, start: new Date(m.startTime), end: new Date(m.endTime), color: m.confirmed ? '#10b981' : '#f59e0b', })); }, // Open meeting form when user clicks an empty slot onSlotClick: (date) => { openMeetingForm({ selectedDate: date }); }, // Show meeting details when user clicks an existing meeting onEventClick: (event) => { showMeetingDetails(event.id); }, });
This renders a navigable week/day calendar showing existing meetings colour-coded by status. Clicking an empty slot opens your meeting creation form. ~14 KB gzipped, zero dependencies, zero framework lock-in.
Step 2: Build the attendee picker and time slot selector
When a user clicks a slot, show a form where they can add attendees and pick a duration:
function openMeetingForm({ selectedDate }) { const form = document.getElementById('meeting-form'); const dateInput = form.querySelector('[name="date"]'); const timeInput = form.querySelector('[name="time"]'); // Pre-fill with the clicked slot's date and time dateInput.value = selectedDate.toISOString().split('T')[0]; timeInput.value = selectedDate.toTimeString().slice(0, 5); document.getElementById('meeting-modal').showModal(); }
Step 3: Find overlapping free time across attendees
This is the core algorithm that separates a meeting scheduler from a simple booking form. Given multiple attendees' busy times, find windows where everyone is free:
function findAvailableSlots(attendeeBusyTimes, date, slotDuration) { const dayStart = new Date(date); dayStart.setHours(9, 0, 0, 0); // Working hours: 9 AM const dayEnd = new Date(date); dayEnd.setHours(17, 0, 0, 0); // to 5 PM // Merge all attendees' busy times into one sorted list const allBusy = attendeeBusyTimes .flat() .map((b) => ({ start: new Date(b.start).getTime(), end: new Date(b.end).getTime(), })) .sort((a, b) => a.start - b.start); // Merge overlapping busy intervals const merged = []; for (const interval of allBusy) { const last = merged[merged.length - 1]; if (last && interval.start <= last.end) { last.end = Math.max(last.end, interval.end); } else { merged.push({ ...interval }); } } // Find gaps between busy intervals that fit the meeting duration const slots = []; let cursor = dayStart.getTime(); const durationMs = slotDuration * 60 * 1000; for (const busy of merged) { if (busy.start > cursor && busy.start - cursor >= durationMs) { slots.push({ start: new Date(cursor), end: new Date(cursor + durationMs), }); } cursor = Math.max(cursor, busy.end); } // Check the gap after the last busy block if (dayEnd.getTime() - cursor >= durationMs) { slots.push({ start: new Date(cursor), end: new Date(cursor + durationMs), }); } return slots; }
The algorithm works in three steps: flatten all attendees' busy times into one list, merge overlapping intervals, then scan the gaps for windows that fit the meeting duration. This is the classic merge intervals approach — O(n log n) time, battle-tested in production systems.
Step 4: Submit the meeting with overlap prevention
async function submitMeeting(formData) { const res = await fetch('/api/meetings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: formData.title, startTime: formData.startTime, endTime: formData.endTime, attendees: formData.attendeeIds, }), }); if (res.status === 409) { // A conflict was detected server-side showError('One or more attendees have a conflict at this time.'); return; } const meeting = await res.json(); closeModal(); // Refresh calendar to show the new meeting calendar.refetchEvents(); }
The 409 Conflict response handles race conditions — two users trying to book overlapping meetings for the same attendee at the same time. The server is the single source of truth.
Server-side overlap detection: the only check that matters
Client-side overlap detection improves UX by showing available slots before submission. But it is not a substitute for server-side validation. Between the time a user loads available slots and clicks "Create Meeting," another meeting could be booked.
The server must perform an atomic check:
-- Check if any attendee has a conflict in the proposed time range SELECT attendee_id FROM meeting_attendees JOIN meetings ON meetings.id = meeting_attendees.meeting_id WHERE attendee_id = ANY(:attendee_ids) AND start_time < :proposed_end AND end_time > :proposed_start;
If this query returns any rows, at least one attendee is already booked. Return a 409. This single query covers all edge cases — partially overlapping meetings, meetings that start exactly when another ends, and fully contained meetings.
Adding multi-attendee availability display
Show each attendee's availability on the calendar before the user picks a time. Fetch busy times for all selected attendees and render them as background events:
async function showAttendeeAvailability(attendeeIds) { const busyTimes = await Promise.all( attendeeIds.map(async (id) => { const res = await fetch(`/api/users/${id}/busy`); return res.json(); }) ); // Render each attendee's busy times as semi-transparent overlays const colors = ['#ef444440', '#3b82f640', '#8b5cf640', '#f59e0b40']; const overlayEvents = busyTimes.flatMap((times, i) => times.map((t) => ({ id: `busy-${attendeeIds[i]}-${t.start}`, title: `Busy: ${t.attendeeName}`, start: new Date(t.start), end: new Date(t.end), color: colors[i % colors.length], })) ); calendar.refetchEvents(); // Reload with busy overlays }
This gives users a visual heatmap of team availability. Slots with no overlays are free for everyone — pick those.
Feature comparison: platform vs. build-your-own
| Feature | Cal.com / Calendly | SimpleCalendarJS + Custom Logic |
|---|---|---|
| Calendar UI | Bundled (their design) | ~14 KB, your design |
| Multi-attendee scheduling | Built-in | Custom (you control the UX) |
| Overlap detection | Automatic | Merge intervals algorithm |
| Email invites | Built-in | Your email service (SendGrid, Resend, etc.) |
| Branding | Limited on free tiers | Fully custom |
| Self-hosted data | Cal.com only (AGPL) | Your infrastructure, your rules |
| Embeddable in existing apps | Iframe/embed | Native integration |
| Total cost | Free tier → $15+/user/mo | $0 (library is free) |
Full platforms win when you need standalone scheduling pages with their own URLs, user accounts, and calendar sync. If your meeting scheduler lives inside an existing app — a project management tool, a CRM, a team dashboard — building with a lightweight calendar library gives you complete control over the data flow and user experience.
Summary
- A meeting scheduler is fundamentally different from a booking system — it requires finding free time across multiple attendees, not just one provider's availability
- Commercial scheduler widgets (Bryntum, Syncfusion, Mobiscroll) bundle overlap detection but cost $395–$995+ per developer or project
- Full platforms like Cal.com and Calendly own the entire scheduling flow — overkill when you need a scheduler embedded in your existing app
- SimpleCalendarJS provides the calendar UI at ~14 KB gzipped with
onSlotClickandfetchEventscallbacks that wire directly into your scheduling logic - The merge intervals algorithm finds available slots across multiple calendars in
O(n log n)— flatten busy times, merge overlaps, scan the gaps - Server-side overlap detection is mandatory — use an atomic database query to prevent race conditions between concurrent booking attempts
Sources & Further Reading
Research & References
- FullCalendar v6 bundle size increase — GitHub #7029
- FullCalendar Pricing — fullcalendar.io
- Mobiscroll Scheduler — Prevent event overlap
- Syncfusion Meeting Room Calendar Tutorial — syncfusion.com
- Best JavaScript Scheduler in 2026 — RevoGrid
- Best Scheduling Libraries for Frontend Developers — DEV Community
- Top JavaScript Scheduler Libraries in 2026 — jqwidgets.com
- Cal.com — Open Source Calendly Alternative
- Algorithms in JS: Merging Meetings — Medium
- Schedule-X — Modern JavaScript Event Calendar
Image Credits
- Cover: People on a Meeting — Pexels
All images free to use under the Pexels License.
Frequently Asked Questions
Can I build a meeting scheduler without React or Vue?
Yes. A meeting scheduler is a calendar UI plus time slot logic plus overlap detection. Vanilla JavaScript handles all three. Libraries like SimpleCalendarJS (~14 KB, zero dependencies) provide the calendar views — month, week, day — and you wire the slot selection and conflict checks yourself. No framework required.
What JavaScript library is best for a meeting scheduler?
It depends on your budget and complexity. Commercial options like Bryntum Scheduler and Syncfusion start at custom/enterprise pricing. Schedule-X (~94K weekly npm downloads) is a modern open-source option. SimpleCalendarJS (~14 KB gzipped, free) provides calendar views with event callbacks you can wire to any scheduling logic.
How do I detect overlapping meetings in JavaScript?
Sort meetings by start time, then iterate through the list. Two meetings overlap when the start time of the second meeting is earlier than the end time of the first. For real-time overlap prevention, check new meetings against all existing meetings in the same time window before saving.
How do I prevent double-booking in a meeting scheduler?
Double-booking prevention must happen server-side. Use an atomic database query that checks for time range overlaps before inserting a new meeting. Client-side checks alone are vulnerable to race conditions when two users book the same slot simultaneously.
What is the difference between a meeting scheduler and a booking system?
A booking system typically handles one-to-one reservations — one customer books one provider's time slot. A meeting scheduler handles many-to-many coordination — multiple attendees need to find a shared free window across all their calendars. The overlap detection logic is different, but the calendar UI component is the same.
Do I need Cal.com or Calendly to build a meeting scheduler?
Not if you want control over the experience. Cal.com (41K+ GitHub stars) and Calendly are full scheduling platforms with their own UI, accounts, and hosting. If you need a meeting scheduler embedded in your own app with your own design and data model, a lightweight calendar library plus custom logic gives you full control at a fraction of the complexity.
Add a calendar to your app today
Free for personal projects. $49/year or $199 lifetime per commercial project.
