timeline-scheduler
v0.1.0
Published
Framework-agnostic Web Component timeline scheduler with drag & drop
Maintainers
Readme
timeline-scheduler
A framework-agnostic Web Component for scheduling and visualising events across multiple resources on a daily timeline.

- Zero dependencies — built with Lit, works in any framework or plain HTML
- Drag & drop — move events across resources and time with snapping
- Resize — drag event edges to adjust duration
- Hold to create — press and hold on empty space to draw a new event
- Navigation — built-in prev/next day buttons with date picker and zoom controls
- Overlap handling — overlapping events are automatically stacked in sub-rows
- Multi-day events — events spanning midnight are clipped and shown on each day
- Keyboard accessible — Tab between items, arrow keys to move them
- Customisable — CSS custom properties for theming, custom item renderer, and full event hooks
- TypeScript — fully typed
Install
npm install timeline-schedulerQuick start
<script type="module">
import "timeline-scheduler";
</script>
<timeline-scheduler id="tl"></timeline-scheduler>const tl = document.getElementById("tl");
tl.resources = [
{ id: "r1", name: "Alice Johnson", avatar: "https://example.com/alice.jpg" },
{ id: "r2", name: "Bob Smith" },
];
tl.items = [
{
id: "i1",
resourceId: "r1",
name: "Team sync",
color: "#3b82f6",
start: new Date("2024-04-13T09:00:00"),
end: new Date("2024-04-13T10:00:00"),
description: "Weekly check-in",
},
];
tl.date = new Date("2024-04-13");
tl.showNav = true;
tl.draggable = true;
tl.resizable = true;
tl.creatable = true;
// Keep data in sync after drag / resize
tl.addEventListener("change", (e) => {
tl.items = e.detail.items;
});
// Navigate to the new date when the user clicks prev/next
tl.addEventListener("date-change", (e) => {
tl.date = e.detail.date;
});React
import "timeline-scheduler";
import type { TimelineScheduler } from "timeline-scheduler";
declare global {
namespace JSX {
interface IntrinsicElements {
"timeline-scheduler": React.DetailedHTMLProps<
React.HTMLAttributes<TimelineScheduler>,
TimelineScheduler
>;
}
}
}
function App() {
const ref = useRef<TimelineScheduler>(null);
useEffect(() => {
const tl = ref.current!;
tl.resources = [...];
tl.items = [...];
tl.showNav = true;
tl.draggable = true;
tl.addEventListener("change", (e: Event) => {
tl.items = (e as CustomEvent).detail.items;
});
}, []);
return <timeline-scheduler ref={ref} style={{ maxHeight: "600px" }} />;
}Features
Navigation bar
tl.showNav = true;Shows previous/next day buttons, a clickable date label that opens the native date picker, and zoom controls. Control individual parts:
tl.showDateNav = false; // hide prev/next + date picker
tl.showZoomControls = false; // hide zoom buttons
// nav bar auto-hides when both are falseZoom
tl.zoom = 2; // 1 (default), 2, or 4At zoom > 1 the timeline scrolls horizontally while resource labels and the time header stay sticky.
Hold to create
tl.creatable = true;
tl.addEventListener("item-create", (e) => {
const { resourceId, start, end } = e.detail;
tl.items = [
...tl.items,
{
id: crypto.randomUUID(),
resourceId,
name: "New event",
color: "#64748b",
start,
end,
},
];
});Press and hold on empty space for ~400 ms, then drag to set the duration.
Custom item renderer
Replace the default name + time display with your own content:
tl.renderItem = (item) => {
const el = document.createElement("span");
el.textContent = `★ ${item.name}`;
el.style.fontWeight = "600";
return el;
};
tl.renderItem = null; // reset to defaultResize constraints
tl.minDurationMinutes = 30; // can't resize shorter than 30 min
tl.maxDurationMinutes = 240; // can't resize longer than 4 hoursMulti-day items
Items whose start and end span multiple days are automatically clipped to the visible day. An event from April 12 at 15:00 to April 13 at 09:00 will show as 15:00–24:00 on the 12th and 00:00–09:00 on the 13th.
Keyboard navigation
| Key | Action |
| -------------------------- | ----------------------------------- |
| ArrowLeft / ArrowRight | Move focused item one snap interval |
| Tab / Shift+Tab | Move focus between items |
Built-in context menu
Right-clicking an item opens a context menu with Edit, Delete, and Close. Edit opens a modal to change the name, resource, and times. Disable with editable = false to handle it yourself:
tl.editable = false;
tl.addEventListener("item-contextmenu", (e) => {
const { item, x, y } = e.detail;
// show your own menu at (x, y)
});Theming
timeline-scheduler {
max-height: 600px;
--tl-resource-col-width: 200px;
--tl-font-family: "Inter", sans-serif;
}Dark mode:
timeline-scheduler {
--tl-bg: #0f172a;
--tl-header-bg: #1e293b;
--tl-resource-border-color: #334155;
--tl-grid-line-color: #334155;
--tl-text-color: #f1f5f9;
--tl-time-label-color: #64748b;
}Reference
Properties
Data
| Property | Type | Default | Description |
| ----------- | ---------------- | ------- | ----------------------------------------------- |
| resources | Resource[] | [] | Rows to display |
| items | TimelineItem[] | [] | Events to render |
| date | Date | today | The day shown. Items are filtered to this date. |
Time range
| Property | Type | Default | Description |
| ------------- | -------- | ------- | ------------------------------------------- |
| startHour | number | 0 | First visible hour (0–23) |
| endHour | number | 24 | Last visible hour (1–24) |
| snapMinutes | number | 15 | Snap interval in minutes during drag/resize |
Interaction
| Property | Type | Default | Description |
| -------------------- | --------- | ------- | ------------------------------------------------------- |
| draggable | boolean | true | Allow drag & drop |
| resizable | boolean | false | Allow resizing by dragging edges |
| readonly | boolean | false | Disable all interaction |
| creatable | boolean | false | Allow hold-to-create on empty space |
| editable | boolean | true | Allow built-in edit modal on double-click / right-click |
| minDurationMinutes | number | 0 | Minimum duration during resize (0 = no limit) |
| maxDurationMinutes | number | 0 | Maximum duration during resize (0 = no limit) |
Display
| Property | Type | Default | Description |
| ------------------ | ------------------------------------------- | ------- | ----------------------------------------- |
| showNav | boolean | false | Show the navigation bar |
| showDateNav | boolean | true | Show prev/next + date picker in nav bar |
| showZoomControls | boolean | true | Show zoom buttons in nav bar |
| zoom | number | 1 | Horizontal zoom factor (1, 2, or 4) |
| showTime | boolean | true | Show start–end time inside event blocks |
| showAvatar | boolean | true | Show avatar / initials in resource column |
| showEventCount | boolean | true | Show event count below resource name |
| showTooltip | boolean | true | Show hover tooltip on events |
| showNowLine | boolean | true | Show current-time indicator line |
| renderItem | ((item: TimelineItem) => unknown) \| null | null | Custom render function for event content |
Events
| Event | Detail | Fires when |
| ------------------ | ------------------------------------ | ------------------------------------------------------------- |
| change | { items: TimelineItem[] } | An item is moved or resized. Always write back to tl.items. |
| item-click | { item } | An item is clicked |
| item-dblclick | { item } | An item is double-clicked |
| item-hover | { item, type: 'enter' \| 'leave' } | Pointer enters or leaves an item |
| item-contextmenu | { item, x, y } | An item is right-clicked |
| item-dragstart | { item } | Drag begins |
| item-dragend | { item, resourceId, start, end } | Drag released — final position |
| item-resizestart | { item } | Resize begins |
| item-resizeend | { item, start, end } | Resize released — final times |
| item-create | { resourceId, start, end } | New item created via hold-to-create |
| date-change | { date } | User navigated to a different day |
CSS custom properties
| Property | Default | Description |
| ---------------------------- | ------------ | ----------------------------------------- |
| --tl-font-family | sans-serif | Font used throughout the component |
| --tl-bg | #ffffff | Background color |
| --tl-header-bg | #f8fafc | Time header and nav bar background |
| --tl-resource-col-width | 160px | Width of the resource label column |
| --tl-resource-border-color | #e2e8f0 | Border between resource rows |
| --tl-grid-line-color | #e2e8f0 | Hour grid lines |
| --tl-text-color | #1e293b | Primary text color |
| --tl-time-label-color | #94a3b8 | Hour labels in the header |
| --tl-item-radius | 4px | Border radius of event blocks |
| --tl-item-text-color | #ffffff | Text color inside event blocks |
| --tl-item-opacity-dragging | 0.4 | Opacity of the source item while dragging |
| --tl-focus-color | #3b82f6 | Focus outline color |
| --tl-now-line-color | #ef4444 | Current-time indicator color |
| --tl-create-ghost-color | #3b82f6 | Hold-to-create ghost block color |
| --tl-tooltip-bg | #1e293b | Tooltip background |
| --tl-tooltip-color | #ffffff | Tooltip text color |
TypeScript types
import type {
Resource,
TimelineItem,
ChangeDetail,
ItemClickDetail,
ItemDblClickDetail,
ItemHoverDetail,
ItemContextMenuDetail,
ItemDragStartDetail,
ItemDragEndDetail,
ItemResizeStartDetail,
ItemResizeEndDetail,
ItemCreateDetail,
DateChangeDetail,
} from "timeline-scheduler";
interface Resource {
id: string;
name: string;
avatar?: string; // URL — falls back to coloured initials
}
interface TimelineItem {
id: string;
resourceId: string;
name: string;
color: string; // any valid CSS color
start: Date;
end: Date;
description?: string; // shown in the hover tooltip
}Development
npm install
npm run dev # playground at localhost:5173
npm test # run tests
npm run build # build dist/License
MIT
