About ChronoLaneJS
ChronoLaneJS is an open-source React and TypeScript calendar and scheduler with day, week, month, agenda, and custom time-grid views. It provides resource scheduling, timezone-aware date handling, range navigation, event layout, selection, movement, resizing, and accessible defaults while keeping state management and persistence outside the component.
[!NOTE]
@chronolanejs/reactis published on npm by an automated, gated release workflow. Remaining follow-up work is tracked in the roadmap.
Why ChronoLaneJS?
- Views that share one model: day, week, month, agenda, and arbitrary time-grid ranges use the same event and navigation contracts.
- Correct across time: IANA timezones, daylight-saving transitions, lazy date-fns locales, and explicit week-start behavior are built in.
- Flexible layout: overlapping events, clipped multi-day events, background events, resources, and non-contiguous ranges are first-class.
- Flexible integration: controlled or uncontrolled navigation, event selection/opening, resizing, and event movement integrate with your state layer.
- Customizable presentation: override the meaningful render boundaries or style the defaults without inheriting a design system.
- Typed and tested: the ESM package emits declarations from source, and every Storybook example runs as a browser and accessibility test.
Interactive documentation
The project website includes a compact playground with every built-in view and resource-column mode. The exhaustive Storybook covers every public customization point, including:
- event overlap, overnight events, and multi-day clipping;
- resources, background events, and custom ranges;
- custom renderers and view registration;
- locale, timezone, and daylight-saving transitions;
- responsive layouts, selection, opening, resizing, and event movement.
Use Storybook's toolbar to change the locale, IANA timezone, and viewport. The
website and full catalog are rebuilt and deployed together from main.
Focused guides cover event drag and resize, resource scheduling, time zones and localization, custom renderers, Vite and Next.js, accessibility, and the complete TypeScript API.
Installation
Install the package and its peer dependencies with:
npm install @chronolanejs/react react react-dom date-fns @date-fns/tz
Import the package stylesheet once at your application entry point:
import "@chronolanejs/react/styles.css";
To evaluate the development build and Storybook locally, clone the repository:
git clone https://github.com/MaximilianWalker/ChronoLaneJS.git
cd ChronoLaneJS
npm ci
npm run storybook
Quick start
import Calendar from "@chronolanejs/react";
import "@chronolanejs/react/styles.css";
const events = [
{
id: "planning",
title: "Planning",
start: new Date(2026, 8, 1, 9),
end: new Date(2026, 8, 1, 10, 30),
color: "#2563eb"
}
];
export default function Schedule() {
return (
<Calendar
view="week"
events={events}
locale="en-US"
timeZone="UTC"
/>
);
}
ChronoLaneJS is marked as a client module and can be imported directly from a Next.js client component without a framework-specific wrapper.
Core concepts
Views
| View | Purpose |
|---|---|
day | One-day time-grid preset |
week | Seven-day time-grid preset |
month | Month grid with optional outside days |
agenda | Event groups across a configurable date range |
time-grid | Generic, configurable time-grid renderer |
day and week are presets over TimeGridView; they share its layout and
interaction behavior rather than maintaining separate engines. Resource
columns are available on both presets and on every custom time-grid range.
The root Calendar props are a discriminated TypeScript union keyed by
view. Shared calendar behavior stays at the root, while configuration owned
by the selected view lives under viewProps. Omitting view selects the
week contract. Misspelled props, view configuration placed at the root, and
combinations such as time-grid scale options on a month view fail compilation.
Direct view components accept their own configuration as direct props.
Use satisfies CalendarViewProps<"month"> (with the appropriate view name)
when defining reusable viewProps objects so TypeScript checks the object at
its declaration site.
Ranges
Time-grid and agenda ranges accept:
"day"or"week";- a positive number of consecutive days;
- an array of visible dates;
{ start, end }or{ start, dayCount };{ dates, navigation }for non-contiguous days with explicit movement;- a callback returning any supported definition.
import { startOfWeek } from "date-fns";
<Calendar
view="time-grid"
viewProps={{
range: {
start: (anchor) => startOfWeek(anchor, { weekStartsOn: 1 }),
dayCount: 7,
includeDay: (day) => day.getDay() >= 1 && day.getDay() <= 5,
navigation: { stepDays: 7 }
}
}}
/>
Non-contiguous ranges are supported, so business calendars do not need a
dedicated work-week view. The range owns previous/next behavior through
navigation: { stepDays } or a custom navigation.resolveAnchor callback.
Use anchor-aware dates for a recurring non-contiguous pattern; a literal
Date[] represents a fixed set of days.
Time-grid scale
Time-grid views keep the visible wall-clock range separate from selection and label cadence:
<Calendar
view="week"
viewProps={{
minTime: "08:00",
maxTime: "18:00",
slotDuration: 30,
resizeStep: 15,
labelInterval: 60
}}
/>
minTime is inclusive and maxTime is exclusive. Both use strict,
zero-padded HH:mm values; maxTime also accepts 24:00. slotDuration
controls selectable granularity, resizeStep independently controls pointer,
touch, and keyboard resize precision, and labelInterval controls time labels
and major dividers. Invalid configurations throw rather than being silently
adjusted.
Multi-day foreground events can remain in those hourly slots or use a compact region aligned below the day/resource headers:
<Calendar
view="week"
viewProps={{ multiDayEventLayout: "dedicated" }}
/>
The default is "timed", which preserves the original layout. "dedicated"
derives placement from start and end: any event crossing local midnight is
shown in the separate region, while background events stay in the hourly grid.
No allDay event flag is required.
Slot sizing
Time-grid slot dimensions use one flat typed contract. width and height
fix their slot axis, while minWidth and minHeight distribute available
space until the minimum would be crossed and then enable scrolling:
<Calendar
view="day"
viewProps={{
slotSizing: {
minWidth: 120,
height: 48
}
}}
/>
width and minWidth are mutually exclusive, as are height and
minHeight. Omitting both width properties gives fluid columns with no
minimum. Omitting both height properties retains the fixed 50px default;
use minHeight: 0 for fully fluid rows. Fixed dimensions must be positive
finite CSS pixel sizes. Minimum dimensions may also be zero. Invalid or
conflicting values throw RangeError during rendering. Fixed dimensions
shrink-wrap the time grid on that axis until its parent constrains it. Fluid
height requires a parent with a definite height so there is vertical space to
distribute.
Time-grid headers use the same column tracks as their slots. A grouped day or resource header spans its underlying tracks and never establishes an independent width, so custom header content cannot drift out of alignment.
Locales and timezones
locale accepts a supported locale name or a date-fns locale object. en-US
is the synchronous default; other named locales are loaded lazily and cached.
<Calendar locale="en-GB" timeZone="UTC" />
Call preloadCalendarLocale(name) when a locale should be available before
render. A view-specific weekStart overrides the locale convention.
Locales format dates and supply calendar conventions. Application labels such
as navigation and empty-state text remain caller-controlled through
messages.
Resources
Resources are arbitrary values rather than a room-specific abstraction:
import type { CalendarResourceConfig } from "@chronolanejs/react";
const calendarResources: CalendarResourceConfig<ScheduleEvent, Person> = {
items: people,
getId: (person) => person.uuid,
getTitle: (person) => person.displayName,
getEventIds: (event) => event.assigneeUuids
};
<Calendar
view="day"
events={events}
viewProps={{ resources: calendarResources }}
/>
The resources object keeps its items and accessors under one inferred generic
contract. The defaults read resource.id, select a title from
resource.title, resource.name, or resource.id, and read assignments from
event.resourceIds, event.resourceId, or event.resource.id.
Resource IDs are CalendarResourceId values: non-empty strings or finite
numbers. Equality uses JavaScript Map/Set SameValueZero semantics: 1 and
"1" identify different resources, while 0 and -0 identify the same one.
Missing or duplicate item IDs throw before layout is rendered.
Event assignments use set behavior: repeated IDs produce one segment per
matching column, while IDs absent from items do not produce a segment.
Omitting resources, or supplying an empty items array, creates one
ungrouped column per visible day and does not render a resource-header row.
When resource columns are configured, groupBy="day" (the default) renders
each day above its resources. Set groupBy="resource" to render each resource
above its visible days:
<Calendar
view="week"
events={events}
viewProps={{
resources: calendarResources,
groupBy: "resource"
}}
/>
Both orders use the same day-resource columns and event assignments; only the
outer grouping and physical column order change. The concrete item remains
available as column.resource, slot.resource, segment.resource, and in
both event-drop positions. Stable IDs are available alongside those values.
Customization
Renderers
Views expose intentional renderer boundaries where applicable:
| View | components keys |
|---|---|
| Agenda | event, dayHeader, empty, navigation |
| Month | event, dayHeader, navigation |
| Time grid and presets | event, slot, backgroundEvent, dayHeader, resourceHeader, navigation |
<Calendar
view="week"
events={events}
viewProps={{
components: {
event: ScheduleEvent,
slot: ScheduleSlot,
navigation: ScheduleNavigation
}
}}
/>
Time-grid event renderers receive { event, segment, selected, elementProps },
slot renderers receive { slot, selected, elementProps }, and day/resource
header renderers receive their concrete value, indexes, covered columns, and
prepared title. Positional values remain available without duplication on the
segment, slot, or covered columns. Agenda and month event renderers receive the
normalized event, their prepared visible values, selected, and
elementProps.
Spread elementProps onto the renderer's root element to retain layout,
accessibility, selection, opening, and raw event behavior. ChronoLaneJS owns those
behaviors while the renderer owns markup and presentation.
Styling
The bundled CSS provides the same neutral, product-ready presentation used by the Storybook examples and website playground. Time grids include bordered surfaces, aligned header dividers, centered time labels, and compact event cards without requiring application CSS.
Use className, style, event colors and styles, renderer overrides, or the
documented custom-property extension points to adapt that default:
.team-schedule {
--month-view-day-min-width: 9rem;
}
CalendarStyle provides autocomplete for the supported calendar tokens while
remaining compatible with regular React styles and application-defined CSS
variables. The same typed className and style props are available on
Calendar, AgendaView, MonthView, DayView, WeekView, and
TimeGridView:
<Calendar
view="week"
style={{
"--calendar-time-grid-header-row-height": "40px",
"--calendar-time-grid-time-axis-width": "72px",
"--calendar-time-grid-line-width": "0px",
"--calendar-time-grid-frame-width": "1px"
}}
/>
Header height applies to each header row. Resource columns therefore add a
second full-height row. The time-axis token sizes both the label column and its
empty header corner. Set the line width to 0px to remove header and slot grid
lines consistently. Layout-sensitive time-grid tokens accept deterministic
non-negative pixel lengths. TypeScript cannot exclude negative numeric
template strings, which browsers treat as invalid for these sizes. The
frame-width token controls both the visible outer border
and the time grid's intrinsic border-box geometry, so custom frames cannot
desynchronize the fixed slot tracks.
Scrollable calendar surfaces retain native browser behavior while using a
compact inset thumb by default. Override the shared scrollbar tokens on the
Calendar root or any ancestor:
<Calendar
className="team-schedule"
style={{
"--calendar-scrollbar-size": "14px",
"--calendar-scrollbar-width": "auto",
"--calendar-scrollbar-inset": "4px",
"--calendar-scrollbar-thumb": "#64748b",
"--calendar-scrollbar-thumb-hover": "#475569",
"--calendar-scrollbar-track": "transparent",
"--calendar-scrollbar-radius": "999px"
}}
/>
The calendar-scroll-region class is the stable advanced extension point for
projects that need to replace the browser-specific scrollbar rules entirely.
The CSS variables inherit into time-grid, month, and agenda scroll regions, so
one root override keeps every built-in view consistent. The
--calendar-scrollbar-width token controls the standards-based scrollbar;
Chromium and Safari use --calendar-scrollbar-size so their inset thumb and
hidden end buttons can be enforced through the WebKit scrollbar API.
Interactions
Selection and opening callbacks receive the normalized source event and a rendered-occurrence context, never a clipped source in place of the event. Single click and Space select; double-click, double-tap, and Enter open. These gestures never change meaning based on which callbacks are present.
eventInteractions adds raw click, double-click, context-menu, and key-down
callbacks without replacing the semantic behavior. Use canSelectEvent and
canOpenEvent to restrict those semantic actions per event occurrence.
Supplying viewProps.onEventDrop makes the event surface draggable by pointer
or touch and movable from its keyboard focus. Movement preserves the pointer's
grab offset, targets the visible slotDuration scale, and announces each
proposed date, time, and resource. Arrow Up/Down changes time, Arrow Left/Right
changes the visible column, Enter or blur commits, and Escape cancels.
Supplying viewProps.onEventResize adds transparent start/end edge hit zones.
The resize cursor appears at the edge and the event follows the pointer while
snapping by resizeStep, independently from the visual slots. Resizing
preserves the resource. Use canDragEvent and canResizeEvent for per-segment
restrictions.
In a dedicated multi-day region, Left/Right moves across visible day/resource columns and resizing uses whole calendar-day steps. Both operations preserve the event's wall-clock fields, including across daylight-saving transitions.
onEventDrop receives the source event, proposed start and end, and
explicit source and destination positions. Moving a clipped multi-day
event preserves the source event's complete duration. Each position includes
both the concrete resource value and its stable resourceId.
onEventResize receives the source event, changed edge, proposed complete
boundaries, and source day/resource position. ChronoLaneJS proposes changes;
application state remains consumer-owned.
Custom views
Extend or replace the view registry through views:
const views = {
quarter: {
component: QuarterView,
defaultProps: { months: 3 }
}
};
<Calendar
view="quarter"
views={views}
viewProps={{ compact: true }}
/>
A custom view receives events, background events, the active view name, shared
calendar props, registered defaults, and viewProps. TypeScript infers the
allowed custom view names and viewProps from the supplied registry.
Public API
The complete consumer documentation is organized as GitHub-native Markdown and rendered from the same files on the project site:
- Getting started
- Drag and resize events
- Resource scheduling
- Time zones and locales
- Vite and Next.js integration
- Styling and theming
- Custom renderers
- Accessibility
- Complete TypeScript API
- Migrating from v1 to v2
- Changelog
ChronoLaneJS exports Calendar as the default, together with:
AgendaView,DayView,MonthView,TimeGridView, andWeekView;defaultCalendarViews;- date parsing and timezone helpers;
- range construction and navigation helpers;
- locale discovery, loading, and preloading helpers;
- public prop, event, range, renderer, resource, registry, and layout types.
Time-grid layout internals and default renderer implementations remain private so they can evolve without expanding the compatibility surface. Public functions and components include TSDoc in their generated declarations.
Runtime support
ChronoLaneJS supports these runtime combinations:
| Runtime | Supported versions |
|---|---|
| Node.js | >=22.14.0 <23, >=24.10.0 <25, or >=26.0.0 <27 |
| React and React DOM | matching >=18.2.0 <20 releases |
| Chrome and Edge | 111 or newer |
| Firefox | 114 or newer |
| Safari and iOS Safari | 16.4 or newer |
Node support follows upstream-maintained release lines. Future Node majors are not supported until they are added to the compatibility matrix. CI tests the minimum supported React release on every supported Node major and also tests the repository's current React release during full validation.
The browser targets match the package's explicit Baseline Widely Available
build target. Supported browsers require native ES modules, Intl.Locale, and
Intl.DateTimeFormat with IANA timezone data. They need no polyfills.
ChronoLaneJS does not ship polyfills; older browsers and runtimes with
incomplete internationalization data are unsupported.
Author
- Website: diogocrava.dev
- GitHub: @MaximilianWalker
- LinkedIn: Diogo Crava
ChronoLaneJS is designed, developed, and maintained by its author as a solo project. Bug reports and feature requests are welcome through GitHub Issues; external pull requests are not part of the project workflow.
Feel free to contact me through any of the platforms above.
License
ChronoLaneJS is available under the MIT License.