react-smart-scheduler
v0.1.5
Published
A production-ready, open-source React scheduler/calendar component with Day, Week, and Month views — drag-and-drop, resize, and a clean controlled-component API.
Maintainers
Readme
📅 react-smart-scheduler
A production-ready, open-source React scheduler / calendar component
Day · Week · Month views · Drag & drop · Resize · TypeScript · Zero UI framework dependency
Live Demo → | GitHub → | npm →
❤️ Support the project
react-smart-scheduler is free and open-source (MIT). If it saves you development time, please consider supporting it — every contribution keeps the project alive and actively maintained.
| | | |---|---| | ☕ Buy Me a Coffee | One-time tip — any amount helps | | 🩷 GitHub Sponsors | Monthly support with perks |
⭐ Star the repo · 🐛 Report a bug · 🔁 Share with a teammate
✨ Features
| Feature | Status | |---|---| | Day / Week / Month views | ✅ | | Drag events to move (time + day) | ✅ | | Drag bottom edge to resize | ✅ | | Click empty slot → create event modal | ✅ | | Overlap / concurrent event layout | ✅ | | Current time indicator ("now" line) | ✅ | | Controlled component API | ✅ | | Accessible (ARIA roles, keyboard nav) | ✅ | | CSS custom-property theming | ✅ | | TypeScript — fully typed | ✅ | | Zero heavy UI framework deps | ✅ | | Source maps included | ✅ |
📦 Installation
npm install react-smart-scheduler
# or
yarn add react-smart-scheduler
# or
pnpm add react-smart-schedulerPeer dependencies — make sure these are already in your project:
npm install react react-dom
🚀 Quick start
// 1. Import the component and CSS
import { Scheduler, CalendarEvent, generateId } from 'react-smart-scheduler';
import 'react-smart-scheduler/dist/scheduler.css';
// 2. Manage your own events (controlled pattern)
import { useState } from 'react';
export default function App() {
const [events, setEvents] = useState<CalendarEvent[]>([]);
return (
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Scheduler
events={events}
view="week"
onEventAdd={(partial) =>
setEvents((prev) => [...prev, { ...partial, id: generateId() }])
}
onEventChange={(updated) =>
setEvents((prev) =>
prev.map((e) => (e.id === updated.id ? updated : e))
)
}
onEventDelete={(id) =>
setEvents((prev) => prev.filter((e) => e.id !== id))
}
/>
</div>
);
}🎨 Styling options
react-smart-scheduler ships with a zero-dependency default theme and two optional CSS adapters. All three are drop-in replacements for <Scheduler /> — just swap the import.
| Adapter | Import | Accent | Font |
|---|---|---|---|
| Default | import { Scheduler } | Blue #3b82f6 | System UI |
| Tailwind | import { TailwindScheduler } | Indigo #6366f1 | Inter |
| MUI | import { MuiScheduler } | MUI Blue #1976d2 | Roboto |
Default (built-in)
import { Scheduler } from 'react-smart-scheduler';
import 'react-smart-scheduler/dist/scheduler.css';
<Scheduler events={events} onEventAdd={...} onEventChange={...} onEventDelete={...} />Tailwind-inspired adapter
No Tailwind installation required — this is CSS-only styling inspired by the Tailwind design system.
import { TailwindScheduler } from 'react-smart-scheduler';
import 'react-smart-scheduler/dist/scheduler.css';
<TailwindScheduler events={events} onEventAdd={...} onEventChange={...} onEventDelete={...} />MUI-inspired adapter
No @mui/material installation required — pure CSS that mirrors Material Design aesthetics.
import { MuiScheduler } from 'react-smart-scheduler';
import 'react-smart-scheduler/dist/scheduler.css';
<MuiScheduler events={events} onEventAdd={...} onEventChange={...} onEventDelete={...} />Headless (bring your own styles)
Use HeadlessScheduler when you want the scheduler's logic with a completely custom CSS layer.
import { HeadlessScheduler } from 'react-smart-scheduler';
import 'react-smart-scheduler/dist/scheduler.css'; // structural styles only
<HeadlessScheduler events={events} className="my-custom-theme" ... />Custom CSS tokens
All adapters expose the same CSS custom properties. Override any token on a parent element:
/* your-theme.css */
.rss-root {
--rss-primary: #0ea5e9; /* sky-500 */
--rss-primary-light: #f0f9ff;
--rss-today-bg: #f0f9ff;
--rss-radius: 10px;
--rss-event-radius: 8px;
}Slots — swap built-in sub-components
For deeper customisation, swap the Header or EventModal with your own components:
import { Scheduler, HeaderSlotProps, EventModalSlotProps } from 'react-smart-scheduler';
const MyHeader: React.FC<HeaderSlotProps> = ({ view, date, onViewChange, onDateChange }) => (
// your custom header JSX
);
<Scheduler
events={events}
slots={{ Header: MyHeader }}
...
/>📖 API
<Scheduler /> Props
| Prop | Type | Default | Description |
|---|---|---|---|
| events | CalendarEvent[] | required | Controlled list of events |
| view | 'day' \| 'week' \| 'month' | 'week' | Active view |
| date | Date | new Date() | Anchor date for the current view |
| onEventAdd | (e: Omit<CalendarEvent, 'id'>) => void | — | Fired when user creates an event |
| onEventChange | (e: CalendarEvent) => void | — | Fired after drag-move or resize |
| onEventDelete | (id: string) => void | — | Fired when user deletes an event |
| onViewChange | (v: ViewType) => void | — | Fired when the view changes |
| onDateChange | (d: Date) => void | — | Fired when the user navigates |
| hourHeight | number | 64 | Pixel height of each hour row |
| startHour | number | 0 | First visible hour (0–23) |
| endHour | number | 24 | Last visible hour (1–24) |
| className | string | '' | Extra CSS class on the root element |
| slots | SchedulerSlots | — | Swap Header or EventModal with custom components |
CalendarEvent type
Every event on the calendar is a plain JavaScript object that satisfies this interface:
interface CalendarEvent {
id: string; // unique identifier
title: string; // text shown on the event chip
start: Date; // inclusive start — must be earlier than end
end: Date; // exclusive end — must be later than start
color?: string; // any valid CSS colour (optional, falls back to palette)
}Field reference
| Field | Type | Required | Notes |
|---|---|---|---|
| id | string | ✅ | Must be unique across all events. Use generateId() for new events or your own UUID/nanoid. |
| title | string | ✅ | Displayed on the chip. Long titles are truncated with … on short events. |
| start | Date | ✅ | JavaScript Date object. Must be strictly before end. Timezone is whatever local time the Date represents. |
| end | Date | ✅ | JavaScript Date object. Must be strictly after start. Same-day is fine; multi-day is supported in month view. |
| color | string | ❌ | Any valid CSS colour string. If omitted, the library picks from EVENT_COLORS using pickColor(id). |
Constraints & rules
✅ start < end
✅ Minimum duration: 1 minute (shorter events still render at minimum chip height)
✅ Multi-day events: supported in Month view; in Day/Week views only the same-day portion is shown
✅ Overlapping events are automatically laid out side-by-side
❌ start === end → undefined behaviour, avoid
❌ start > end → event will not renderCreating events
import { generateId } from 'react-smart-scheduler';
// From a date + hour offsets
const event: CalendarEvent = {
id: generateId(),
title: 'Team standup',
start: new Date(2025, 0, 15, 9, 0), // Jan 15 2025 09:00
end: new Date(2025, 0, 15, 9, 30), // Jan 15 2025 09:30
color: '#3b82f6',
};
// From ISO strings (common when coming from an API / database)
const fromApi: CalendarEvent = {
id: apiEvent.id,
title: apiEvent.title,
start: new Date(apiEvent.startIso), // new Date('2025-01-15T09:00:00')
end: new Date(apiEvent.endIso),
color: apiEvent.color ?? '#8b5cf6',
};
// All-day style (midnight-to-midnight)
const allDay: CalendarEvent = {
id: generateId(),
title: 'Company holiday',
start: new Date(2025, 0, 20, 0, 0, 0),
end: new Date(2025, 0, 20, 23, 59, 59),
color: '#10b981',
};Color reference
color accepts any valid CSS colour string:
color: '#3b82f6' // hex (recommended — predictable across browsers)
color: '#3b82f680' // hex with alpha (50% transparent)
color: 'royalblue' // named colour
color: 'rgb(59,130,246)' // rgb()
color: 'hsl(217,91%,60%)' // hsl()Built-in palette (used when color is omitted):
import { EVENT_COLORS, pickColor } from 'react-smart-scheduler';
// EVENT_COLORS — the full string[] palette
console.log(EVENT_COLORS);
// ['#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#06b6d4', ...]
// pickColor(id) — deterministic colour assignment so the same event
// always gets the same colour, even across re-renders
const color = pickColor(event.id); // returns one of EVENT_COLORSExtending with custom fields
To attach your own metadata (room ID, attendees, status, etc.) without losing TypeScript safety, extend the interface:
// types.ts
import type { CalendarEvent } from 'react-smart-scheduler';
export interface MyEvent extends CalendarEvent {
roomId: string;
attendees: string[];
status: 'confirmed' | 'tentative' | 'cancelled';
description: string;
}
// Your component — cast when passing to Scheduler
const events: MyEvent[] = [...];
<Scheduler
events={events as CalendarEvent[]} // widening cast — safe, Scheduler only reads base fields
onEventAdd={(partial) => {
// partial is Omit<CalendarEvent, 'id'> — add your extra fields before saving
const newEvent: MyEvent = {
...partial,
id: generateId(),
roomId: selectedRoom,
attendees: [],
status: 'confirmed',
description: '',
};
setEvents((prev) => [...prev, newEvent]);
}}
onEventChange={(updated) =>
// updated is CalendarEvent — merge back with your extra fields
setEvents((prev) =>
prev.map((e) => (e.id === updated.id ? { ...e, ...updated } : e))
)
}
onEventDelete={(id) =>
setEvents((prev) => prev.filter((e) => e.id !== id))
}
/>Serialising to / from a database
// Saving to DB — convert Date → ISO string
const toDb = (e: CalendarEvent) => ({
...e,
start: e.start.toISOString(),
end: e.end.toISOString(),
});
// Loading from DB — convert ISO string → Date
const fromDb = (row: DbRow): CalendarEvent => ({
...row,
start: new Date(row.start),
end: new Date(row.end),
});Exported utilities
import {
Scheduler, // the main component
generateId, // generate a unique event id
pickColor, // pick a colour from the palette deterministically
EVENT_COLORS, // the default colour palette (string[])
VERSION, // current library version string
} from 'react-smart-scheduler';
import type {
CalendarEvent, // event shape
SchedulerProps, // props interface
ViewType, // 'day' | 'week' | 'month'
} from 'react-smart-scheduler';🎨 Theming
Override CSS custom properties on .rss-root to customise appearance without touching source:
.rss-root {
--rss-primary: #6366f1; /* accent colour */
--rss-primary-light: #eef2ff;
--rss-border: #d1d5db; /* grid lines */
--rss-bg: #ffffff; /* surface */
--rss-bg-alt: #f9fafb; /* alternate bg */
--rss-today-bg: #eef2ff; /* today column tint */
--rss-text: #111827;
--rss-text-muted: #9ca3af;
--rss-time-gutter-w: 52px;
--rss-radius: 6px;
}💡 Usage patterns
Controlled view + date (recommended)
const [view, setView] = useState<ViewType>('week');
const [date, setDate] = useState(new Date());
<Scheduler
view={view}
date={date}
onViewChange={setView}
onDateChange={setDate}
events={events}
onEventAdd={...}
onEventChange={...}
onEventDelete={...}
/>Semi-controlled (internal navigation state)
// Omit view/date/onViewChange/onDateChange — scheduler manages them internally
<Scheduler
events={events}
onEventAdd={handleAdd}
onEventChange={handleChange}
onEventDelete={handleDelete}
/>Custom business hours
<Scheduler
events={events}
startHour={7}
endHour={20}
hourHeight={80}
...
/>With Zustand
// store.ts
import { create } from 'zustand';
import { CalendarEvent, generateId } from 'react-smart-scheduler';
const useCalendarStore = create<{
events: CalendarEvent[];
add: (p: Omit<CalendarEvent, 'id'>) => void;
update: (e: CalendarEvent) => void;
remove: (id: string) => void;
}>((set) => ({
events: [],
add: (p) => set((s) => ({ events: [...s.events, { ...p, id: generateId() }] })),
update: (e) => set((s) => ({ events: s.events.map((x) => x.id === e.id ? e : x) })),
remove: (id) => set((s) => ({ events: s.events.filter((x) => x.id !== id) })),
}));🛠 Development
# Clone
git clone https://github.com/satthish/react-smart-scheduler.git
cd react-smart-scheduler/packages/react-smart-scheduler
# Install
npm install
# Start demo playground (hot-reload at http://localhost:5173)
npm run dev
# Build library → /dist (runs tsc + vite)
npm run build
# Type-check only
npm run type-check
# Preview built demo
npm run previewPublishing to npm
# Log in (first time)
npm login
# Publish (prepublishOnly runs type-check + build automatically)
npm publishThe prepublishOnly hook ensures the published package is always a fresh, type-safe build.
📸 Demo
Features you can try:
- Switch between Day, Week, and Month views
- Click any empty time slot to open the Create Event modal
- Drag an event to move it (changes time and, in week view, the day)
- Drag the bottom edge of an event to resize its duration
- Click an event chip to edit its title, time, color, or delete it
- + Add random event button to quickly populate the calendar
🏗 Architecture
src/
├── index.ts — public API entry point
├── types.ts — all TypeScript interfaces
├── Scheduler.tsx — root + single DndContext + drag math
├── scheduler.css — all library styles
│
├── views/
│ ├── DayView.tsx — 1-column TimeGrid wrapper
│ ├── WeekView.tsx — 7-column TimeGrid wrapper
│ └── MonthView.tsx — 6×7 month grid + draggable pills
│
├── components/
│ ├── Header.tsx — navigation + view switcher
│ ├── TimeGrid.tsx — shared hour-grid backbone
│ ├── EventItem.tsx — chip: dnd-kit drag + Pointer Capture resize
│ └── EventModal.tsx — add / edit / delete modal
│
├── hooks/
│ └── useScheduler.ts — UI-only state (modal, pending slot)
│
└── utils/
├── dateUtils.ts — date-fns wrappers, grid math, navigation
└── eventUtils.ts — overlap detection, column layout, coloursKey decisions:
| Decision | Why |
|---|---|
| Single DndContext at root | Centralised drag-end math; avoids nested context issues |
| Delta-based drag (not per-cell droppables) | Sub-cell time precision, minimal DOM nodes |
| Pointer Capture API for resize | Tracks cursor anywhere on screen without document listeners |
| Controlled component | Consumer owns state — works with any store |
| CSS custom properties | Zero-runtime theming, no CSS-in-JS required |
🗺 Roadmap
v0.2 — Core polish
- [ ] All-day events row
- [ ] Multi-day spanning events in week view
- [ ] Keyboard shortcut to delete selected event
- [ ] Mini-month navigation widget
v0.3 — Power features
- [ ] Drag-to-create (click-drag on empty slot)
- [ ] Recurring events (RRULE / iCal)
- [ ] Timezone-aware rendering
- [ ] External
.icsimport / export
Future Pro features (opt-in add-ons, core stays free)
- [ ] Resource / room view (multi-column by resource)
- [ ] Gantt-style timeline
- [ ] Custom event render slot (render prop)
- [ ] Virtual scroll for large event volumes
- [ ] Backend sync hooks (optimistic updates)
🤝 Contributing
All contributions are welcome — bug fixes, features, docs, tests.
- Fork & clone the repo
npm installinpackages/react-smart-scheduler- Make your changes (
npm run devfor hot-reload) npm run type-check && npm run buildmust pass- Open a pull request with a clear description of why, not just what
📄 License
MIT © react-smart-scheduler contributors
Built with ❤️ and TypeScript · If this saves you time, consider starring ⭐ the repo
