How to Build a Shift Scheduling Calendar in JavaScript
By SimpleCalendarJS Team
Every team that runs on shifts — hospitals, restaurants, warehouses, call centres, retail stores — needs a scheduling calendar. Managers drag employees into time slots, the system flags conflicts, and staff see their upcoming shifts at a glance. The obvious path is licensing an enterprise scheduler component, but those start at $480/developer/year and ship 100–200 KB of JavaScript you don't need. A shift scheduling calendar is simpler than it looks when you break it down.
What enterprise schedulers actually charge
Before writing any code, understand the landscape. Shift scheduling calendars fall into two categories: full-stack SaaS platforms (Deputy, When I Work, Homebase) and JavaScript UI components you embed in your own app. The SaaS route means recurring per-user fees, zero customisation, and your workforce data on someone else's servers. The component route gives you control — but the price tags are steep.
| Library | License Cost | Bundle Size | Resource Views | Framework Lock-in |
|---|---|---|---|---|
| FullCalendar Premium | $480/dev/yr | ~200 KB | Timeline, DayGrid | React/Vue/Angular adapters |
| Bryntum Scheduler | $2,040+ (3 devs) | ~150 KB+ | Timeline, histogram | React/Vue/Angular |
| DHTMLX Scheduler | $1,299+ (5 devs) | ~100 KB+ | Timeline, units | React/Vue/Angular |
| DayPilot Pro | $649+ (1 dev, 1 app) | ~80 KB+ | Scheduler, timeline | React/Vue/Angular |
| Mobiscroll | $395+/project | ~80 KB+ | Timeline | React/Vue/Angular/jQuery |
| SimpleCalendarJS | Free personal / $49/yr or $199 lifetime commercial | ~14 KB | Month, week, day | None — vanilla JS |
FullCalendar is the go-to for calendar UIs, but its free MIT core does not include resource views. Displaying employees as rows with shifts as bars requires the premium Scheduler plugin — that's a $480/developer/year subscription. The full bundle with resource timeline, resource day grid, and interaction plugins lands around 200 KB gzipped.
Bryntum Scheduler is purpose-built for resource scheduling with histogram summaries and multi-assignment support. It's the most feature-complete option — and the most expensive at $2,040 for three developers. That's a subscription, not a one-time purchase.
DHTMLX Scheduler handles large datasets well (benchmarked up to 100,000 events without lag), but the starting price of $1,299 for five developers puts it out of reach for small teams and side projects.
The pattern: enterprise schedulers charge enterprise prices for what is fundamentally a grid of time slots with events. If your shift calendar doesn't need Gantt-chart-level complexity — and most don't — you can build it with a lightweight calendar library and own the result.
Building the shift scheduling calendar
A shift scheduling calendar has four requirements: display shifts visually, assign employees to time slots, detect conflicts, and support recurring rosters. Here's how to build each one.
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 shiftCalendar = new SimpleCalendarJs('#shift-calendar', { defaultView: 'week', locale: 'en-US', enabledViews: ['week', 'month'], fetchEvents: async (start, end) => { const res = await fetch( `/api/shifts?from=${start.toISOString()}&to=${end.toISOString()}` ); const shifts = await res.json(); return shifts.map((shift) => ({ id: shift.id, title: `${shift.employeeName} — ${shift.role}`, start: new Date(shift.startTime), end: new Date(shift.endTime), color: shift.roleColor, })); }, onSlotClick: (date) => { openShiftAssignmentModal(date); }, onEventClick: (event) => { openShiftDetailModal(event); }, });
This renders a week-view calendar at ~14 KB gzipped with zero dependencies. Each shift appears as a colour-coded block — morning shifts in blue, afternoon in green, night in purple. Clicking an empty slot opens the assignment form; clicking an existing shift opens its details.
Step 2: Assign shifts with a form
When a manager clicks a time slot, present a form to assign an employee:
async function assignShift(date, formData) { const res = await fetch('/api/shifts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ employeeId: formData.employeeId, role: formData.role, startTime: combineDateAndTime(date, formData.startTime), endTime: combineDateAndTime(date, formData.endTime), notes: formData.notes, }), }); if (res.status === 409) { showError('This employee already has a shift during this time.'); return; } const shift = await res.json(); shiftCalendar.refetchEvents(); showSuccess(`Shift assigned to ${shift.employeeName}`); } function combineDateAndTime(date, timeStr) { const [hours, minutes] = timeStr.split(':').map(Number); const dt = new Date(date); dt.setHours(hours, minutes, 0, 0); return dt.toISOString(); }
The 409 Conflict response is critical. Two managers might assign the same employee to overlapping times — the server rejects the second request, and the UI shows a clear error instead of silently double-booking.
Step 3: Detect conflicts server-side
Conflict detection belongs on the server, not in the browser. Before inserting a shift, check for overlaps:
// Server-side (Node.js + SQL example) async function createShift(pool, shiftData) { const { employeeId, startTime, endTime, role, notes } = shiftData; const conflict = await pool.query( `SELECT id FROM shifts WHERE employee_id = $1 AND start_time < $2 AND end_time > $3`, [employeeId, endTime, startTime] ); if (conflict.rows.length > 0) { return { status: 409, error: 'Overlapping shift exists' }; } const result = await pool.query( `INSERT INTO shifts (employee_id, start_time, end_time, role, notes) VALUES ($1, $2, $3, $4, $5) RETURNING *`, [employeeId, startTime, endTime, role, notes] ); return { status: 201, shift: result.rows[0] }; }
The overlap query uses the standard interval intersection check: two time ranges overlap if one starts before the other ends and ends after the other starts. For production systems, wrap the check and insert in a transaction or use a database constraint to prevent race conditions.
Step 4: Support recurring shift templates
Most rosters follow a pattern — the same employees work the same shifts every week, with occasional swaps. Instead of creating 52 individual shifts per year, define templates:
const shiftTemplates = [ { id: 'morning-mon-wed-fri', employeeId: 'emp-001', role: 'Front Desk', days: [1, 3, 5], // Monday, Wednesday, Friday startTime: '09:00', endTime: '17:00', color: '#3b82f6', }, { id: 'evening-tue-thu', employeeId: 'emp-002', role: 'Support', days: [2, 4], // Tuesday, Thursday startTime: '17:00', endTime: '01:00', color: '#8b5cf6', }, ]; function expandTemplates(templates, rangeStart, rangeEnd) { const events = []; const current = new Date(rangeStart); while (current <= rangeEnd) { const dayOfWeek = current.getDay(); templates.forEach((template) => { if (template.days.includes(dayOfWeek)) { const [startH, startM] = template.startTime.split(':').map(Number); const [endH, endM] = template.endTime.split(':').map(Number); const start = new Date(current); start.setHours(startH, startM, 0, 0); const end = new Date(current); end.setHours(endH, endM, 0, 0); if (end <= start) end.setDate(end.getDate() + 1); // overnight shift events.push({ id: `${template.id}-${current.toISOString().split('T')[0]}`, title: `${template.role}`, start, end, color: template.color, templateId: template.id, employeeId: template.employeeId, }); } }); current.setDate(current.getDate() + 1); } return events; }
The expandTemplates function generates individual shift events from templates for any date range. Overnight shifts (ending after midnight) are handled by bumping the end date forward. On the server, store overrides — cancellations, swaps, time changes — as exceptions linked to the template ID.
Colour-coding shifts by role
A glance at the calendar should tell the manager who's covering what. Map roles to colours with CSS custom properties:
#shift-calendar { --uc-primary: #2563eb; --uc-bg: #ffffff; --uc-border: #e5e7eb; --uc-text: #111827; } .shift-morning { background: #dbeafe; border-left: 3px solid #3b82f6; } .shift-afternoon { background: #dcfce7; border-left: 3px solid #22c55e; } .shift-night { background: #ede9fe; border-left: 3px solid #8b5cf6; } .shift-conflict { background: #fee2e2; border-left: 3px solid #ef4444; } @media (prefers-color-scheme: dark) { #shift-calendar { --uc-primary: #60a5fa; --uc-bg: #1f2937; --uc-border: #374151; --uc-text: #f9fafb; } .shift-morning { background: #1e3a5f; } .shift-afternoon { background: #14532d; } .shift-night { background: #2e1065; } .shift-conflict { background: #7f1d1d; } }
Red highlights for conflicts make scheduling errors impossible to miss. The left border provides a secondary visual cue that works for colour-blind users.
Feature comparison: enterprise scheduler vs custom build
| Feature | FullCalendar Premium | Bryntum Scheduler | Custom + SimpleCalendarJS |
|---|---|---|---|
| Setup time | Hours | Hours | Half a day |
| Annual cost (1 dev) | $480/yr | $680+/yr | $49/yr or $199 lifetime |
| Bundle size | ~200 KB | ~150 KB+ | ~14 KB |
| Resource timeline view | Built-in | Built-in | Custom (week/month grid) |
| Recurring shift templates | Plugin | Built-in | Your logic |
| Conflict detection | Manual | Built-in | Your logic (server-side) |
| Drag-and-drop shift moves | Built-in | Built-in | Custom handler |
| Export to PDF/Excel | Plugin | Built-in | Build with jsPDF/SheetJS |
| Framework lock-in | React/Vue/Angular | React/Vue/Angular | None |
Enterprise schedulers win when you need a resource timeline view (employees as rows, hours as columns) out of the box and your budget supports $480–$2,040+ per year. The custom build wins when you need a clean shift calendar without the overhead — especially for small teams, internal tools, and applications where 14 KB matters more than a built-in Gantt chart.
Extending the calendar
Shift swap requests
Let employees request swaps through the calendar:
async function requestSwap(shiftId, targetEmployeeId) { const res = await fetch('/api/shifts/swap-request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ shiftId, targetEmployeeId }), }); if (res.ok) { showSuccess('Swap request sent. Waiting for approval.'); } }
Availability overlay
Before assigning a shift, show who's available. Fetch employee availability and render it as a background layer:
async function loadAvailability(date) { const res = await fetch(`/api/availability?date=${date.toISOString().split('T')[0]}`); const employees = await res.json(); return employees.map((emp) => ({ name: emp.name, available: emp.availableFrom && emp.availableTo, hours: emp.availableFrom ? `${emp.availableFrom} – ${emp.availableTo}` : 'Unavailable', })); }
Labour law compliance
Add validation rules for minimum rest periods between shifts:
function validateRestPeriod(employee, newShift, minRestHours = 11) { const prevShiftEnd = new Date(employee.lastShiftEnd); const newShiftStart = new Date(newShift.startTime); const restHours = (newShiftStart - prevShiftEnd) / (1000 * 60 * 60); if (restHours < minRestHours) { return { valid: false, message: `Only ${restHours.toFixed(1)}h rest. Minimum is ${minRestHours}h.`, }; } return { valid: true }; }
Many jurisdictions require 11 hours of rest between shifts (EU Working Time Directive) or 8 hours (US FLSA guidelines). Encoding these rules server-side prevents managers from accidentally creating illegal schedules.
Summary
- Enterprise shift scheduling components (FullCalendar Premium, Bryntum, DHTMLX) cost $480–$2,040+ per year and ship 100–200 KB of JavaScript — most of which you'll never use
- A custom shift scheduling calendar built with SimpleCalendarJS ships at ~14 KB with full control over assignment logic, conflict detection, and recurring rosters
- Conflict detection must happen server-side — check for overlapping time ranges in the database before inserting, and handle
409 Conflictin the UI - Recurring templates eliminate repetitive scheduling — define a pattern once, expand it to individual shifts per date range, and store exceptions for swaps and cancellations
- Colour-coding by role (morning/afternoon/night) gives managers an instant visual overview of coverage, with red highlights for conflicts
- Add compliance checks for minimum rest periods between shifts to avoid violating labour laws
Sources & Further Reading
Research & References
- Top JavaScript Scheduler Libraries in 2026 — jqwidgets.com
- Compare the Best React Scheduler Components for 2025-2026 — DHTMLX Blog
- Best JavaScript Scheduler in 2026 — Bryntum, DHTMLX & RevoGrid — rv-grid.com
- React FullCalendar vs Big Calendar — Bryntum Blog
- DayPilot Pro for JavaScript — Licensing and Pricing
- JavaScript Timeline Employee Shift Planning Example — Mobiscroll
- employee-scheduling — GitHub Topics
- FullCalendar Scheduler Premium Plugin — npm
Image Credits
- Cover: Colorful Sticky Notes Taped to Schedule on Whiteboard — Pexels
All images free to use under the Pexels License.
Frequently Asked Questions
What is the best JavaScript library for shift scheduling?
It depends on your complexity. Enterprise resource schedulers like Bryntum ($2,040+) and DHTMLX ($1,299+) offer built-in timeline and resource views. For most shift calendars — where you need a month or week view with colour-coded shifts and click-to-assign — SimpleCalendarJS (~14 KB) handles the UI at a fraction of the cost, and you keep full control of your scheduling logic.
Can I build a shift scheduling calendar without a framework?
Yes. SimpleCalendarJS is a vanilla JavaScript library with zero dependencies. It works in plain HTML pages as well as React, Vue, Angular, and Svelte apps. The scheduling logic (conflict detection, recurring shifts, availability rules) is standard JavaScript that runs anywhere.
How do I handle recurring shifts in JavaScript?
Define a shift template with a recurrence rule (e.g. every Monday and Wednesday, 09:00–17:00) and expand it into individual events on the server. When fetching events for a date range, generate occurrences from the template and return them as normal calendar events. Store overrides (swaps, cancellations) as exceptions linked to the template ID.
How do I prevent double-booking an employee for two overlapping shifts?
Conflict detection must happen server-side. Before inserting a shift assignment, query for any existing shifts that overlap the same time range for the same employee. Use an atomic database operation (a single INSERT with a WHERE NOT EXISTS clause or a unique constraint) to prevent race conditions when two managers assign the same slot simultaneously.
Is FullCalendar free for shift scheduling?
FullCalendar's core library is free and open source (MIT license), but it only supports basic calendar views. The Resource Timeline and Resource Day/Week views — which display employees as rows with shifts as horizontal bars — require the premium Scheduler plugin, starting at $480 per developer per year.
How much does enterprise shift scheduling software cost?
Dedicated JavaScript scheduler components range from $480/developer/year (FullCalendar Premium) to $2,040+ for three developers (Bryntum) or $1,299+ for five developers (DHTMLX). SaaS scheduling platforms like Deputy and When I Work charge $2–$5 per user per month. Building your own with SimpleCalendarJS costs $49/year or $199 lifetime for commercial use.
Add a calendar to your app today
Free for personal projects. $49/year or $199 lifetime per commercial project.
