litterally-calendar
v0.4.0
Published
Standalone React calendar component with week & day views, drag & drop, and timezone support
Downloads
395
Maintainers
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-calendarImport 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"