npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@omidrahmati/react-slot-scheduler

v1.0.1

Published

Production-ready React scheduler with time-grid, task timeline (Gantt), and resource planning modes

Downloads

233

Readme

react-slot-scheduler

The lightweight slot calendar for React — zero dependencies, RTL-first, 3 scheduler modes + complete demo patterns.

npm gzip size TypeScript React zero deps license: MIT



Why react-slot-scheduler?

| | react-slot-scheduler | Alternatives | |---|---|---| | Bundle (gzip) | 27 kB | 400–800 kB (antd + react-dnd + dayjs) | | Dependencies | 0 | 5–10 packages | | RTL / Persian / Arabic | ✅ built-in | ❌ not supported | | Dark mode | ✅ CSS tokens | ❌ manual | | TypeScript | ✅ 100% typed | partial | | Scheduling modes | ✅ Multiple production modes | time-grid only | | API style | ✅ hooks props | class-based (SchedulerData) | | UI framework lock-in | ✅ none | Ant Design required | | Next.js / SSR | ✅ ready | varies |


Scheduling Modes

| Mode | Import / mode prop | Use case | |---|---|---| | Time Grid | default — no mode needed | Appointment booking, clinic, salon | | Meeting Room | default — use DaySchedule[] | Office meeting room reservation | | Task Timeline | mode="task-timeline" + createTaskTimelineAdapter() | Team task / sprint scheduling | | Resource Planner | mode="resource-planner" + createResourcePlannerAdapter() | Multi-resource (rooms, doctors, staff) | | Theme Playground (Demo tab) | — | Preview 6 built-in color presets |


Install

npm i @omidrahmati/react-slot-scheduler
# yarn add @omidrahmati/react-slot-scheduler
# pnpm add @omidrahmati/react-slot-scheduler

Import the stylesheet once at your app entry:

import '@omidrahmati/react-slot-scheduler/dist/index.css';

Quick Start (from scratch)

import { useState } from 'react';
import { BookingCalendar, type DaySchedule } from '@omidrahmati/react-slot-scheduler';
import '@omidrahmati/react-slot-scheduler/dist/index.css';

const schedules: DaySchedule[] = [
  {
    date: '2026-06-01',
    isWorkingDay: true,
    slots: [
      { startTime: '09:00', endTime: '10:00', status: 'booked', itemId: 'a-1', title: 'Standup' },
    ],
  },
];

export default function App() {
  const [value, setValue] = useState(new Date('2026-06-01'));
  return <BookingCalendar value={value} onChange={setValue} schedules={schedules} locale="en-US" />;
}

Next.js App Router

Import CSS once in your app layout or root client entry:

// app/layout.tsx
import '@omidrahmati/react-slot-scheduler/dist/index.css';

Mode 1 — Appointment / Time Grid

The default mode. Pass a DaySchedule[] array — each day has a list of time slots.

import { useState } from 'react';
import { BookingCalendar } from '@omidrahmati/react-slot-scheduler';
import type { DaySchedule } from '@omidrahmati/react-slot-scheduler';
import '@omidrahmati/react-slot-scheduler/dist/index.css';

const schedules: DaySchedule[] = [
  {
    date: '2026-06-01',        // YYYY-MM-DD
    isWorkingDay: true,
    workStartTime: '09:00',    // sets the visible grid start
    workEndTime: '18:00',      // sets the visible grid end
    slots: [
      {
        startTime: '09:00',
        endTime: '10:00',
        status: 'booked',
        itemId: 'appt-1',      // passed to onItemClick when clicked
        title: 'Alice — Haircut',
        description: 'Confirmed',
      },
      {
        startTime: '11:00',
        endTime: '11:30',
        status: 'blocked',
        title: 'Lunch Break',
      },
    ],
  },
  {
    date: '2026-06-02',
    isWorkingDay: false,       // greys out the column, no slots rendered
    slots: [],
  },
];

