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

react-weekly-availability-calendar

v1.6.1

Published

A customizable, draggable weekly availability calendar component for React. Create, resize, move time slots with zero external dependencies.

Readme

react-weekly-availability-calendar

A customizable, draggable weekly availability calendar component for React. Create, resize, and move time slots with zero external styling dependencies.

npm license

→ Try the live demo

Dragging is the whole point, so a screenshot undersells it. Every example on the demo page is interactive — create a slot by dragging on empty space, move one by dragging it, resize it by its edges.

Features

  • Drag to create, resize, and move availability slots
  • Blocked time slots with striped overlay
  • Multi-day create: one drag lays the same range across several days
  • Per-slot colors, plus a built-in darkTheme preset
  • Locale-aware day names and times via Intl
  • Undo / redo through the useAvailabilityHistory hook
  • Fully customizable via theme, classNames, or render props
  • Zero external dependencies (only react and react-dom as peer deps)
  • Styles auto-injected at runtime — no CSS import needed
  • SSR safe, and ships a "use client" directive for the Next.js App Router
  • TypeScript first

Install

npm install react-weekly-availability-calendar

Quick Start

import { useState } from "react";
import { AvailabilityCalendar } from "react-weekly-availability-calendar";
import type { AvailabilitySlot } from "react-weekly-availability-calendar";

function App() {
  const [slots, setSlots] = useState<AvailabilitySlot[]>([
    { id: 1, dayOfWeek: 1, startTime: "09:00", endTime: "12:00" },
    { id: 2, dayOfWeek: 3, startTime: "14:00", endTime: "17:00" },
  ]);

  return (
    <AvailabilityCalendar
      slots={slots}
      onSlotsChange={setSlots}
      snapMinutes={30}
      timeFormat="12"
    />
  );
}

Keyboard

The calendar is fully operable without a pointer.

| Focus | Key | Action | | ------------ | -------------------------------------------- | ------------------------------------------ | | A day column | Enter / Space | Add a slot at the earliest free time | | A slot | ↑ / ↓ | Move earlier / later by one snap increment | | A slot | ← / → | Move to the previous / next day | | A slot | Shift + ↑/↓ | Resize from the end edge | | A slot | Delete / Backspace | Remove the slot | | A slot | Enter / Space | Fire onSlotClick |

Every change is announced in a polite live region. Moves that would collide with another slot or a blocked range are refused and announced rather than silently ignored.

The calendar deliberately does not use ARIA's grid role. That role requires row and gridcell descendants across the whole surface — over a thousand cells at a ten-minute snap — which would leave a screen-reader user traversing all of them to reach a handful of slots. Slots and day columns are exposed as labelled buttons instead.

Knowing what changed

onSlotsChange receives a description of the edit as its second argument, so you can issue targeted writes instead of re-saving the week:

<AvailabilityCalendar
  slots={slots}
  onSlotsChange={(next, { created, updated, removed }) => {
    setSlots(next);
    created.forEach((s) => api.post("/slots", s));
    updated.forEach((s) => api.patch(`/slots/${s.id}`, s));
    removed.forEach((id) => api.delete(`/slots/${id}`));
  }}
  snapMinutes={30}
  timeFormat="12"
/>

Ignore the second argument and nothing changes — it is purely additive.

A merge appears as an update plus a removal: the surviving slot's range grows and the absorbed id is reported as removed, which is exactly what a backend needs to hear.

Tooltips

Slots and blocked ranges carry a native title by default — useful because a slot at a small snap increment can be too short to show its own labels, and long blocked labels are truncated. Override or suppress it:

<AvailabilityCalendar
  slotTooltip={(slot, info) => `${info.startLabel}–${info.endLabel}`}
  blockedSlotTooltip={(slot) => slot.label}
  // return null from either to remove the tooltip
/>

Slots that touch are merged

When an edit leaves two slots touching or overlapping, they are merged into one. The merge keeps the fields — including the id — of the slot that starts earliest:

// before
[
  { id: "a", dayOfWeek: 1, startTime: "09:00", endTime: "10:00" },
  { id: "b", dayOfWeek: 1, startTime: "10:00", endTime: "11:00" },
][
  // after onSlotsChange — one slot, and "b" is gone
  { id: "a", dayOfWeek: 1, startTime: "09:00", endTime: "11:00" }
];

This applies to every path: dragging, resizing, and keyboard edits. Treat the array onSlotsChange hands you as a fresh statement of the week's availability, not as a diff against what you passed in — reconciling it by id will lose merged slots.

End-of-day slots and storage

A slot running to midnight ends at "24:00". That is deliberate, and matches ISO 8601, which allows hour 24 as the end of an interval. It is also the only representation that survives a round trip:

| End value | Parses back to | Result | | --------- | -------------- | ------------------------------------------------------------- | | "24:00" | 1440 | correct | | "00:00" | 0 | end is before the start — the slot is rejected and disappears | | "23:59" | 1439 | silently a minute short |

Some stores reject hour 24 — SQL TIME, and most date parsers. Convert at that boundary, where you know the string is an end time:

import {
  toStorageSlots,
  fromStorageSlots,
} from "react-weekly-availability-calendar";

await db.save(toStorageSlots(slots)); // "24:00" -> "00:00"
const slots = fromStorageSlots(await db.load()); // "00:00" -> "24:00"

Both are pure, leave every other slot untouched, and preserve custom fields.

Documentation

The full API — every prop, every variant, with live controls — is generated from the TypeScript types, so it never drifts from the source:

→ Storybook: full API reference

→ Live demo and examples

At a glance

| | | | ---------------- | ---------------------------------------------------------------------- | | Required | slots, onSlotsChange, snapMinutes, timeFormat | | Range | startHour, endHour — show only the hours you schedule in | | Week | startDay, dayLabelFormat, locale, gridLineStyle | | Behaviour | readOnly, multiDayCreate, onSlotClick, blockedSlots | | Limits | disabledDays, minSlotMinutes, maxSlotMinutes | | Styling | theme (plus the darkTheme preset), classNames, per-slot color | | Render props | renderSlot, renderBlockedSlot, slotTooltip, blockedSlotTooltip | | Hook | useAvailabilityHistory for undo / redo |

Types are exported for all of the above, and every prop carries TSDoc — your editor will show the same descriptions Storybook does.

Types

type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6;

interface AvailabilitySlot {
  id: number | string;
  dayOfWeek: DayOfWeek;
  startTime: string; // "HH:mm"
  endTime: string; // "HH:mm"
  color?: string; // optional per-slot background
}

interface BlockedSlot {
  dayOfWeek: DayOfWeek;
  startTime: string;
  endTime: string;
  label: string;
}

Links

Notes

  • End-of-day slots: Slots ending at midnight display as 24:00 in 24-hour format (not 00:00) to clearly represent end-of-day rather than start-of-day.
  • SSR safe: All DOM access is guarded. Works with Next.js, Remix, etc.
  • No CSS import needed: Styles are auto-injected via a <style> tag on first render.

License

MIT