JavaScript Timeline

@lemonadejs/timeline · ✓ 32 contract checks · framework-agnostic · zero dependencies

<Timeline /> — full behavioral parity with the v5 plugin.

The v5 model, ported faithfully:

  • a feed of events ({ title, subtitle, description, date, borderColor, borderStyle, tags }) sorted by date (order asc | desc), each item showing a formatted day bullet (v5 jSuites mask, default ‘dddd, dd’ — or ‘dd mmm yyyy’ in monthly mode)
  • type=“monthly”: only the viewed month’s events show, plus a header with the year / month name and prev / next month navigation (December/January roll the year over)
  • items can also come from element children (v5 extractFromHtml: title from textContent or title=, data-date, data-color, data-style)
  • per-item borders through borderColor / borderStyle, per-tag colors, tag onclick(e, tag) handlers (clickable tags are keyboard-operable: role=“button”, tabindex, Enter/Space)
  • editable: an edit button per item firing onedition(record)
  • url: data fetched remotely ({ result: […] } or a plain array); remote + monthly asks the server per month (?year&month&asc) and suppresses repeated consecutive en-GB day labels (v5 dateSignature)
  • align (left | right | top | bottom, invalid values fall back to left as v5), width/height in px, message when the feed is empty

v5 → v6 mapping: value → date (the viewed month anchor — “value” is reserved for form semantics in v6); controls defaults true (the header only ever shows in monthly mode, so the visual default is identical to v5’s controls = type === ‘monthly’); onupdate(records) unchanged; self.next/self.prev → api { next, prev }. The empty message is a real .lm-timeline-message element (v5 used :empty::before, which cannot see v6’s slot markers). Border CSS vars are scoped: —lm-timeline-border-*. Fetched data is kept internally instead of overwriting the data prop (v5 wrote self.data); assigning data later replaces it, exactly v5.

Example

live
import { html } from 'lemonadejs';
import Timeline from '@lemonadejs/timeline';

const releases = [
    { title: 'Kickoff', subtitle: 'Scope agreed, repository created', date: '2026-06-02T09:00:00',
        tags: [{ title: 'milestone', color: '#bfdbfe' }] },
    { title: 'First prototype', subtitle: 'The reactive core renders its first view', date: '2026-06-05T15:00:00',
        borderColor: '#1f64e1', tags: [{ title: 'build', color: '#bbf7d0' }] },
    { title: 'Beta release', subtitle: 'Out the door to the early adopters', date: '2026-06-09T11:00:00',
        borderColor: '#16a34a', borderStyle: 'dashed' },
    { title: 'Version 6.0', subtitle: 'Public launch', date: '2026-06-24T10:00:00',
        borderColor: '#9333ea', tags: [{ title: 'release', color: '#e9d5ff' }] },
];

const App = (props, { state }) => {
    const order = state('asc');

    return html`<div>
        <button onclick="${() => (order.value = order.value === 'asc' ? 'desc' : 'asc')}">
            Order: ${order}
        </button>
        <${Timeline} data="${releases}" order="${order}" />
    </div>`;
};

Installation

npm install @lemonadejs/timeline
import Timeline from '@lemonadejs/timeline';
import '@lemonadejs/timeline/style.css';

The icons come from Google Material Symbols. Load the font once per page:

<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">

Three deployment forms, one component:

html`<${Timeline} />`                       // by value (no registration)
setComponents({ Timeline });               // then <Timeline /> by name anywhere
createWebComponent(Timeline);              // <lm-timeline> 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.

PropTypeDefaultDescription
dataarrayTimelineItem[]
typestring''’monthly’ filters by the viewed month + shows controls
datestring''viewed month anchor (v5: value) — defaults to today
formatstring''day mask (v5 defaults: monthly ‘dd mmm yyyy’, feed ‘dddd, dd’)
messagestring"No records found"text shown when the feed is empty
orderstring"asc"asc
alignstring"left"left
positionstring''v5 pass-through → data-mode on the feed
controlsbooleantruemonth navigation header (visible in monthly mode only, as v5)
editablebooleanfalseshows the per-item edit button
remotebooleanfalsewith url + monthly: server-side month queries
urlstring''fetch the data remotely
widthnumber0px, 0 = natural
heightnumber0px, 0 = natural

Events

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

  • onupdate — (records) after every recompute
  • onedition — (record) when an item’s edit button is clicked

API

import { ref } from 'lemonadejs';
const timeline = ref();
html`<${Timeline} ref="${timeline}" />`;
// timeline.current.next(...)  ·  timeline.current.prev(...)
  • next()
  • prev()

Styling

All classes follow the lm-timeline-* 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:

import contract from '@lemonadejs/timeline/contract.json';

verify.json carries the conformance proof produced by verify(Timeline).

Looking for the v5 plugin? See the archived v5 documentation.