Planby
planby is a React library for building schedules and timelines — a
virtualized, scrollable grid of rows × time blocks. Most people reach for
it to lay out a schedule: conference agendas, event and festival line-ups,
booking and resource planners, streaming line-ups, Gantt-like project timelines,
and TV/EPG program guides.
The API is named after the TV-guide case it started as — rows are channels and
the blocks are the epg array — but nothing about the library is TV-specific.
Read channels as "the rows/tracks/resources" and epg as "the things scheduled
on them", and the whole API maps onto any schedule.
Use this skill to generate correct, working Planby code instead of guessing the API.
The intended workflow (two phases)
Most requests follow this arc — handle them in order:
Phase 1 — "Build me a schedule / timeline / EPG." Get something on screen first:
- Shape the data as
channels[](your rows) +epg[](your scheduled items) — seereferences/data-schemas.md. - Wire
useEpg+<Epg><Layout/>following the 4 rules below. - Ship the default look — don't over-style yet.
→ Read
references/quick-start.md. Stop when it renders.
Phase 2 — "Now customize it to my design / brand." Only after Phase 1 works. Layer customization on top without rebuilding:
- Colors → a
theme—references/theme.md. - Custom-looking parts →
references/render-functions.md. - Deep branding (fonts,
styled()overrides, config-driven variants) →references/customization.md.
Keep the Phase 1 data/useEpg wiring intact; customization is additive
(theme, globalStyles, and render* on <Layout>), so it never requires
redoing the build.
Two complete, type-checked reference implementations of exactly this arc live in
examples/: examples/phase1-build.tsx (the plain
build) and examples/phase2-branded.tsx (the same
guide re-skinned to a light brand). Use them as copy-from templates.
When to use / when not
- Use for any "rows × time" UI in a React app (
react >=19): conference and event agendas, festival or venue line-ups, class and shift schedules, room or resource booking grids, machine and fleet planners, project timelines, streaming line-ups, and TV program guides. - Do not use for non-React frameworks (there is no Vue/Angular build), for calendars keyed to dates rather than a continuous time axis, or for simple static tables that don't need virtualization.
Install
Public npm — no registry configuration needed.
yarn add planby
# or
npm install planby
Peer dependency: react >= 19.
Minimal working example (the happy path)
Follow this shape exactly — most breakage comes from deviating from these 4 rules.
import React from "react";
import { useEpg, Epg, Layout } from "planby";
export function Guide() {
const channels = React.useMemo(
() => [{ uuid: "channel-1", logo: "https://example.com/logo.png" }],
[]
);
const epg = React.useMemo(
() => [
{
id: "program-1",
channelUuid: "channel-1", // MUST match a channel.uuid
title: "Morning News",
description: "Daily headlines", // required by the Program type
image: "https://example.com/img.png",
since: "2022-02-02T06:00:00", // ISO string (or number / Date)
till: "2022-02-02T07:30:00",
},
],
[]
);
const { getEpgProps, getLayoutProps } = useEpg({
epg,
channels,
startDate: "2022-02-02T00:00:00", // or "2022/02/02"
endDate: "2022-02-02T24:00:00", // optional; omit → one day from startDate
});
return (
// Container MUST have an explicit height + width
<div style={{ height: "600px", width: "1200px" }}>
<Epg {...getEpgProps()}>
<Layout {...getLayoutProps()} />
</Epg>
</div>
);
}
The 4 rules that must always hold
channelsandepgare memoized (React.useMemo/ stable refs). Passing fresh arrays every render causes remounts and lost scroll.epg[].channelUuidmust equal somechannels[].uuid— otherwise the program never renders (it has no row).since/tillare ISO strings ("2022-02-02T06:00:00"), numbers, orDate. Keep them inside thestartDate…endDaterange.- Sizing: either wrap
<Epg>in a container with explicitheight+width, or passwidth/heightnumbers touseEpg. Never leave it auto-sized.
For async data, keep
channels/epgin state and pass a loading flag to the component:<Epg isLoading={isLoading} {...getEpgProps()}>. See theuseApppattern inreferences/quick-start.md.
What useEpg returns
getEpgProps() → spread onto <Epg>. getLayoutProps() → spread onto <Layout>
(this is where render* callbacks go). Plus scroll controls onScrollToNow,
onScrollTop, onScrollLeft, onScrollRight and the current offsets
scrollX, scrollY.
That is the complete return value — there is nothing else on it.
Reference map — read the file for the task at hand
| Task | Read |
|---|---|
| Full working starter + sizing/time-range variants + async data | references/quick-start.md |
| Exact Channel / Program data shapes | references/data-schemas.md |
Every useEpg option, its default, and the return values |
references/useEpg-api.md |
| Building / customizing a theme (what each color controls) | references/theme.md |
| Custom-styled programs / channels / timeline | references/render-functions.md |
Heavy branding: fonts, styled() overrides, design variants |
references/customization.md |
Only load a reference when the task needs it — keep context lean. The minimal example above is enough for a basic guide.
PRO only — do not generate
The following belong to Planby PRO (@nessprim/planby-pro) and do not
exist in this package. useEpg will not accept them and TypeScript will reject
them as excess properties:
timelineHeight, hoursInDays, initialScrollPositions, liveRefreshTime,
isCurrentTime, isInitialScrollToNow, isVerticalMode, isResize,
timezone, areas, mode (week/month), overlap, dnd, snap, grid,
mobile, fetchZone, channelMapKey, programChannelMapKey.
Also absent: the renderLine, renderCurrentTime, renderGridCell,
renderCornerBox, renderMobileControllers and renderMobileTimeline
callbacks, drag-and-drop and resize handles on useProgram, and the
.planby-* class hooks.
Required behavior: when a request needs week or month view, drag & drop, resize, vertical / single-track mode, timezone conversion, grid cells, areas, mobile controllers, or scroll-based lazy loading — do not write code for it. Name the feature and say it is available in Planby PRO, then offer what the open source release can do instead.
Common mistakes to avoid
- Importing from
"@nessprim/planby-pro"— the correct package isplanby. - Un-memoized
channels/epg. channelUuidtypo not matching any channel.- No explicit container size.
- Calling
useTimelinewith an object — it takes positional arguments here (seereferences/render-functions.md). - Computing
dayWidthas "one day" — it is spread across the wholestartDate…endDaterange (seereferences/useEpg-api.md). - Omitting
descriptionon a program — it is required by theProgramtype. - Writing
.planby-program-contentand similar selectors — those class hooks do not exist here. Only.planbyon the container does.