export default function App() {
  const [date, setDate] = useState(new Date());

  return (
    <BookingCalendar
      value={date}
      onChange={setDate}
      schedules={schedules}
      locale="en-US"
      weekStartsOn={1}         // 1 = Monday
      onItemClick={(id) => console.log('booked slot clicked:', id)}
      onSlotClick={(date, slot) => console.log('empty area clicked:', date, slot)}
    />
  );
}

Drag & Drop Rescheduling

const [schedules, setSchedules] = useState<DaySchedule[]>(initial);

function handleMove({ slot, from, to }: SlotMovePayload) {
  setSchedules(prev =>
    prev.map(day => {
      if (day.date === from.date)
        return {
          ...day,
          slots: day.slots.filter(s => s.startTime !== from.startTime),
        };
      if (day.date === to.date)
        return {
          ...day,
          slots: [...day.slots, { ...slot, startTime: to.startTime, endTime: to.endTime }],
        };
      return day;
    })
  );
}

<BookingCalendar
  draggableSlots
  onSlotMove={handleMove}
  schedules={schedules}
  value={date}
  onChange={setDate}
/>

Multi-Select Empty Slots

Drag across empty cells to select a time range. Only fires on cells that have no existing slot.

<BookingCalendar
  selectionMode
  onSlotDragSelectEnd={(slots) => {
    // slots: Array<{ date: string; startTime: string; endTime: string }>
    openBookingDialog(slots);
  }}
  onSelectionChange={(slots) => setSelectionCount(slots.length)}
  schedules={schedules}
  value={date}
  onChange={setDate}
/>

Mode 2 — Persian / Arabic RTL

Switch locale and the layout, dates, and labels all change automatically.

<BookingCalendar
  locale="fa-IR"
  weekStartsOn={6}       // 6 = Saturday (start of week in Iran)
  direction="auto"       // RTL detected from locale automatically
  translations={{
    previous: 'Previous',
    today:    'Today',
    next:     'Next',
    day:      'Day',
    week:     'Week',
  }}
  schedules={schedules}
  value={date}
  onChange={setDate}
/>

Supported locales: fa-IR, ar, ar-SA, ar-EG, en-US, en-GB, and any BCP-47 string supported by Intl.


Mode 3 — Task Timeline (Gantt)

Use mode="task-timeline" with createTaskTimelineAdapter(). Each item has an assignee and progress field.

import {
  BookingCalendar,
  createTaskTimelineAdapter,
} from '@omidrahmati/react-slot-scheduler';
import type { TaskTimelineItem } from '@omidrahmati/react-slot-scheduler';

const tasks: TaskTimelineItem[] = [
  {
    id: 'task-1',
    date: '2026-06-01',
    startTime: '09:00',
    endTime: '10:30',
    title: 'Design Review',
    status: 'booked',       // 'booked' | 'blocked' | 'custom'
    assignee: 'Sara',
    progress: 70,           // 0–100
  },
  {
    id: 'task-2',
    date: '2026-06-01',
    startTime: '11:00',
    endTime: '12:00',
    title: 'API Contract',
    status: 'custom',
    assignee: 'Arman',
    progress: 35,
  },
  {
    id: 'task-3',
    date: '2026-06-02',
    startTime: '10:00',
    endTime: '11:30',
    title: 'QA Sync',
    status: 'blocked',
    assignee: 'Neda',
    progress: 50,
  },
];

export default function TaskScheduler() {
  const [date, setDate] = useState(new Date());
  const adapter = useMemo(() => createTaskTimelineAdapter(), []);

  return (
    <BookingCalendar
      mode="task-timeline"
      value={date}
      onChange={setDate}
      schedules={[]}              // not used in task-timeline mode
      dataAdapter={adapter}
      dataSource={tasks}
      locale="en-US"
      onItemClick={(id) => {
        const task = tasks.find(t => t.id === id);
        console.log('Task clicked:', task);
      }}
    />
  );
}

TaskTimelineItem interface

interface TaskTimelineItem {
  id: string;
  date: string;        // start date: 'YYYY-MM-DD'
  endDate?: string;    // optional multi-day end date (inclusive)
  startTime: string;   // 'HH:mm'
  endTime: string;     // 'HH:mm'
  title: string;
  status?: 'available' | 'booked' | 'blocked' | 'outside' | 'custom';
  description?: string;
  assignee?: string;
  progress?: number;   // 0–100
  resourceId?: string;
  meta?: Record<string, unknown>;
}

