karolkozer/planby

TypeScript

1,721

150 commits

updated Aug 23, 2026

See the code

README

πŸ€– New: build Planby with AI one command wires the official Planby skill into your coding assistant πŸ€– πŸ‘‰See how πŸ‘ˆ

Description

Planby is a React based component for a quick implementation of Epg, schedules, live streaming, music events, timelines and many more ideas. It uses a custom virtual view which allows you to operate on a really big number of data. The component has a simple API that you can easily integrate with other third party UI libraries. The component theme is customised to the needs of the application design.

A Planby conference schedule: five stage rows from 9:00 to 17:00, light theme, session cards with speaker names and durations

πŸ”₯ Planby Native β€” React Native beta

Planby is coming to React Native, and the beta is open for free testing. It is a separate package from this one; the web component documented here is unaffected.

πŸ‘‰ Sign up for the Planby Native beta

Codesandbox example

Live example - Codesandbox

Live example - Typescript Codesandbox

Live example - website with control panel

Testimonials

πŸš€ Become a Sponsor! πŸš€

Become a sponsor, support, and help us in continuing our development. -> Opencollective

Getting Started

Installation

  • yarn
yarn add planby
  • npm
npm install planby

Using Planby with AI assistants

AI assistants know React, but they don't know Planby. Left to guess, they invent useEpg options that don't exist, forget that channels and epg have to be memoized, and reach for features that are only in Planby PRO.

Planby ships an agent skill β€” a compact description of the API, verified against the source β€” that your assistant reads before it writes any code.

Setup

npm install planby
npx planby init-ai

One command wires the skill into every agent you use:

AgentWhat it writes
Claude Code / Agent SDK.claude/skills/planby
Cursor.cursor/rules/planby.mdc
Windsurf.windsurf/rules/planby.md
Codex, Gemini CLI, Antigravity, …a section in AGENTS.md

Each file is a thin pointer to the skill bundled inside node_modules/planby, so the guidance always matches the version you actually installed. Re-run it after upgrading β€” it is safe to run repeatedly.

Using ChatGPT or another tool without a rules file? Paste node_modules/planby/skills/planby/SKILL.md into the conversation.

Example: ask for a schedule

"Build me a conference schedule with five stages from 9:00 to 17:00."

import React from 'react';
import { useEpg, Epg, Layout } from 'planby';

const channels = [
  { uuid: 'stage-a', logo: '/stage-a.png' },
  { uuid: 'stage-b', logo: '/stage-b.png' },
  { uuid: 'stage-c', logo: '/stage-c.png' },
  { uuid: 'stage-d', logo: '/stage-d.png' },
  { uuid: 'stage-e', logo: '/stage-e.png' },
];

const epg = [
  {
    id: 'p-1',
    channelUuid: 'stage-a', // must match a channels[].uuid
    title: 'Opening Keynote',
    description: 'Welcome & vision', // required by the Program type
    image: '/keynote.png',
    since: '2022-02-02T09:00:00',
    till: '2022-02-02T10:00:00',
  },
  {
    id: 'p-2',
    channelUuid: 'stage-b',
    title: 'Workshop: Data',
    description: 'Hands-on session',
    image: '/workshop.png',
    since: '2022-02-02T10:30:00',
    till: '2022-02-02T12:00:00',
  },
  {
    id: 'p-3',
    channelUuid: 'stage-e',
    title: 'Closing Panel',
    description: 'Q&A with the speakers',
    image: '/panel.png',
    since: '2022-02-02T16:00:00',
    till: '2022-02-02T17:00:00',
  },
];

export function Schedule() {
  const channelsData = React.useMemo(() => channels, []);
  const epgData = React.useMemo(() => epg, []);

  const { getEpgProps, getLayoutProps } = useEpg({
    channels: channelsData,
    epg: epgData,
    startDate: '2022-02-02T09:00:00',
    endDate: '2022-02-02T17:00:00',
    dayWidth: 2400, // 8h range Γ— 300px per hour
  });

  return (
    <div style={{ height: '600px', width: '1200px' }}>
      <Epg {...getEpgProps()}>
        <Layout {...getLayoutProps()} />
      </Epg>
    </div>
  );
}

Example: ask for your branding

"Now restyle it to match our light theme, add shadows, and show each session’s duration."

The assistant layers customization on top without rebuilding the data wiring β€” a full theme, and globalStyles for the font and the shadow around the whole guide, plus a custom renderProgram built on the useProgram hook:

// `.planby` is the container of the whole schedule β€” that is where a shadow
// around the guide belongs. It is the one class hook the library exposes.
const globalStyles = `
  @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");

  .planby {
    font-family: "Inter", system-ui, sans-serif;
    border-radius: 12px;
    box-shadow: 0 10px 30px rgb(15 23 42 / 15%);
    overflow: hidden;
  }
`;

const formatDuration = (since, till) => {
  const minutes = Math.round((new Date(till) - new Date(since)) / 60000);
  const h = Math.floor(minutes / 60);
  const m = minutes % 60;
  return h ? (m ? `${h}h ${m}m` : `${h}h`) : `${m}m`;
};

const Program = ({ program, ...rest }) => {
  const { styles, formatTime, isLive } = useProgram({ program, ...rest });
  const { title, since, till } = program.data;

  return (
    <ProgramBox width={styles.width} style={styles.position}>
      <ProgramContent width={styles.width} isLive={isLive}>
        <ProgramStack>
          <ProgramTitle>{title}</ProgramTitle>
          <ProgramText>
            {formatTime(since)} - {formatTime(till)} Β·{' '}
            {formatDuration(since, till)}
          </ProgramText>
        </ProgramStack>
      </ProgramContent>
    </ProgramBox>
  );
};

// globalStyles goes on useEpg, alongside your theme:
useEpg({ channels, epg, startDate, endDate, theme, globalStyles });

<Layout
  {...getLayoutProps()}
  renderProgram={({ program, ...rest }) => (
    <Program key={program.data.id} program={program} {...rest} />
  )}
/>;
A Planby conference schedule: five stage rows from 9:00 to 17:00, light theme, session cards with speaker names and durations

Both prompts above, run end to end β€” five stages across a 9:00–17:00 range, re-skinned to a light theme with a shadow around the guide and a duration on every session card.

Other things worth asking for

  • "Switch the guide to 12-hour time and right-to-left."
  • "Make the program cards match this Figma β€” rounded corners, our brand hover."
  • "Load the programs from our API and show the spinner while it fetches."
  • "Why is my guide zoomed in so far?" β€” a classic: dayWidth is spread across the whole startDate…endDate range, not per day.
  • "Nothing renders." β€” usually an un-memoized array, a channelUuid that matches no channel, or a container with no explicit height.

