---
title: "JavaScript Schedule"
description: "The LemonadeJS Schedule block for JavaScript: Calendar and agenda scheduling views. Contract-verified, framework-agnostic, zero dependencies, with a live example."
source: https://lemonadejs.com/docs/plugins/schedule/
---

<link rel="stylesheet" href="/v6/modal.css">
<link rel="stylesheet" href="/v6/calendar.css">
<link rel="stylesheet" href="/v6/dropdown.css">
<link rel="stylesheet" href="/v6/schedule.css">

# JavaScript Schedule

`@lemonadejs/schedule` · ✓ 37 contract checks · framework-agnostic · zero dependencies

`<Schedule />` — the week/day time-grid scheduler, ported from the v5
plugin (@lemonadejs/schedule). The v5 block is NOT a month calendar:
it is a vertical time grid (1px per minute) with day columns, and that
exact model is preserved here:

  - views: type 'week' (Sun–Sat) | 'weekdays' (Mon–Fri) | 'day';
    weekly mode swaps real dates for abstract weekdays (recurring
    template schedules — events carry `weekday` instead of `date`)
  - grid: minutes per row (default 15, row height = grid px); snap:
    create/resize step in minutes (defaults to grid)
  - drag-create on empty cells, drag-move (top 25px zone) and
    drag-resize (bottom 5px zone) of events, with conflict blocking
    when overlap=false and read-only hour ranges
  - selection (click, Ctrl+click multi), keyboard: arrows walk events,
    Enter opens the editor for the selected event, N creates an event
    at the first free slot, Alt+arrows move and Shift+Up/Down resize
    the selected event (the same commit path as the drag gestures),
    Delete removes, Ctrl+C/V copy/paste (+1 row shift), Ctrl+Z/Y
    undo/redo (full history: add/update/delete/setData)
  - validrange hides hours outside the window; readonlyrange disables
    (striped) hour ranges; now-pointer line on today's column
  - the v5 Event editor (dist/event.js — a Modal with title/when/
    start/end/location/color palette) is built in, composing
    @lemonadejs/modal; it opens on double click and after drag-create
    (v5's onedition moments) — disable with editor=false

v5 → v6 mapping: validRange → validrange, readOnlyRange → readonlyrange,
render() → api.refresh(); onchangeevent(record, oldValue, newValue)
keeps its v5 name — it fires after every committed event update
(drag-move/resize commit, editor save, api.updateEvent), alongside
onupdate (same payload); onbeforechangeevent(record, oldValue,
newValue) is its cancellable pre-flight — return false and nothing
mutates (the drag snaps back). v5 fired onbeforechangeevent at
gesture START with the raw drag state; the v6 gesture-start veto is
onbeforedrag({ kind, record }) — onbeforechangeevent now guards the
concrete change at COMMIT, so it sees the real oldValue/newValue.
Callbacks drop the leading `self` argument (pure
components, no this); document.dictionary → weekdays prop; getEvent
returns the RECORD (not a DOM node). Data is BY REFERENCE: mutate the
array (or a record) and touch() — the grid re-renders once.

## Example

<!--example-->

```js
import { html } from 'lemonadejs';
import Schedule from '@lemonadejs/schedule';

// This week's dates (offset from Sunday, local time), so the example is always populated
const day = (offset) => {
    const d = new Date();
    d.setDate(d.getDate() - d.getDay() + offset);
    return [d.getFullYear(), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0')].join('-');
};

const App = (props, { state }) => {
    const events = state([
        { date: day(1), start: '09:00', end: '09:30', title: 'Standup', color: '#3f51b5' },
        { date: day(1), start: '11:00', end: '12:30', title: 'Design review', color: '#009688', location: 'Room 2' },
        { date: day(2), start: '14:00', end: '16:00', title: 'Deep work', color: '#795548' },
        { date: day(3), start: '10:00', end: '11:00', title: 'Customer call', color: '#ff9800', location: 'Zoom' },
        { date: day(4), start: '15:00', end: '17:00', title: 'Sprint planning', color: '#e91e63', location: 'Room 4' },
        { date: day(5), start: '13:00', end: '14:00', title: 'All-hands', color: '#9c27b0', readonly: true },
    ]);
    const note = state('Drag empty cells to create an event, drag events to move or resize, double-click to edit.');

    return html`<div>
        <${Schedule} data="${events}" type="weekdays" validrange="${['08:00', '18:00']}" grid="30"
            oncreate="${(added) => (note.value = 'Created: ' + added.map((e) => e.title).join(', '))}"
            onupdate="${(record) => (note.value = `${record.title} is now ${record.date} ${record.start}–${record.end}`)}"
            ondelete="${(record) => (note.value = 'Deleted: ' + record.title)}" />
        <p style="font-size:13px">${note}</p>
    </div>`;
};
```

## Installation

```bash
npm install @lemonadejs/schedule
```

```js
import Schedule from '@lemonadejs/schedule';
import '@lemonadejs/schedule/style.css';
```

Three deployment forms, one component:

```js
html`<${Schedule} />`                       // by value (no registration)
setComponents({ Schedule });               // then <Schedule /> by name anywhere
createWebComponent(Schedule);              // <lm-schedule> in plain HTML/any framework
```

## Props

Every declared prop arrives as a **live state** — pass a value for a snapshot or a
state for a two-way live wire. Attribute strings are coerced to the declared type.

| Prop | Type | Default | Description |
|---|---|---|---|
| `data` | array | — | ScheduleEvent[] BY REFERENCE (mutate + touch()) |
| `value` | string | `''` | anchor date 'YYYY-MM-DD' (default: today) |
| `type` | string | `"week"` | 'week' | 'weekdays' | 'day' |
| `weekly` | boolean | `false` | abstract weekday columns (no dates) |
| `grid` | number | `15` | minutes per row (row height = grid px) |
| `snap` | number | `0` | create/resize step in minutes (0 = grid) |
| `overlap` | boolean | `false` | allow overlapping events (true staggers them) |
| `validrange` | array | — | visible hours, e.g. ['08:00','20:00'] (v5: validRange) |
| `readonlyrange` | array | — | disabled hours: ['a','b'] or [['a','b'],...] (v5: readOnlyRange) |
| `editor` | boolean | `true` | built-in event editor (v5 shipped it as lm-event) |
| `weekdays` | array | — | 7 weekday names (v5: document.dictionary) |

## Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

- `onchange` — (data) — any user/api change to the data
- `oncreate` — (events) — events added
- `onbeforecreate` — (events) — return false to cancel
- `onbeforeinsert` — (event) — drag-create template; false cancels, object replaces
- `onupdate` — (record, oldValue, newValue) — alias of onchangeevent
- `onchangeevent` — (record, oldValue, newValue) — v5 name, after a committed update
- `onbeforechange` — ({ action, ... }) — return false to cancel
- `onbeforechangeevent` — (record, oldValue, newValue) — return false to cancel the update
- `onbeforedrag` — ({ kind, record }) — false cancels the gesture at its start
- `ondelete` — (record) — per removed event
- `ondblclick` — (record)
- `onedition` — (record) — editor moment (dblclick / after drag-create)
- `onerror` — (message)

## API

```js
import { ref } from 'lemonadejs';
const schedule = ref();
html`<${Schedule} ref="${schedule}" />`;
// schedule.current.addEvents(...)  ·  schedule.current.updateEvent(...)  ·  schedule.current.deleteEvents(...)  ·  schedule.current.getData(...)  ·  schedule.current.setData(...)  ·  schedule.current.getEvent(...)  ·  schedule.current.getSelected(...)  ·  schedule.current.resetSelection(...)  ·  schedule.current.setRange(...)  ·  schedule.current.setReadOnly(...)  ·  schedule.current.undo(...)  ·  schedule.current.redo(...)  ·  schedule.current.next(...)  ·  schedule.current.prev(...)  ·  schedule.current.today(...)  ·  schedule.current.openEditor(...)  ·  schedule.current.refresh(...)
```

- `addEvents()`
- `updateEvent()`
- `deleteEvents()`
- `getData()`
- `setData()`
- `getEvent()`
- `getSelected()`
- `resetSelection()`
- `setRange()`
- `setReadOnly()`
- `undo()`
- `redo()`
- `next()`
- `prev()`
- `today()`
- `openEditor()`
- `refresh()`

## Styling

All classes follow the `lm-schedule-*` convention; visual variants are `data-*`
attributes on the root. Override freely — there is no styling engine to fight.

## Contract

The machine-readable schema ships with the package:

```js
import contract from '@lemonadejs/schedule/contract.json';
```

`verify.json` carries the conformance proof produced by `verify(Schedule)`.

Looking for the v5 plugin? See the [archived v5 documentation](/docs/v5/plugins/).