Mode 4 — Resource Planner

Use mode="resource-planner" with createResourcePlannerAdapter(resources). Each item is linked to a resource via resourceId. The resource name is shown as the sub-label in each slot.

import {
  BookingCalendar,
  createResourcePlannerAdapter,
} from '@omidrahmati/react-slot-scheduler';
import type {
  ResourceDefinition,
  ResourcePlannerItem,
} from '@omidrahmati/react-slot-scheduler';

const resources: ResourceDefinition[] = [
  { id: 'room-a',  title: 'Room A' },
  { id: 'room-b',  title: 'Room B' },
  { id: 'dr-amini', title: 'Dr. Amini' },
];

const items: ResourcePlannerItem[] = [
  {
    id: 'rp-1',
    date: '2026-06-01',
    startTime: '09:00',
    endTime: '10:00',
    resourceId: 'room-a',    // must match a resource id
    title: 'Team Sync',
    status: 'booked',
  },
  {
    id: 'rp-2',
    date: '2026-06-01',
    startTime: '10:30',
    endTime: '11:30',
    resourceId: 'room-b',
    title: 'Client Call',
    status: 'custom',
  },
  {
    id: 'rp-3',
    date: '2026-06-02',
    startTime: '14:00',
    endTime: '15:00',
    resourceId: 'dr-amini',
    title: 'Consultation',
    status: 'booked',
  },
];

export default function ResourceCalendar() {
  const [date, setDate] = useState(new Date());
  const adapter = useMemo(() => createResourcePlannerAdapter(resources), []);

  return (
    <BookingCalendar
      mode="resource-planner"
      value={date}
      onChange={setDate}
      schedules={[]}
      resources={resources}
      dataAdapter={adapter}
      dataSource={items}
      locale="en-US"
      onItemClick={(id) => {
        const item = items.find(i => i.id === id);
        const resource = resources.find(r => r.id === item?.resourceId);
        console.log(`${item?.title} — ${resource?.title}`);
      }}
    />
  );
}

ResourceDefinition interface

interface ResourceDefinition {
  id: string;
  title: string;   // displayed as slot sub-label
  meta?: Record<string, unknown>;
}

ResourcePlannerItem interface

interface ResourcePlannerItem {
  id: string;
  date: string;        // 'YYYY-MM-DD'
  startTime: string;   // 'HH:mm'
  endTime: string;     // 'HH:mm'
  resourceId: string;  // must match a ResourceDefinition.id
  title: string;
  status?: 'available' | 'booked' | 'blocked' | 'outside' | 'custom';
  description?: string;
  meta?: Record<string, unknown>;
}

Dark Mode / Custom Theming

Override any or all of the 9 CSS design tokens:

<BookingCalendar
  theme={{
    primary:   '#818cf8',   // accent — buttons, today cell, active tab
    bg:        '#0f172a',   // grid background
    panel:     '#1e293b',   // toolbar + time-label column
    border:    '#334155',   // grid lines
    text:      '#f1f5f9',   // primary text
    mutedText: '#94a3b8',   // time labels + slot sub-labels
    bookedBg:  '#3730a3',   // booked slot fill
    blockedBg: '#334155',   // blocked / outside slot fill
    customBg:  '#5b21b6',   // custom slot fill
  }}
  schedules={schedules}
  value={date}
  onChange={setDate}
/>

Full API Reference

BookingCalendarProps

