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

litterally-calendar

v0.4.0

Published

Standalone React calendar component with week & day views, drag & drop, and timezone support

Downloads

395

Readme

litterally-calendar

A React calendar component with week and day views, drag-and-drop, resize, and timezone support. Zero runtime dependencies beyond React.

Installation

npm install litterally-calendar
# or
pnpm add litterally-calendar

Import the styles once in your app entry:

import "litterally-calendar/styles.css"

Quick start

import { Calendar } from "litterally-calendar"
import "litterally-calendar/styles.css"

const events = [
  {
    id: "1",
    title: "Team sync",
    start: "2025-05-26T09:00:00.000Z",
    end: "2025-05-26T10:00:00.000Z",
  },
]

export default function App() {
  return (
    <div style={{ height: 700 }}>
      <Calendar events={events} />
    </div>
  )
}

The calendar needs a fixed height on its parent to render correctly.


Events

All times are UTC ISO 8601 strings. The timezone prop controls how they are displayed — never pass pre-shifted local times.

type CalendarEvent<TData = Record<string, unknown>> = {
  id: string
  title: string
  start: string // UTC ISO — "2025-05-26T09:00:00.000Z"
  end: string // UTC ISO
  color?: string // hex or CSS color — overrides the default blue
  allDay?: boolean // renders in the all-day row at the top
  data?: TData // arbitrary payload, passed back in all callbacks
}

Props

Data

| Prop | Type | Description | | --------------- | ----------------- | ------------------------------------- | | events | CalendarEvent[] | Controlled events array. | | defaultEvents | CalendarEvent[] | Initial events for uncontrolled mode. |

View

| Prop | Type | Default | Description | | ------------- | ----------------- | -------- | ----------------------------------- | | view | "week" \| "day" | "week" | Controlled current view. | | date | Date \| string | today | Controlled anchor date. | | defaultDate | Date \| string | today | Initial date for uncontrolled mode. |

Configuration

| Prop | Type | Default | Description | | ------------ | ---------------- | ---------------- | ------------------------------------------------ | | timezone | string | browser timezone | IANA timezone string, e.g. "America/New_York". | | locale | string | "en-US" | BCP 47 locale for all date/time formatting. | | weekConfig | WeekViewConfig | — | See below. | | timeGrid | TimeGridConfig | — | See below. | | className | string | — | Extra class on the root element. |

WeekViewConfig

type WeekViewConfig = {
  daysToShow?: number // 1–7, default 7
  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 // 0 = Sunday, 1 = Monday (default)
  hideWeekends?: boolean // default false
}

TimeGridConfig

type TimeGridConfig = {
  startHour?: number // 0–23, default 6
  endHour?: number // 0–23, default 22
}

Callbacks

| Prop | Signature | Fires when | | --------------- | -------------------------------- | ---------------------------------------------------------------------------- | | onEventClick | (event) => void | User clicks an event. | | onSlotClick | (slotInfo) => void | User clicks an empty time slot. | | onEventCreate | (slotInfo) => void | Alias for onSlotClick, semantic sugar. | | onEventDrop | (result: DragResult) => void | User drops a dragged event. | | onEventResize | (result: ResizeResult) => void | User finishes resizing an event. | | onEventChange | (event) => void | Fires after both drop and resize with the updated event. | | onEventDelete | (event) => void | Convenience — not called automatically; wire it to onEventClick if needed. | | onDateChange | (date: Date) => void | Internal navigation changed the date. | | onViewChange | (view) => void | Internal view changed. |

type SlotInfo = {
  start: string // UTC ISO of the clicked slot start
  end: string // UTC ISO of the clicked slot end (start + 30 min)
  date: string // "YYYY-MM-DD" of the column
}

type DragResult = {
  event: CalendarEvent
  start: string // new UTC ISO start
  end: string // new UTC ISO end
}

type ResizeResult = {
  event: CalendarEvent
  start: string
  end: string
  edge: "top" | "bottom"
}

Controlled vs. uncontrolled

Each of the three stateful axes (events, view, date) can be controlled or uncontrolled independently.

Fully uncontrolled — the calendar manages everything internally:

<Calendar defaultEvents={events} defaultDate={new Date()} />

Fully controlled — you own all state:

const [events, setEvents] = useState(initialEvents)
const [view, setView] = useState<CalendarView>("week")
const [date, setDate] = useState(new Date())