What the skill covers

TopicReference
Starter, sizing, async datareferences/quick-start.md
Channel & Program schemasreferences/data-schemas.md
Every useEpg option and return valuereferences/useEpg-api.md
Theme β€” and what each color controlsreferences/theme.md
Custom program / channel / timeline renderingreferences/render-functions.md
Branding, fonts, styled() overridesreferences/customization.md

Two runnable, type-checked examples ship alongside them in skills/planby/examples/.

What it will not do

The skill knows which features belong to Planby PRO and will tell you instead of generating code that cannot work: vertical / single-track mode, drag-and-drop, resizing, week and month calendar views, timezone conversion, grid cells, highlighted areas, mobile controllers, and scroll-based lazy loading.

Usage

import { useEpg, Epg, Layout } from 'planby';

const channels = React.useMemo(
  () => [
    {
      logo: 'https://via.placeholder.com',
      uuid: '10339a4b-7c48-40ab-abad-f3bcaf95d9fa',
      ...
    },
  ],
  []
);

const epg = React.useMemo(
  () => [
    {
      channelUuid: '30f5ff1c-1346-480a-8047-a999dd908c1e',
      description:
        'Ut anim nisi consequat minim deserunt...',
      id: 'b67ccaa3-3dd2-4121-8256-33dbddc7f0e6',
      image: 'https://via.placeholder.com',
      since: "2022-02-02T23:50:00",
      till: "2022-02-02T00:55:00",
      title: 'Title',
      ...
    },
  ],
  []
);

const {
  getEpgProps,
  getLayoutProps,
  onScrollToNow,
  onScrollLeft,
  onScrollRight,
} = useEpg({
  epg,
  channels,
  startDate: '2022-02-02T00:00:00'
});

return (
  <div>
    <div style={{ height: '600px', width: '1200px' }}>
      <Epg {...getEpgProps()}>
        <Layout
          {...getLayoutProps()}
        />
      </Epg>
    </div>
  </div>
);

or

Custom width and height

const {
  getEpgProps,
  getLayoutProps,
  ...
} = useEpg({
  epg,
  channels,
 startDate: '2022/02/02', // or 2022-02-02T00:00:00
  width: 1200,
  height: 600
});