| Prop | Type | Default | Description | |---|---|---|---| | value | Date | — | Required. Currently focused date | | onChange | (date: Date) => void | — | Required. Navigation callback | | schedules | DaySchedule[] | — | Required. Pass [] when using dataSource | | mode | 'time-grid' \| 'task-timeline' \| 'resource-planner' | 'time-grid' | Scheduler mode | | dataAdapter | SchedulerDataAdapter | — | Required when mode is not 'time-grid' | | dataSource | TaskTimelineItem[] \| ResourcePlannerItem[] | — | Data for non-time-grid modes | | resources | ResourceDefinition[] | — | Required when mode="resource-planner" | | ganttTimeUnit | 'hour' \| 'day' | adapter-driven | Override Gantt time unit in non-time-grid modes | | ganttScale | 'day' \| 'week' \| 'month' | adapter/default | Override day-mode column scale | | viewMode | 'day' \| 'week' | auto | Controlled view; auto = Day on mobile | | onViewModeChange | (mode) => void | — | Fired on Day/Week toggle | | draggableSlots | boolean | false | Enable HTML5 drag & drop | | onSlotMove | (payload: SlotMovePayload) => void | — | Fires after a successful drop | | onBeforeSlotMove | (payload) => boolean \| Promise<boolean> | — | Validate/deny drop before onSlotMove | | onSlotConflict | (payload: SlotConflictPayload) => void | — | Fires on overlap or policy denial | | onGanttItemMove | (payload: GanttMovePayload) => void | — | Fired when an item is moved in Gantt modes | | onGanttItemResize | (payload: GanttResizePayload) => void | — | Fired when an item is resized in hour-based Gantt | | onGanttItemCreate | (payload: GanttCreatePayload) => void | — | Fired when a new item is created by dragging on empty row space (hour mode) | | selectionMode | boolean | false | Enable drag-to-select on empty cells | | selectedSlots | Array<{ date; startTime; endTime }> | — | Controlled external selection model | | onSlotDragSelectStart | (slot) => void | — | Fires when drag selection starts | | onSlotDragSelectMove | (slot) => void | — | Fires for each entered empty cell | | onSlotDragSelectEnd | (slots[]) => void | — | Fires on mouseup with selected cells | | onSelectionChange | (slots[]) => void | — | Fires on each added cell | | isSlotSelected | (slot) => boolean | — | Controlled external selection | | onSlotClick | (date, slot) => void | — | Click on any slot | | onItemClick | (itemId) => void | — | Click on slots with itemId / id | | onBookingClick | (bookingId) => void | — | Deprecated alias of onItemClick | | slotGranularity | number | 30 | Minutes per grid row (15, 30, 60…) | | locale | string | 'fa-IR' | BCP-47 locale — drives labels & direction | | weekStartsOn | 0 \| 1 \| 6 | 6 | Week start: 0=Sun, 1=Mon, 6=Sat | | direction | 'rtl' \| 'ltr' \| 'auto' | 'auto' | Layout direction | | translations | Partial<Translations> | — | Override toolbar label strings | | theme | Partial<CalendarTheme> | — | Override design tokens | | hideTimeColumn | boolean | false | Hide the time-label column | | className | string | — | Extra CSS class on root |

Notes For Production Usage

  • In task-timeline and resource-planner modes, pass schedules={[]} and provide dataAdapter + dataSource.
  • onGanttItemResize applies to hour-based Gantt mode (ganttTimeUnit="hour").
  • onGanttItemCreate applies to hour-based Gantt mode (ganttTimeUnit="hour").
  • For policy validation in time-grid mode, use onBeforeSlotMove and handle denials via onSlotConflict.

DaySchedule

Used in default time-grid mode. The workStartTime / workEndTime pair defines the visible hour range.

interface DaySchedule {
  date: string;            // 'YYYY-MM-DD'
  isWorkingDay: boolean;   // false → column is greyed, slots ignored
  workStartTime?: string;  // 'HH:mm' — grid visible start (default: min slot time)
  workEndTime?: string;    // 'HH:mm' — grid visible end   (default: max slot time)
  slots: CalendarSlot[];
}

CalendarSlot

interface CalendarSlot {
  startTime: string;    // 'HH:mm'
  endTime: string;      // 'HH:mm'
  status: 'booked' | 'blocked' | 'outside' | 'custom';
  itemId?: string;      // → onItemClick fired when clicked
  bookingId?: string;   // deprecated alias for itemId
  title?: string;       // main label in slot
  description?: string; // sub-label in slot
}

Tip: Do not create status: 'available' slots — empty grid cells already represent available time. In selectionMode, users drag across empty cells to select them.