;<Calendar
  events={events}
  view={view}
  date={date}
  onEventDrop={({ event, start, end }) =>
    setEvents((prev) =>
      prev.map((e) => (e.id === event.id ? { ...e, start, end } : e))
    )
  }
  onViewChange={setView}
  onDateChange={setDate}
/>

Mixed — control only what you need:

// Controlled events, uncontrolled view/date
<Calendar events={events} onEventDrop={handleDrop} />

Custom rendering

renderEvent

Replace the default event block entirely. Receives the pre-calculated position styles — you must apply them.

<Calendar
  events={events}
  renderEvent={({ event, style, isDragging }) => (
    <div
      style={{
        ...style,
        background: isDragging ? "#6366f1" : "#3b82f6",
        borderRadius: 4,
        padding: "2px 6px",
        color: "#fff",
        fontSize: 12,
      }}
    >
      {event.title}
    </div>
  )}
/>

renderDayHeader

Replace the column header for each day.

<Calendar
  events={events}
  renderDayHeader={({ date, isToday, label }) => (
    <div style={{ padding: 8, fontWeight: isToday ? 700 : 400 }}>{label}</div>
  )}
/>

renderTimeSlot

Replace each time label in the left gutter.

<Calendar
  events={events}
  renderTimeSlot={({ label }) => (
    <span style={{ fontSize: 10, color: "#9ca3af" }}>{label}</span>
  )}
/>

Headless usage — useCalendar

Use the hook directly to build a fully custom UI, keeping only the calendar logic.

import { useCalendar } from "litterally-calendar"

function MyCalendar() {
  const calendar = useCalendar({
    events,
    timezone: "Europe/Madrid",
  })

  return (
    <div>
      <button onClick={calendar.goToPrevious}>‹</button>
      <button onClick={calendar.goToToday}>Today</button>
      <button onClick={calendar.goToNext}>›</button>

      {/* calendar.visibleDays, calendar.positionedEventsByDay, etc. */}
    </div>
  )
}

UseCalendarReturn exposes:

| Field | Type | Description | | ----------------------- | --------------------------------- | ---------------------------------------- | | config | ResolvedConfig | Merged config with all defaults applied. | | view | CalendarView | Current view. | | setView | (view) => void | Change view. | | currentDate | Date | Current anchor date. | | setDate | (date) => void | Change date. | | goToNext | () => void | Advance by 1 day or 1 week. | | goToPrevious | () => void | Go back by 1 day or 1 week. | | goToToday | () => void | Jump to today. | | events | CalendarEvent[] | Active events (controlled or internal). | | visibleDays | Date[] | Dates of the currently visible columns. | | timeSlots | { hour, minute, label }[] | 30-min slots for the time gutter. | | dayHeaders | { date, isToday, label }[] | One entry per visible column. | | positionedEventsByDay | PositionedEvent[][] | Pre-laid-out events per column. | | allDayEvents | { event, startCol, spanCols }[] | All-day events with grid positions. |


CSS customization

Import the stylesheet then override variables on .ltc-calendar or :root:

.ltc-calendar {
  --ltc-event-color: #6366f1; /* default event color */
  --ltc-bg-today: #f5f3ff; /* today column background */
  --ltc-gutter-width: 56px; /* width of the time gutter */
  --ltc-header-height: 44px;
  --ltc-border: #e5e7eb;
  --ltc-font-family: "Inter", system-ui, sans-serif;
}

Dark mode

Add .dark or data-theme="dark" to any ancestor of .ltc-calendar:

<div class="dark">
  <div style="height: 700px">
    <!-- calendar renders in dark mode -->
  </div>
</div>

Or define your own overrides:

[data-theme="dark"] .ltc-calendar {
  --ltc-bg: #0f172a;
  --ltc-bg-today: #1e293b;
  --ltc-border: #334155;
  --ltc-text: #f1f5f9;
  --ltc-text-muted: #94a3b8;
}

Per-event color

Set color on any event to override the default for that event only:

{ id: "1", title: "Urgent", start: "...", end: "...", color: "#ef4444" }

Utility exports

import {
  utcToZoned, // (utcISO: string, timezone: string) => Date
  zonedToUtc, // (localDate: Date, timezone: string) => string
  getBrowserTimezone, // () => string  — e.g. "Europe/Madrid"
  formatTime, // (date: Date, locale?: string) => string  — "9:30 AM"
  formatDayHeader, // (date: Date, locale?: string) => string  — "Mon 26"
} from "litterally-calendar"