return (
  <div>
     <Epg {...getEpgProps()}>
        <Layout
          {...getLayoutProps()}
        />
      </Epg>
  </div>

or

Time range

const {
  getEpgProps,
  getLayoutProps,
  ...
} = useEpg({
  epg,
  channels,
  startDate: '2022-02-02T10:00:00',
  endDate: '2022-02-02T20:00:00',
  width: 1200,
  height: 600
});

return (
  <div>
     <Epg {...getEpgProps()}>
        <Layout
          {...getLayoutProps()}
        />
      </Epg>
  </div>

API

useEpg

Options

Available options in useEpg

PropertyTypeStatusDescriptionAccess
channelsarrayrequiredArray with channels data
epgarrayrequiredArray with EPG data
widthnumberoptionalEPG width
heightnumberoptionalEPG height
sidebarWidthnumberoptionalWidth of the sidebar with channels
timelineHeightnumberoptionalHeight of the timelinePRO
itemHeightnumberoptionalHeight of channels and programs in the EPG. Default value is 80
dayWidthnumberoptionalWidth of the day. Default value is 7200. Calculation to set up day width with own hour width value e.g., 24h * 300px (your custom hour width) = 7200px -> dayWidth
startDatestringoptionalDate format 2022/02/02 or 2022-02-02T00:00:00. You can set your own start time, e.g., 2022-02-02T10:00:00, 2022-02-02T14:00:00, etc. Full clock hours only
endDatestringoptionalDate format 2022-02-02T00:00:00, 2022-02-02T20:00:00, etc. Must be within the same 24-hour period as startDate. Full clock hours only. Scroll through multiple days and timeline mode is available only in PRO plan.PRO
hoursInDaysarrayoptionalSet start time and end time of each day in multiple days feature if your data for each day has some time spaces between items in the day.PRO
initialScrollPositionsobjectoptionalSet initial scroll position in Layout, e.g., initialScrollPositions: { top: 500, left: 800 }PRO
liveRefreshTimenumberoptionalLive refresh time of the events. Default value is 120 sec.PRO
isBaseTimeFormatbooleanoptionalConvert to 12-hour format, e.g., 2:00am, 4:00pm, etc. Default value is false.
isCurrentTimebooleanoptionalShow current time in Timeline. Default value is false.PRO
isInitialScrollToNowbooleanoptionalScroll to the current live element.PRO
isVerticalModebooleanoptionalShow Timeline in vertical view. Default value is false.PRO
isResizebooleanoptionalPossibility to resize the element.PRO
isSidebarbooleanoptionalShow/hide sidebar
isTimelinebooleanoptionalShow/hide timeline
isLinebooleanoptionalShow/hide line
isRTLbooleanoptionalChange direction to RTL or LTR. Default value is false.PRO
themeobjectoptionalObject with theme schema
timezoneobjectoptionalConvert and display data from UTC format to your own time zonePRO
areasarrayoptionalArea gives possibilities to add field ranges to the Timeline layout.PRO
modeobjectoptionalType values: day/week/month. Style values: default/modern Define the mode and style of the timeline. Default mode is day and style is defaultPRO
overlapobjectoptionalEnable the element overlaps in the layout. Mode values: stack/layer, layerOverlapLevel: numberPRO
drag and dropobjectoptionalDrag and move the element in the layout. Mode values: row/multi-rowsPRO
grid layoutobjectoptionalBackground grid on the layout. Mode hoverHighlight values: true/false, onGridItemClick: function with all the properties on clicked item gridPRO
channelMapKeystringoptionalThe Channel uuid attribute can be controlled by prop. Key map gives a possibilities to use specific prop from own data instead of needing to map to uuid in own dataPRO
programChannelMapKeystringoptionalThe Programs channelUuid attributes can be controlled by prop. Key map gives a possibilities to use a specific prop from own data instead of needing to map to channelUuid in your dataPRO
globalStylesstringoptionalInject custom global styles and font. Font weight: 400,500,600. Default font is "Inter"PRO

Note about width and height props

Without declaring the width and length properties, the component takes the dimensions of the parent element.

globalStyles

Inject own custom font and other global styles.

const globalStyles = `
  @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");

/* Available in PRO plan */
 .planby {
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica,
    Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";

  /* Layout */
  .planby-layout {}

  /* Line */
  .planby-line {}

  /* Current time */
  .planby-current-time {}
  .planby-current-content {}

  /* Channels */
  .planby-channels {}

  /* Channel */
  .planby-channel {}

  /* Program */
  .planby-program {}
  .planby-program-content {}
  .planby-program-flex {}
  .planby-program-stack {}
  .planby-program-title {}
  .planby-program-text {}

  /* Timeline */
  .planby-timeline-wrapper {}
  .planby-timeline-box {}
  .planby-timeline-time {}
  .planby-timeline-dividers {}
  .planby-timeline-wrapper {}
}
  
`;

Instance Properties

Properties returned from useEpg

PropertyTypeDescription
scrollYnumberCurrent scroll y value
scrollXnumberCurrent scroll x value
onScrollLeftfunction(value: number)Default value is 300
onScrollRightfunction(value: number)Default value is 300
onScrollToNowfunction()Scroll to current time/live programs
onScrollTopfunction(value: number)Default value is 300

Channel schema

PropertyTypeStatus
logostringrequired
uuidstringrequired

Epg schema

PropertyTypeStatusDescriptionAccess
channelUuidstringrequired
idstringrequired
imagestringrequired
sincestringrequired
tillstringrequired
titlestringrequired
fixedVisibilitybooleanoptionalThe element is always visible in the layout during the scroll eventsSponsors

Epg

Base props

Available props in Epg

PropertyTypeDescriptionStatus
isLoadingbooleanLoader stateoptional
loaderComponentLoader custom componentoptional

Layout

Base props

Available props in Layout.

PropertyTypeDescriptionStatusAccess
renderProgramfunction({ program: { data: object, position: object})data object contains all properties related to the program, position object includes all position stylesoptional
renderChannelfunction({ channel: { ..., position: object})channel object contains all properties related to the channel, position object includes all position stylesoptional
renderTimelinefunction({sidebarWidth: number})sidebarWidth value of the channel's sidebar widthoptional
renderLinefunction({styles: object})basic styles and position values for the custom live tracking LineoptionalSponsors
renderCurrentTimefunction({styles: object, isRTL: boolean, isBaseTimeFormat: boolean, time: string})basic styles values for the custom current timeoptionalSponsors

Render functions

You can use Plaby's style components to develop main features. Moreover, you can integrate with third party UI library eg. Chakra UI, Material UI etc or make custom styles.

renderProgram

Below is an example that allows you to render your custom Program component using Plaby's style components.

import {
  useEpg,
  Epg,
  Layout,
  ProgramBox,
  ProgramContent,
  ProgramFlex,
  ProgramStack,
  ProgramTitle,
  ProgramText,
  ProgramImage,
  useProgram,
  Program,
  ProgramItem
} from "planby";


const Item = ({ program,...rest }: ProgramItem) => {
  const { styles, formatTime, isLive, isMinWidth } = useProgram({ program,...rest });

  const { data } = program;
  const { image, title, since, till } = data;

  const sinceTime = formatTime(since);
  const tillTime = formatTime(till);

  return (
    <ProgramBox width={styles.width} style={styles.position}>
      <ProgramContent
        width={styles.width}
        isLive={isLive}
      >
        <ProgramFlex>
          {isLive && isMinWidth && <ProgramImage src={image} alt="Preview" />}
          <ProgramStack>
            <ProgramTitle>{title}</ProgramTitle>
            <ProgramText>
              {sinceTime} - {tillTime}
            </ProgramText>
          </ProgramStack>
        </ProgramFlex>
      </ProgramContent>
    </ProgramBox>
  );
};

function App() {

  ...

 const {
  getEpgProps,
  getLayoutProps,
} = useEpg({
  epg,
  channels,
  startDate: '2022/02/02', // or 2022-02-02T00:00:00
});

return (
  <div>
    <div style={{ height: '600px', width: '1200px' }}>
      <Epg {...getEpgProps()}>
        <Layout
            {...getLayoutProps()}
            renderProgram={({ program,...rest }) => (
              <Item key={program.data.id} program={program} {...rest} />
            )}
          />
      </Epg>
    </div>
  </div>
);
}

export default App;

renderProgram - 12 hours time format

Below is an example that allows you to render your custom Program component with 12 hours time format using Plaby's style components.

...
const Item = ({ program, ...rest }: ProgramItem) => {
  const {
    styles,
    formatTime,
    set12HoursTimeFormat,
    isLive,
    isMinWidth,
  } = useProgram({
    program,
    ...rest
  });

  const { data } = program;
  const { image, title, since, till } = data;

  const sinceTime = formatTime(since, set12HoursTimeFormat()).toLowerCase();
  const tillTime = formatTime(till, set12HoursTimeFormat()).toLowerCase();

  return (
    <ProgramBox width={styles.width} style={styles.position}>
      <ProgramContent
        width={styles.width}
        isLive={isLive}
      >
        <ProgramFlex>
          {isLive && isMinWidth && <ProgramImage src={image} alt="Preview" />}
          <ProgramStack>
            <ProgramTitle>{title}</ProgramTitle>
            <ProgramText>
              {sinceTime} - {tillTime}
            </ProgramText>
          </ProgramStack>
        </ProgramFlex>
      </ProgramContent>
    </ProgramBox>
  );
};

function App() {

  ...

 const {
  getEpgProps,
  getLayoutProps,
} = useEpg({
  epg,
  channels,
  isBaseTimeFormat: true,
  startDate: '2022/02/02', // or 2022-02-02T00:00:00
});

...
}

export default App;

renderProgram - RTL direction

Below is an example that allows you to render your custom Program component with RTL direction using Plaby's style components.

...
const Item = ({ program, ...rest }: ProgramItem) => {
  const {
    isRTL,
    isLive,
    isMinWidth,
    formatTime,
    styles,
    set12HoursTimeFormat,
    getRTLSinceTime,
    getRTLTillTime,
  } = useProgram({
    program,
    ...rest
  });

  const { data } = program;
  const { image, title, since, till } = data;

  const sinceTime = formatTime(
    getRTLSinceTime(since),
    set12HoursTimeFormat()
  ).toLowerCase();
  const tillTime = formatTime(
    getRTLTillTime(till),
    set12HoursTimeFormat()
  ).toLowerCase();

  return (
    <ProgramBox width={styles.width} style={styles.position}>
      <ProgramContent width={styles.width} isLive={isLive}>
        <ProgramFlex>
          {isLive && isMinWidth && <ProgramImage src={image} alt="Preview" />}
          <ProgramStack isRTL={isRTL}>
            <ProgramTitle>{title}</ProgramTitle>
            <ProgramText>
              {sinceTime} - {tillTime}
            </ProgramText>
          </ProgramStack>
        </ProgramFlex>
      </ProgramContent>
    </ProgramBox>
  );
};

function App() {

  ...

 const {
  getEpgProps,
  getLayoutProps,
} = useEpg({
  epg,
  channels,
  isBaseTimeFormat: true,
  startDate: '2022/02/02', // or 2022-02-02T00:00:00
});

...
}

export default App;

renderChannel

Below is an example that allows you to render your custom Channel component using Plaby's style components.

import { useEpg, Epg, Layout, ChannelBox, ChannelLogo, Channel } from 'planby';

interface ChannelItemProps {
  channel: Channel;
}

const ChannelItem = ({ channel }: ChannelItemProps) => {
  const { position, logo } = channel;
  return (
    <ChannelBox {...position}>
      <ChannelLogo
        onClick={() => console.log('channel', channel)}
        src={logo}
        alt="Logo"
      />
    </ChannelBox>
  );
};


function App() {

  ...

  const {
    getEpgProps,
    getLayoutProps,
  } = useEpg({
    epg,
    channels,
    startDate: '2022/02/02', // or 2022-02-02T00:00:00
  });

  return (
    <div>
      <div style={{ height: '600px', width: '1200px' }}>
        <Epg {...getEpgProps()}>
          <Layout
              {...getLayoutProps()}
              renderChannel={({ channel }) => (
              <ChannelItem key={channel.uuid} channel={channel} />
            )}
            />
        </Epg>
      </div>
    </div>
  );
}

renderTimeline

Below is an example that allows you to render your custom Timeline component using Plaby's style components.

import {
  TimelineWrapper,
  TimelineBox,
  TimelineTime,
  TimelineDivider,
  TimelineDividers,
  useTimeline,
} from 'planby';

interface TimelineProps {
  isBaseTimeFormat: boolean;
  isSidebar: boolean;
  dayWidth: number;
  hourWidth: number;
  numberOfHoursInDay: number;
  offsetStartHoursRange: number;
  sidebarWidth: number;
}

export function Timeline({
  isBaseTimeFormat,
  isSidebar,
  dayWidth,
  hourWidth,
  numberOfHoursInDay,
  offsetStartHoursRange,
  sidebarWidth,
}: TimelineProps) {
  const { time, dividers, formatTime } = useTimeline(
    numberOfHoursInDay,
    isBaseTimeFormat
  );

  const renderTime = (index: number) => (
    <TimelineBox key={index} width={hourWidth}>
      <TimelineTime>
        {formatTime(index + offsetStartHoursRange).toLowerCase()}
      </TimelineTime>
      <TimelineDividers>{renderDividers()}</TimelineDividers>
    </TimelineBox>
  );

  const renderDividers = () =>
    dividers.map((_, index) => (
      <TimelineDivider key={index} width={hourWidth} />
    ));

  return (
    <TimelineWrapper
      dayWidth={dayWidth}
      sidebarWidth={sidebarWidth}
      isSidebar={isSidebar}
    >
      {time.map((_, index) => renderTime(index))}
    </TimelineWrapper>
  );
}

function App() {

  ...

  const {
    getEpgProps,
    getLayoutProps,
  } = useEpg({
    epg,
    channels,
    startDate: '2022/02/02', // or 2022-02-02T00:00:00
  });

  return (
    <div>
      <div style={{ height: '600px', width: '1200px' }}>
        <Epg {...getEpgProps()}>
          <Layout
              {...getLayoutProps()}
              renderTimeline={(props) => <Timeline {...props} />}
            />
        </Epg>
      </div>
    </div>
  );
}

export default App;

renderTimeline - RTL direction

Below is an example that allows you to render your custom Timeline component using Plaby's style components.

import {
  TimelineWrapper,
  TimelineBox,
  TimelineTime,
  TimelineDivider,
  TimelineDividers,
  useTimeline,
} from 'planby';

interface TimelineProps {
  isRTL: boolean;
  isBaseTimeFormat: boolean;
  isSidebar: boolean;
  dayWidth: number;
  hourWidth: number;
  numberOfHoursInDay: number;
  offsetStartHoursRange: number;
  sidebarWidth: number;
}

export function Timeline({
  isRTL,
  isBaseTimeFormat,
  isSidebar,
  dayWidth,
  hourWidth,
  numberOfHoursInDay,
  offsetStartHoursRange,
  sidebarWidth,
}: TimelineProps) {
  const { time, dividers, formatTime } = useTimeline(
    numberOfHoursInDay,
    isBaseTimeFormat
  );

  const renderTime = (index: number) => (
    <TimelineBox key={index} width={hourWidth}>
      <TimelineTime isBaseTimeFormat={isBaseTimeFormat} isRTL={isRTL}>
        {formatTime(index + offsetStartHoursRange).toLowerCase()}
      </TimelineTime>
      <TimelineDividers>{renderDividers()}</TimelineDividers>
    </TimelineBox>
  );

 ...
}

Theme

Schema

Make your theme custom. Below is theme schema that you can pass as one of the options to useEpg hook.

const theme = {
  primary: {
    600: '#1a202c',
    900: '#171923',
  },
  grey: { 300: '#d1d1d1' },
  white: '#fff',
  green: {
    300: '#2C7A7B',
  },
  loader: {
    teal: '#5DDADB',
    purple: '#3437A2',
    pink: '#F78EB6',
    bg: '#171923db',
  },
  scrollbar: {
    border: '#ffffff',
    thumb: {
      bg: '#e1e1e1',
    },
  },
  gradient: {
    blue: {
      300: '#002eb3',
      600: '#002360',
      900: '#051937',
    },
  },
  text: {
    grey: {
      300: '#a0aec0',
      500: '#718096',
    },
  },
  timeline: {
    divider: {
      bg: '#718096',
    },
  },
};

All import options

import {
  Epg,
  Layout,
  ChannelBox,
  ChannelLogo,
  ProgramBox,
  ProgramContent,
  ProgramFlex,
  ProgramStack,
  ProgramTitle,
  ProgramText,
  ProgramImage,
  TimelineWrapper,
  TimelineBox,
  TimelineTime,
  TimelineDividers,
  useEpg,
  useProgram,
  useTimeline,
  Program, // Interface
  Channel, // Interface
  ProgramItem, // Interface for program render
  Theme, // Interface
} from 'planby';

License

Custom License - All Rights Reserved. See LICENSE for more information.

Contact

Karol Kozer - @kozerkarol_twitter

Project Link: https://github.com/karolkozer/planby

app
component
eletronic
epg
events
guide
harmonogram
hooks
music
online
program
react
schedule
television
tv
vod
web
website

karolkozer/planby

TypeScript

1,721

150 commits

updated Aug 23, 2026

See the code

README

πŸ€– New: build Planby with AI one command wires the official Planby skill into your coding assistant πŸ€– πŸ‘‰See how πŸ‘ˆ

Description

Planby is a React based component for a quick implementation of Epg, schedules, live streaming, music events, timelines and many more ideas. It uses a custom virtual view which allows you to operate on a really big number of data. The component has a simple API that you can easily integrate with other third party UI libraries. The component theme is customised to the needs of the application design.

A Planby conference schedule: five stage rows from 9:00 to 17:00, light theme, session cards with speaker names and durations

πŸ”₯ Planby Native β€” React Native beta

Planby is coming to React Native, and the beta is open for free testing. It is a separate package from this one; the web component documented here is unaffected.

πŸ‘‰ Sign up for the Planby Native beta

Codesandbox example

Live example - Codesandbox

Live example - Typescript Codesandbox

Live example - website with control panel

Testimonials

πŸš€ Become a Sponsor! πŸš€

Become a sponsor, support, and help us in continuing our development. -> Opencollective

Getting Started

Installation

  • yarn
yarn add planby
  • npm
npm install planby

Using Planby with AI assistants

AI assistants know React, but they don't know Planby. Left to guess, they invent useEpg options that don't exist, forget that channels and epg have to be memoized, and reach for features that are only in Planby PRO.

Planby ships an agent skill β€” a compact description of the API, verified against the source β€” that your assistant reads before it writes any code.

Setup

npm install planby
npx planby init-ai

One command wires the skill into every agent you use:

AgentWhat it writes
Claude Code / Agent SDK.claude/skills/planby
Cursor.cursor/rules/planby.mdc
Windsurf.windsurf/rules/planby.md
Codex, Gemini CLI, Antigravity, …a section in AGENTS.md

Each file is a thin pointer to the skill bundled inside node_modules/planby, so the guidance always matches the version you actually installed. Re-run it after upgrading β€” it is safe to run repeatedly.

Using ChatGPT or another tool without a rules file? Paste node_modules/planby/skills/planby/SKILL.md into the conversation.

Example: ask for a schedule

"Build me a conference schedule with five stages from 9:00 to 17:00."

import React from 'react';
import { useEpg, Epg, Layout } from 'planby';

const channels = [
  { uuid: 'stage-a', logo: '/stage-a.png' },
  { uuid: 'stage-b', logo: '/stage-b.png' },
  { uuid: 'stage-c', logo: '/stage-c.png' },
  { uuid: 'stage-d', logo: '/stage-d.png' },
  { uuid: 'stage-e', logo: '/stage-e.png' },
];

const epg = [
  {
    id: 'p-1',
    channelUuid: 'stage-a', // must match a channels[].uuid
    title: 'Opening Keynote',
    description: 'Welcome & vision', // required by the Program type
    image: '/keynote.png',
    since: '2022-02-02T09:00:00',
    till: '2022-02-02T10:00:00',
  },
  {
    id: 'p-2',
    channelUuid: 'stage-b',
    title: 'Workshop: Data',
    description: 'Hands-on session',
    image: '/workshop.png',
    since: '2022-02-02T10:30:00',
    till: '2022-02-02T12:00:00',
  },
  {
    id: 'p-3',
    channelUuid: 'stage-e',
    title: 'Closing Panel',
    description: 'Q&A with the speakers',
    image: '/panel.png',
    since: '2022-02-02T16:00:00',
    till: '2022-02-02T17:00:00',
  },
];

export function Schedule() {
  const channelsData = React.useMemo(() => channels, []);
  const epgData = React.useMemo(() => epg, []);

  const { getEpgProps, getLayoutProps } = useEpg({
    channels: channelsData,
    epg: epgData,
    startDate: '2022-02-02T09:00:00',
    endDate: '2022-02-02T17:00:00',
    dayWidth: 2400, // 8h range Γ— 300px per hour
  });

  return (
    <div style={{ height: '600px', width: '1200px' }}>
      <Epg {...getEpgProps()}>
        <Layout {...getLayoutProps()} />
      </Epg>
    </div>
  );
}

Example: ask for your branding

"Now restyle it to match our light theme, add shadows, and show each session’s duration."

The assistant layers customization on top without rebuilding the data wiring β€” a full theme, and globalStyles for the font and the shadow around the whole guide, plus a custom renderProgram built on the useProgram hook:

// `.planby` is the container of the whole schedule β€” that is where a shadow
// around the guide belongs. It is the one class hook the library exposes.
const globalStyles = `
  @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");

  .planby {
    font-family: "Inter", system-ui, sans-serif;
    border-radius: 12px;
    box-shadow: 0 10px 30px rgb(15 23 42 / 15%);
    overflow: hidden;
  }
`;

const formatDuration = (since, till) => {
  const minutes = Math.round((new Date(till) - new Date(since)) / 60000);
  const h = Math.floor(minutes / 60);
  const m = minutes % 60;
  return h ? (m ? `${h}h ${m}m` : `${h}h`) : `${m}m`;
};

const Program = ({ program, ...rest }) => {
  const { styles, formatTime, isLive } = useProgram({ program, ...rest });
  const { title, since, till } = program.data;

  return (
    <ProgramBox width={styles.width} style={styles.position}>
      <ProgramContent width={styles.width} isLive={isLive}>
        <ProgramStack>
          <ProgramTitle>{title}</ProgramTitle>
          <ProgramText>
            {formatTime(since)} - {formatTime(till)} Β·{' '}
            {formatDuration(since, till)}
          </ProgramText>
        </ProgramStack>
      </ProgramContent>
    </ProgramBox>
  );
};

// globalStyles goes on useEpg, alongside your theme:
useEpg({ channels, epg, startDate, endDate, theme, globalStyles });

<Layout
  {...getLayoutProps()}
  renderProgram={({ program, ...rest }) => (
    <Program key={program.data.id} program={program} {...rest} />
  )}
/>;
A Planby conference schedule: five stage rows from 9:00 to 17:00, light theme, session cards with speaker names and durations

Both prompts above, run end to end β€” five stages across a 9:00–17:00 range, re-skinned to a light theme with a shadow around the guide and a duration on every session card.

Other things worth asking for

  • "Switch the guide to 12-hour time and right-to-left."
  • "Make the program cards match this Figma β€” rounded corners, our brand hover."
  • "Load the programs from our API and show the spinner while it fetches."
  • "Why is my guide zoomed in so far?" β€” a classic: dayWidth is spread across the whole startDate…endDate range, not per day.
  • "Nothing renders." β€” usually an un-memoized array, a channelUuid that matches no channel, or a container with no explicit height.

What the skill covers

TopicReference
Starter, sizing, async datareferences/quick-start.md
Channel & Program schemasreferences/data-schemas.md
Every useEpg option and return valuereferences/useEpg-api.md
Theme β€” and what each color controlsreferences/theme.md
Custom program / channel / timeline renderingreferences/render-functions.md
Branding, fonts, styled() overridesreferences/customization.md

Two runnable, type-checked examples ship alongside them in skills/planby/examples/.

What it will not do

The skill knows which features belong to Planby PRO and will tell you instead of generating code that cannot work: vertical / single-track mode, drag-and-drop, resizing, week and month calendar views, timezone conversion, grid cells, highlighted areas, mobile controllers, and scroll-based lazy loading.

Usage

import { useEpg, Epg, Layout } from 'planby';

const channels = React.useMemo(
  () => [
    {
      logo: 'https://via.placeholder.com',
      uuid: '10339a4b-7c48-40ab-abad-f3bcaf95d9fa',
      ...
    },
  ],
  []
);

const epg = React.useMemo(
  () => [
    {
      channelUuid: '30f5ff1c-1346-480a-8047-a999dd908c1e',
      description:
        'Ut anim nisi consequat minim deserunt...',
      id: 'b67ccaa3-3dd2-4121-8256-33dbddc7f0e6',
      image: 'https://via.placeholder.com',
      since: "2022-02-02T23:50:00",
      till: "2022-02-02T00:55:00",
      title: 'Title',
      ...
    },
  ],
  []
);

const {
  getEpgProps,
  getLayoutProps,
  onScrollToNow,
  onScrollLeft,
  onScrollRight,
} = useEpg({
  epg,
  channels,
  startDate: '2022-02-02T00:00:00'
});

return (
  <div>
    <div style={{ height: '600px', width: '1200px' }}>
      <Epg {...getEpgProps()}>
        <Layout
          {...getLayoutProps()}
        />
      </Epg>
    </div>
  </div>
);

or

Custom width and height

const {
  getEpgProps,
  getLayoutProps,
  ...
} = useEpg({
  epg,
  channels,
 startDate: '2022/02/02', // or 2022-02-02T00:00:00
  width: 1200,
  height: 600
});

return (
  <div>
     <Epg {...getEpgProps()}>
        <Layout
          {...getLayoutProps()}
        />
      </Epg>
  </div>

or

Time range

const {
  getEpgProps,
  getLayoutProps,
  ...
} = useEpg({
  epg,
  channels,
  startDate: '2022-02-02T10:00:00',
  endDate: '2022-02-02T20:00:00',
  width: 1200,
  height: 600
});

return (
  <div>
     <Epg {...getEpgProps()}>
        <Layout
          {...getLayoutProps()}
        />
      </Epg>
  </div>

API

useEpg

Options

Available options in useEpg

PropertyTypeStatusDescriptionAccess
channelsarrayrequiredArray with channels data
epgarrayrequiredArray with EPG data
widthnumberoptionalEPG width
heightnumberoptionalEPG height
sidebarWidthnumberoptionalWidth of the sidebar with channels
timelineHeightnumberoptionalHeight of the timelinePRO
itemHeightnumberoptionalHeight of channels and programs in the EPG. Default value is 80
dayWidthnumberoptionalWidth of the day. Default value is 7200. Calculation to set up day width with own hour width value e.g., 24h * 300px (your custom hour width) = 7200px -> dayWidth
startDatestringoptionalDate format 2022/02/02 or 2022-02-02T00:00:00. You can set your own start time, e.g., 2022-02-02T10:00:00, 2022-02-02T14:00:00, etc. Full clock hours only
endDatestringoptionalDate format 2022-02-02T00:00:00, 2022-02-02T20:00:00, etc. Must be within the same 24-hour period as startDate. Full clock hours only. Scroll through multiple days and timeline mode is available only in PRO plan.PRO
hoursInDaysarrayoptionalSet start time and end time of each day in multiple days feature if your data for each day has some time spaces between items in the day.PRO
initialScrollPositionsobjectoptionalSet initial scroll position in Layout, e.g., initialScrollPositions: { top: 500, left: 800 }PRO
liveRefreshTimenumberoptionalLive refresh time of the events. Default value is 120 sec.PRO
isBaseTimeFormatbooleanoptionalConvert to 12-hour format, e.g., 2:00am, 4:00pm, etc. Default value is false.
isCurrentTimebooleanoptionalShow current time in Timeline. Default value is false.PRO
isInitialScrollToNowbooleanoptionalScroll to the current live element.PRO
isVerticalModebooleanoptionalShow Timeline in vertical view. Default value is false.PRO
isResizebooleanoptionalPossibility to resize the element.PRO
isSidebarbooleanoptionalShow/hide sidebar
isTimelinebooleanoptionalShow/hide timeline
isLinebooleanoptionalShow/hide line
isRTLbooleanoptionalChange direction to RTL or LTR. Default value is false.PRO
themeobjectoptionalObject with theme schema
timezoneobjectoptionalConvert and display data from UTC format to your own time zonePRO
areasarrayoptionalArea gives possibilities to add field ranges to the Timeline layout.PRO
modeobjectoptionalType values: day/week/month. Style values: default/modern Define the mode and style of the timeline. Default mode is day and style is defaultPRO
overlapobjectoptionalEnable the element overlaps in the layout. Mode values: stack/layer, layerOverlapLevel: numberPRO
drag and dropobjectoptionalDrag and move the element in the layout. Mode values: row/multi-rowsPRO
grid layoutobjectoptionalBackground grid on the layout. Mode hoverHighlight values: true/false, onGridItemClick: function with all the properties on clicked item gridPRO
channelMapKeystringoptionalThe Channel uuid attribute can be controlled by prop. Key map gives a possibilities to use specific prop from own data instead of needing to map to uuid in own dataPRO
programChannelMapKeystringoptionalThe Programs channelUuid attributes can be controlled by prop. Key map gives a possibilities to use a specific prop from own data instead of needing to map to channelUuid in your dataPRO
globalStylesstringoptionalInject custom global styles and font. Font weight: 400,500,600. Default font is "Inter"PRO

Note about width and height props

Without declaring the width and length properties, the component takes the dimensions of the parent element.

globalStyles

Inject own custom font and other global styles.

const globalStyles = `
  @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");

/* Available in PRO plan */
 .planby {
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica,
    Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";

  /* Layout */
  .planby-layout {}

  /* Line */
  .planby-line {}

  /* Current time */
  .planby-current-time {}
  .planby-current-content {}

  /* Channels */
  .planby-channels {}

  /* Channel */
  .planby-channel {}

  /* Program */
  .planby-program {}
  .planby-program-content {}
  .planby-program-flex {}
  .planby-program-stack {}
  .planby-program-title {}
  .planby-program-text {}

  /* Timeline */
  .planby-timeline-wrapper {}
  .planby-timeline-box {}
  .planby-timeline-time {}
  .planby-timeline-dividers {}
  .planby-timeline-wrapper {}
}
  
`;

Instance Properties

Properties returned from useEpg

PropertyTypeDescription
scrollYnumberCurrent scroll y value
scrollXnumberCurrent scroll x value
onScrollLeftfunction(value: number)Default value is 300
onScrollRightfunction(value: number)Default value is 300
onScrollToNowfunction()Scroll to current time/live programs
onScrollTopfunction(value: number)Default value is 300

Channel schema

PropertyTypeStatus
logostringrequired
uuidstringrequired

Epg schema

PropertyTypeStatusDescriptionAccess
channelUuidstringrequired
idstringrequired
imagestringrequired
sincestringrequired
tillstringrequired
titlestringrequired
fixedVisibilitybooleanoptionalThe element is always visible in the layout during the scroll eventsSponsors

Epg

Base props

Available props in Epg

PropertyTypeDescriptionStatus
isLoadingbooleanLoader stateoptional
loaderComponentLoader custom componentoptional

Layout

Base props

Available props in Layout.

PropertyTypeDescriptionStatusAccess
renderProgramfunction({ program: { data: object, position: object})data object contains all properties related to the program, position object includes all position stylesoptional
renderChannelfunction({ channel: { ..., position: object})channel object contains all properties related to the channel, position object includes all position stylesoptional
renderTimelinefunction({sidebarWidth: number})sidebarWidth value of the channel's sidebar widthoptional
renderLinefunction({styles: object})basic styles and position values for the custom live tracking LineoptionalSponsors
renderCurrentTimefunction({styles: object, isRTL: boolean, isBaseTimeFormat: boolean, time: string})basic styles values for the custom current timeoptionalSponsors

Render functions

You can use Plaby's style components to develop main features. Moreover, you can integrate with third party UI library eg. Chakra UI, Material UI etc or make custom styles.

renderProgram

Below is an example that allows you to render your custom Program component using Plaby's style components.

import {
  useEpg,
  Epg,
  Layout,
  ProgramBox,
  ProgramContent,
  ProgramFlex,
  ProgramStack,
  ProgramTitle,
  ProgramText,
  ProgramImage,
  useProgram,
  Program,
  ProgramItem
} from "planby";


const Item = ({ program,...rest }: ProgramItem) => {
  const { styles, formatTime, isLive, isMinWidth } = useProgram({ program,...rest });

  const { data } = program;
  const { image, title, since, till } = data;

  const sinceTime = formatTime(since);
  const tillTime = formatTime(till);

  return (
    <ProgramBox width={styles.width} style={styles.position}>
      <ProgramContent
        width={styles.width}
        isLive={isLive}
      >
        <ProgramFlex>
          {isLive && isMinWidth && <ProgramImage src={image} alt="Preview" />}
          <ProgramStack>
            <ProgramTitle>{title}</ProgramTitle>
            <ProgramText>
              {sinceTime} - {tillTime}
            </ProgramText>
          </ProgramStack>
        </ProgramFlex>
      </ProgramContent>
    </ProgramBox>
  );
};

function App() {

  ...

 const {
  getEpgProps,
  getLayoutProps,
} = useEpg({
  epg,
  channels,
  startDate: '2022/02/02', // or 2022-02-02T00:00:00
});

return (
  <div>
    <div style={{ height: '600px', width: '1200px' }}>
      <Epg {...getEpgProps()}>
        <Layout
            {...getLayoutProps()}
            renderProgram={({ program,...rest }) => (
              <Item key={program.data.id} program={program} {...rest} />
            )}
          />
      </Epg>
    </div>
  </div>
);
}

export default App;

renderProgram - 12 hours time format

Below is an example that allows you to render your custom Program component with 12 hours time format using Plaby's style components.

...
const Item = ({ program, ...rest }: ProgramItem) => {
  const {
    styles,
    formatTime,
    set12HoursTimeFormat,
    isLive,
    isMinWidth,
  } = useProgram({
    program,
    ...rest
  });

  const { data } = program;
  const { image, title, since, till } = data;

  const sinceTime = formatTime(since, set12HoursTimeFormat()).toLowerCase();
  const tillTime = formatTime(till, set12HoursTimeFormat()).toLowerCase();

  return (
    <ProgramBox width={styles.width} style={styles.position}>
      <ProgramContent
        width={styles.width}
        isLive={isLive}
      >
        <ProgramFlex>
          {isLive && isMinWidth && <ProgramImage src={image} alt="Preview" />}
          <ProgramStack>
            <ProgramTitle>{title}</ProgramTitle>
            <ProgramText>
              {sinceTime} - {tillTime}
            </ProgramText>
          </ProgramStack>
        </ProgramFlex>
      </ProgramContent>
    </ProgramBox>
  );
};

function App() {

  ...

 const {
  getEpgProps,
  getLayoutProps,
} = useEpg({
  epg,
  channels,
  isBaseTimeFormat: true,
  startDate: '2022/02/02', // or 2022-02-02T00:00:00
});

...
}

export default App;

renderProgram - RTL direction

Below is an example that allows you to render your custom Program component with RTL direction using Plaby's style components.

...
const Item = ({ program, ...rest }: ProgramItem) => {
  const {
    isRTL,
    isLive,
    isMinWidth,
    formatTime,
    styles,
    set12HoursTimeFormat,
    getRTLSinceTime,
    getRTLTillTime,
  } = useProgram({
    program,
    ...rest
  });

  const { data } = program;
  const { image, title, since, till } = data;

  const sinceTime = formatTime(
    getRTLSinceTime(since),
    set12HoursTimeFormat()
  ).toLowerCase();
  const tillTime = formatTime(
    getRTLTillTime(till),
    set12HoursTimeFormat()
  ).toLowerCase();

  return (
    <ProgramBox width={styles.width} style={styles.position}>
      <ProgramContent width={styles.width} isLive={isLive}>
        <ProgramFlex>
          {isLive && isMinWidth && <ProgramImage src={image} alt="Preview" />}
          <ProgramStack isRTL={isRTL}>
            <ProgramTitle>{title}</ProgramTitle>
            <ProgramText>
              {sinceTime} - {tillTime}
            </ProgramText>
          </ProgramStack>
        </ProgramFlex>
      </ProgramContent>
    </ProgramBox>
  );
};

function App() {

  ...

 const {
  getEpgProps,
  getLayoutProps,
} = useEpg({
  epg,
  channels,
  isBaseTimeFormat: true,
  startDate: '2022/02/02', // or 2022-02-02T00:00:00
});

...
}

export default App;

renderChannel

Below is an example that allows you to render your custom Channel component using Plaby's style components.

import { useEpg, Epg, Layout, ChannelBox, ChannelLogo, Channel } from 'planby';

interface ChannelItemProps {
  channel: Channel;
}

const ChannelItem = ({ channel }: ChannelItemProps) => {
  const { position, logo } = channel;
  return (
    <ChannelBox {...position}>
      <ChannelLogo
        onClick={() => console.log('channel', channel)}
        src={logo}
        alt="Logo"
      />
    </ChannelBox>
  );
};


function App() {

  ...

  const {
    getEpgProps,
    getLayoutProps,
  } = useEpg({
    epg,
    channels,
    startDate: '2022/02/02', // or 2022-02-02T00:00:00
  });

  return (
    <div>
      <div style={{ height: '600px', width: '1200px' }}>
        <Epg {...getEpgProps()}>
          <Layout
              {...getLayoutProps()}
              renderChannel={({ channel }) => (
              <ChannelItem key={channel.uuid} channel={channel} />
            )}
            />
        </Epg>
      </div>
    </div>
  );
}

renderTimeline

Below is an example that allows you to render your custom Timeline component using Plaby's style components.

import {
  TimelineWrapper,
  TimelineBox,
  TimelineTime,
  TimelineDivider,
  TimelineDividers,
  useTimeline,
} from 'planby';

interface TimelineProps {
  isBaseTimeFormat: boolean;
  isSidebar: boolean;
  dayWidth: number;
  hourWidth: number;
  numberOfHoursInDay: number;
  offsetStartHoursRange: number;
  sidebarWidth: number;
}

export function Timeline({
  isBaseTimeFormat,
  isSidebar,
  dayWidth,
  hourWidth,
  numberOfHoursInDay,
  offsetStartHoursRange,
  sidebarWidth,
}: TimelineProps) {
  const { time, dividers, formatTime } = useTimeline(
    numberOfHoursInDay,
    isBaseTimeFormat
  );

  const renderTime = (index: number) => (
    <TimelineBox key={index} width={hourWidth}>
      <TimelineTime>
        {formatTime(index + offsetStartHoursRange).toLowerCase()}
      </TimelineTime>
      <TimelineDividers>{renderDividers()}</TimelineDividers>
    </TimelineBox>
  );

  const renderDividers = () =>
    dividers.map((_, index) => (
      <TimelineDivider key={index} width={hourWidth} />
    ));

  return (
    <TimelineWrapper
      dayWidth={dayWidth}
      sidebarWidth={sidebarWidth}
      isSidebar={isSidebar}
    >
      {time.map((_, index) => renderTime(index))}
    </TimelineWrapper>
  );
}

function App() {

  ...

  const {
    getEpgProps,
    getLayoutProps,
  } = useEpg({
    epg,
    channels,
    startDate: '2022/02/02', // or 2022-02-02T00:00:00
  });

  return (
    <div>
      <div style={{ height: '600px', width: '1200px' }}>
        <Epg {...getEpgProps()}>
          <Layout
              {...getLayoutProps()}
              renderTimeline={(props) => <Timeline {...props} />}
            />
        </Epg>
      </div>
    </div>
  );
}

export default App;

renderTimeline - RTL direction

Below is an example that allows you to render your custom Timeline component using Plaby's style components.

import {
  TimelineWrapper,
  TimelineBox,
  TimelineTime,
  TimelineDivider,
  TimelineDividers,
  useTimeline,
} from 'planby';

interface TimelineProps {
  isRTL: boolean;
  isBaseTimeFormat: boolean;
  isSidebar: boolean;
  dayWidth: number;
  hourWidth: number;
  numberOfHoursInDay: number;
  offsetStartHoursRange: number;
  sidebarWidth: number;
}

export function Timeline({
  isRTL,
  isBaseTimeFormat,
  isSidebar,
  dayWidth,
  hourWidth,
  numberOfHoursInDay,
  offsetStartHoursRange,
  sidebarWidth,
}: TimelineProps) {
  const { time, dividers, formatTime } = useTimeline(
    numberOfHoursInDay,
    isBaseTimeFormat
  );

  const renderTime = (index: number) => (
    <TimelineBox key={index} width={hourWidth}>
      <TimelineTime isBaseTimeFormat={isBaseTimeFormat} isRTL={isRTL}>
        {formatTime(index + offsetStartHoursRange).toLowerCase()}
      </TimelineTime>
      <TimelineDividers>{renderDividers()}</TimelineDividers>
    </TimelineBox>
  );

 ...
}

Theme

Schema

Make your theme custom. Below is theme schema that you can pass as one of the options to useEpg hook.

const theme = {
  primary: {
    600: '#1a202c',
    900: '#171923',
  },
  grey: { 300: '#d1d1d1' },
  white: '#fff',
  green: {
    300: '#2C7A7B',
  },
  loader: {
    teal: '#5DDADB',
    purple: '#3437A2',
    pink: '#F78EB6',
    bg: '#171923db',
  },
  scrollbar: {
    border: '#ffffff',
    thumb: {
      bg: '#e1e1e1',
    },
  },
  gradient: {
    blue: {
      300: '#002eb3',
      600: '#002360',
      900: '#051937',
    },
  },
  text: {
    grey: {
      300: '#a0aec0',
      500: '#718096',
    },
  },
  timeline: {
    divider: {
      bg: '#718096',
    },
  },
};

All import options

import {
  Epg,
  Layout,
  ChannelBox,
  ChannelLogo,
  ProgramBox,
  ProgramContent,
  ProgramFlex,
  ProgramStack,
  ProgramTitle,
  ProgramText,
  ProgramImage,
  TimelineWrapper,
  TimelineBox,
  TimelineTime,
  TimelineDividers,
  useEpg,
  useProgram,
  useTimeline,
  Program, // Interface
  Channel, // Interface
  ProgramItem, // Interface for program render
  Theme, // Interface
} from 'planby';

License

Custom License - All Rights Reserved. See LICENSE for more information.

Contact

Karol Kozer - @kozerkarol_twitter

Project Link: https://github.com/karolkozer/planby

app
component
eletronic
epg
events
guide
harmonogram
hooks
music
online
program
react
schedule
television
tv
vod
web
website

Project health

Latest release

2.1.0 Β· Aug 23, 2026

Releases this year

1

Significant stargazers

Andrew Lisowski

431 followers Β· starred Oct 2022

lijianan

2,020 followers Β· starred Feb 2025

Lazar Nikolov

776 followers Β· starred Mar 2022

Terminal

65 followers Β· starred May 2023