Slot Status Guide

| Status | Typical use | Click | Drag | |---|---|---|---| | booked | Confirmed reservation | ✅ onItemClick | ✅ if draggableSlots | | blocked | Break, holiday, lunch | — | — | | outside | Outside working hours | — | — | | custom | App-defined category | ✅ onItemClick | ✅ if draggableSlots |

SlotMovePayload

interface SlotMovePayload {
  slot: CalendarSlot;
  from: { date: string; startTime: string; endTime: string };
  to:   { date: string; startTime: string; endTime: string };
}

SlotConflictPayload

interface SlotConflictPayload extends SlotMovePayload {
  reason: 'overlap' | 'blocked-by-policy';
  conflictingSlot?: CalendarSlot;
}

GanttMovePayload

interface GanttMovePayload {
  item: GanttItem;
  newRowId: string;
  newDate: string;       // 'YYYY-MM-DD'
  newEndDate?: string;   // set for multi-day item moves
  newStartTime: string;  // 'HH:mm'
  newEndTime: string;    // 'HH:mm'
}

GanttResizePayload

interface GanttResizePayload {
  item: GanttItem;
  newStartTime: string;
  newEndTime: string;
}

GanttCreatePayload

interface GanttCreatePayload {
  rowId: string;
  date: string;       // start date: 'YYYY-MM-DD'
  endDate?: string;   // end date in day-scale multi-column drag
  startTime: string;  // 'HH:mm'
  endTime: string;    // 'HH:mm'
}

CalendarTheme Tokens

| Token | Default | Controls | |---|---|---| | primary | #0f766e | Buttons, today column, active view tab | | bg | #f4f7f7 | Grid background | | panel | #ffffff | Toolbar + time column | | border | #d6e0df | Grid lines | | text | #102725 | Primary text | | mutedText | #5c7270 | Time labels, slot sub-labels | | availableBg | #ffffff | Empty-slot surface | | bookedBg | #fee2e2 | Booked slot background | | blockedBg | #e5e7eb | Blocked/outside slot background | | customBg | #e0f2fe | Custom slot background |


Demo App

git clone https://github.com/omidrahmati2000/react-slot-scheduler
cd react-slot-scheduler/example
npm install
npm run dev   # → http://localhost:5173

Deploy Demo on Vercel

The example app is deploy-ready for Vercel.

  1. Import this repository into Vercel
  2. Set Root Directory to example
  3. Deploy

Detailed guide: Vercel Deployment Guide

| Tab | Mode | What it shows | |---|---|---| | 📅 Appointment | time-grid | FA ↔ EN, drag & drop, multi-select, 4 color swatches | | 🏢 Meeting Room | time-grid | Live drag-rescheduling with React state | | 🗂️ Task Timeline | task-timeline | Bilingual tasks with assignee, dark/light | | 🧩 Resource Planner | resource-planner | Multi-resource (rooms + doctors), FA/EN | | 🎨 Themes | — | 6 presets: Ocean, Forest, Sunset, Midnight, Sakura, Gold |


Scripts

npm run build          # compile → dist/
npm run typecheck      # TypeScript strict check
npm run test           # vitest run
npm run test:coverage  # coverage report

Deep Usage Guides

AI Usage & Attribution

  • This project may use AI-assisted drafting for some docs, examples, or refactors.
  • All published code and documentation are manually reviewed and validated before release.
  • No generated output is accepted as-is without human verification.
  • If a contribution includes AI-assisted content, keep prompts/private data out of commits and ensure licensing/compliance checks are completed.

Documentation Standard

  • All public documentation and code examples are maintained in English.
  • Root README.md is the primary reference for installation, usage patterns, API, and production integration.

Roadmap

  • [ ] Month view
  • [ ] Event resize — drag slot start/end edges
  • [x] Click-to-create (hour mode) — drag on empty area to define a new slot
  • [ ] Cross-day slots — slots spanning midnight

PRs and issues welcome — see CONTRIBUTING.md.


License

MIT © Omid Rahmati

Author

Omid Rahmati
Email: [email protected]