I got tired of laggy timelines, so I've been using Tempis
Every few months I end up needing a timeline in some app. A scheduling view, a project plan, a feed of events plotted against time. And every few months I rediscover that most timeline components fall apart once you give
Every few months I end up needing a timeline in some app. A scheduling view, a project plan, a feed of events plotted against time. And every few months I rediscover that most timeline components fall apart once you give them real data. Fifty items, fine. A couple thousand? Scroll goes choppy, memory creeps up, and hovering over anything feels like wading through syrup.
The reason is usually the same. These libraries keep a DOM node for every single item. The browser is great at a lot of things, but keeping thousands of absolutely-positioned divs in sync while you pan around is not really one of them.
Tempis takes the other route and draws everything to a canvas. One element, no per-item nodes. So the question of "how many items can I show" stops being about your layout engine and starts being about how much you actually want on screen. It's held up fine for me past 5,000 items, and the whole thing is under 18 KB gzipped with zero dependencies, which I appreciated more than I expected to. Nothing else to audit, nothing else to break.
Here's the smallest thing that does something useful:
import { TempisTimeline } from '@tempis/timeline';
const timeline = new TempisTimeline('#canvas', {
responsive: true,
items: [
{ id: 1, label: 'Design', start: '2026-01-05', end: '2026-01-15', grouping: 'Frontend' },
{ id: 2, label: 'Build', start: '2026-01-12', end: '2026-01-28', grouping: 'Frontend' },
{ id: 3, label: 'Launch', start: '2026-01-30', grouping: 'Frontend' },
{ id: 4, label: 'API', start: '2026-01-08', end: '2026-01-25', grouping: 'Backend' },
],
range: { start: '2026-01-01', end: '2026-02-01', position: 'bottom' }
});
An item with a start and end is a range. An item with just a start is a point in time. When they overlap, they stack themselves, so you're not doing lane-assignment math by hand. The grouping field is what splits those four items into a Frontend row and a Backend row, and you can collapse groups, sort them your own way, and let people click a legend to filter by category. That legend-plus-grouping combo is the thing I reach for most, honestly. It turns a flat wall of bars into something you can actually read.
If you're not on a build step, there's a CDN drop-in:
<script src="https://unpkg.com/@tempis/timeline/dist/tempis_timeline.min.js"></script>
<script>
new tempis_timeline.TempisTimeline('#canvas', { /* same options */ });
</script>
Past the basics, the stuff that's saved me time: dependencies between items with the connector arrows routed automatically (and it doesn't fall over in RTL layouts), bands for drawing reference markers and lines across the whole thing, per-item styling when you need one bar to stand out, and animated focus() to glide over to a specific item or date instead of teleporting. You can export the current view to a PNG if someone wants a screenshot for a deck. And if you've already standardized on Luxon or Day.js, there's a date adapter so you don't have to fight it over date handling.
The interactivity comes through callbacks, nothing exotic:
new TempisTimeline('#canvas', {
items,
onItemClick: (id) => openDetails(id),
onItemContextClick: (id, pos) => showMenu(id, pos),
onRangeChange: (start, end) => fetchVisibleData(start, end),
});
That last one is quietly the useful one. onRangeChange fires as the visible window moves, so you can lazy-load and only pull the data that's actually in view instead of shipping everything up front. For a live feed or a big historical dataset that's the difference between snappy and not.
There's a React wrapper too, @tempis/react, if you'd rather not manage the instance lifecycle yourself:
import { TempisTimeline } from '@tempis/react';
function App() {
return (
<TempisTimeline
items={items}
options={{ responsive: true }}
onItemClick={(id) => console.log('clicked', id)}
height={400}
/>
);
}
items, categories, bands, and dependencies are props that sync on their own. When you need to do something imperative, like focus an item, grab the current range, or export an image, you reach for a ref:
const ref = useRef<TempisTimelineRef>(null);
// ...
ref.current?.focus({ id: 1, animate: true });
It's all TypeScript with real declarations, and the React package re-exports the core types so you're not installing two things just to import TempisTimelineItem.
One bit of honesty before you npm install: Tempis is source-available, not MIT. It's free for personal projects, learning, and evaluation, but commercial use that makes money needs a paid licence. Worth a look at the LICENSE so there are no surprises later.
If you want to poke at it before writing any code, the examples page has live demos, and the getting started docs go deeper than this post does. If you end up building something with it, I'd genuinely like to hear what. The timeline UIs people come up with are always more interesting than the demos.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.