Skip to main content
Light Dark System

Schedule Builder

<zn-schedule-builder> | ZnScheduleBuilder
Since 1.0 experimental

Builds a weekly opening-hours schedule as a drag-to-paint calendar or a compact list of time ranges, and posts the result as JSON.

<zn-schedule-builder
  name="opening-hours"
  value='{
    "timezone": "Europe/London",
    "days": {
      "mon": ["08:00-18:00"],
      "tue": ["08:00-18:00"],
      "wed": ["08:00-18:00"],
      "thu": ["08:30-18:00"],
      "fri": ["08:00-18:00"],
      "sat": ["09:00-13:00"],
      "sun": []
    },
    "exceptions": [
      {"label": "All-hands offsite", "date": "2026-08-28", "ranges": ["08:00-13:00"]},
      {"label": "Systems maintenance", "date": "2026-09-14", "ranges": ["10:00-18:00"]},
      {"label": "Christmas Eve — early close", "date": "2026-12-24", "ranges": ["08:00-13:00"]}
    ]
  }'>
</zn-schedule-builder>

The Value

The schedule is serialised to JSON and exposed three ways:

  • as the value attribute/property — a JSON string, which is what gets submitted with the form;
  • as the schedule property — the same data as a plain object;
  • as the zn-change event, emitted whenever the schedule changes.
{
  "timezone": "UTC",
  "days": {
    "mon": [{ "start": "08:00", "end": "18:00" }],
    "tue": [{ "start": "08:00", "end": "12:00" }, { "start": "13:30", "end": "18:00" }],
    "wed": [], "thu": [], "fri": [], "sat": [], "sun": []
  },
  "exceptions": [
    { "label": "Christmas Eve — early close", "date": "2026-12-24", "ranges": [{ "start": "08:00", "end": "13:00" }] }
  ]
}

Ranges are always sorted, and overlapping or touching ranges are merged, so 08:00–12:00 plus 12:00–18:00 becomes a single 08:00–18:00. Times are 24 hour HH:MM regardless of the time-format used for display, and timezone names the timezone they are expressed in — see Timezones.

When you write the value yourself you can use the "08:00-18:00" shorthand instead of {"start": "08:00", "end": "18:00"}, and you can pass a bare day map without the days wrapper. Both are normalised on the way in.

Examples

Views

The builder has two views. calendar is a drag-to-paint week grid; form is a compact list of time ranges that fits inside a normal settings form. Use the view attribute to choose the starting view, and no-toggle to remove the switcher and lock the component to one of them.

<zn-schedule-builder
  view="form"
  value='{"mon":["08:00-18:00"],"tue":["08:00-18:00"],"wed":["08:00-12:00","13:30-18:00"],"thu":["08:00-18:00"],"fri":["08:00-16:00"],"sat":["09:00-13:00"],"sun":[]}'>
</zn-schedule-builder>

In the form view, click a range to edit it, and use Add range to split a day into multiple periods. In the calendar view, drag across the grid to open hours and drag over open hours to close them — a drag can span several days at once.

Timezones

A schedule has two timezones: the one it is stored in, and the one it is shown in.

  • save-timezone is the timezone the value is written in. It defaults to UTC as soon as the schedule is timezone-aware, so hours land in your database in one canonical zone.
  • display-timezone is the timezone the grid and the list are drawn in. It defaults to save-timezone, and accepts auto for the viewer’s own timezone.
  • show-timezone adds a picker so the viewer can read the same schedule in any timezone.

Switching the display timezone never changes the value — the same hours are simply labelled in another zone, and zn-change doesn’t fire. Edits work the other way around: you drag or type in the zone you are looking at, and the component converts back to save-timezone before storing.



              

              
<zn-schedule-builder
  id="tz-demo"
  show-timezone
  display-timezone="Europe/London"
  start-hour="0"
  end-hour="24"
  hide-summary
  value='{"timezone":"UTC","days":{"mon":["08:00-18:00"],"tue":["08:00-18:00"],"wed":["08:00-18:00"],"thu":["08:00-18:00"],"fri":["08:00-16:00"]}}'>
</zn-schedule-builder>

<pre id="tz-demo-output" style="margin-top: 1rem"></pre>

<script>
  const tzBuilder = document.querySelector('#tz-demo');
  const tzOutput = document.querySelector('#tz-demo-output');

  const printTz = () => (tzOutput.textContent = `stored (UTC):\n${JSON.stringify(tzBuilder.schedule.days, null, 2)}`);

  tzBuilder.addEventListener('zn-change', printTz);
  printTz();
</script>

Pick a timezone far from UTC and the blocks slide, wrapping onto the next day — or onto Monday, from the end of Sunday — while the stored hours below stay put.

Which Timezones the Picker Offers

timezones takes IANA names, or one of three named sets:

Set Zones What it is
en 6 US Eastern, Central, Mountain and Pacific, the UK, and Australia — listed under those names rather than their IANA ones. For an English-speaking audience who would rather not read America/Los_Angeles.
offsets ~42 One zone per UTC offset. The default — enough to read a schedule from anywhere without a long list, though the city standing in for each offset is arbitrary.
common ~90 Every offset in use, plus the business and population centres that share one — so Berlin, Paris, Madrid and Rome all appear rather than one standing for the rest. Includes the half and quarter-hour zones (India, Iran, Nepal, Newfoundland, central Australia, Chatham).
all ~420 Everything Intl.supportedValuesOf('timeZone') reports. Complete, but full of aliases and zones nobody selects.

common is the one to reach for whenever real users pick their own timezone; offsets suits an internal tool where the offset is all that matters; en suits a product whose customers are all in the US, the UK or Australia.

<zn-schedule-builder
  show-timezone
  timezones="en"
  display-timezone="Europe/London"
  view="form"
  value='{"timezone":"UTC","days":{"mon":["13:00-21:00"],"tue":["13:00-21:00"]}}'>
</zn-schedule-builder>

Sets and explicit names can be mixed — timezones="en Asia/Tokyo" adds Tokyo to the six above, and a set’s friendly name wins over a later plain listing of the same zone. The viewer’s own timezone plus both configured zones are always added, so the current selection is never missing from the list.

<zn-schedule-builder
  show-timezone
  display-timezone="America/New_York"
  timezones="UTC Europe/London America/New_York Asia/Tokyo"
  view="form"
  value='{"timezone":"UTC","days":{"mon":["13:00-21:00"],"tue":["13:00-21:00"]}}'>
</zn-schedule-builder>

Reading the hours in code follows the same split: schedule, value and getDay() are in the save timezone, while displayedDays and setDisplayDay() are in the display timezone.

Open and Closed Labels

open-label and closed-label name the two states. They appear in the calendar legend, against days with no hours, and in the exception annotations, so use whatever wording fits the thing being scheduled — availability, cover, staffing, opening hours.

<zn-schedule-builder
  open-label="On call"
  closed-label="Off"
  start-hour="8"
  end-hour="20"
  hide-summary
  value='{"mon":["09:00-17:00"],"tue":["09:00-17:00"],"wed":["09:00-17:00"],"thu":["09:00-17:00"],"fri":["09:00-13:00"]}'>
</zn-schedule-builder>

Labels and Help Text

<zn-schedule-builder
  label="Opening hours"
  help-text="Customers can only book appointments during these hours."
  view="form"
  value='{"mon":["09:00-17:00"],"tue":["09:00-17:00"],"wed":["09:00-17:00"],"thu":["09:00-17:00"],"fri":["09:00-17:00"]}'>
</zn-schedule-builder>

Grid Range and Granularity

start-hour and end-hour set the window the calendar draws, and interval sets both the grid granularity and the step of the time inputs in the form view. The interval must be one of 5, 10, 15, 20, 30 or 60 minutes.

<zn-schedule-builder
  start-hour="7"
  end-hour="13"
  interval="15"
  hide-summary
  value='{"mon":["08:00-12:00"],"tue":["08:15-11:45"],"wed":["08:00-12:00"],"thu":["08:00-12:00"],"fri":["08:00-11:00"]}'>
</zn-schedule-builder>

Exceptions

Exceptions are dated deviations from the weekly pattern. The builder never edits them — it displays them, and carries them through the value untouched — so the surrounding application stays in charge of how they are created.

Hours an exception removes are drawn in a lighter shade in the calendar, and annotated against the day in the form view. An exception can be a single date, or a from/to window, and can either close the day outright (closed) or replace its hours (ranges).

Only exceptions that change the repeating weekly pattern are drawn against it: ones that name their days, and ones whose date window covers at least a full week. A one-off date is a single occurrence rather than a pattern, so it is carried through the value untouched but leaves the grid alone.

<zn-schedule-builder
  view="form"
  value='{
    "days": {
      "mon": ["08:00-18:00"],
      "tue": ["08:00-18:00"],
      "wed": ["08:00-18:00"],
      "thu": ["08:00-18:00"],
      "fri": ["08:00-16:00"],
      "sat": ["09:00-13:00"],
      "sun": []
    },
    "exceptions": [
      {"label": "Summer Fridays", "from": "2026-06-01", "to": "2026-08-29", "days": ["fri"], "ranges": ["08:00-16:00"]},
      {"label": "Bank holiday", "date": "2026-08-31", "closed": true}
    ]
  }'>
</zn-schedule-builder>

Week Start and Time Format

<zn-schedule-builder
  week-start="sun"
  time-format="12"
  view="form"
  value='{"mon":["08:00-18:00"],"sat":["09:00-13:00"],"sun":["10:00-16:00"]}'>
</zn-schedule-builder>

Readonly and Disabled

Use readonly to display a schedule without any editing affordances, and disabled to grey it out and take it out of form submission.

<zn-schedule-builder
  readonly
  view="form"
  value='{"mon":["08:00-18:00"],"tue":["08:00-18:00"],"wed":["08:00-18:00"],"thu":["08:00-18:00"],"fri":["08:00-18:00"],"sat":["09:00-13:00"]}'>
</zn-schedule-builder>

Reading the Value from JavaScript



              

              
<zn-schedule-builder
  id="schedule-demo"
  view="form"
  value='{"mon":["09:00-17:00"],"tue":["09:00-17:00"]}'>
</zn-schedule-builder>

<pre id="schedule-demo-output" style="margin-top: 1rem"></pre>

<script>
  const builder = document.querySelector('#schedule-demo');
  const output = document.querySelector('#schedule-demo-output');

  const print = () => (output.textContent = JSON.stringify(builder.schedule, null, 2));

  builder.addEventListener('zn-change', print);
  print();
</script>

In a Form

The schedule posts as a single JSON field named after the control.


Submit
<form class="schedule-form">
  <zn-schedule-builder
    name="opening-hours"
    label="Opening hours"
    required
    view="form"
    value='{"mon":["09:00-17:00"],"tue":["09:00-17:00"],"wed":["09:00-17:00"],"thu":["09:00-17:00"],"fri":["09:00-17:00"]}'>
  </zn-schedule-builder>

  <br />
  <zn-button type="submit">Submit</zn-button>
</form>

<script>
  const form = document.querySelector('.schedule-form');

  form.addEventListener('submit', event => {
    event.preventDefault();
    const data = new FormData(form);
    alert(data.get('opening-hours'));
  });
</script>

Accessibility

The form view is fully keyboard operable: ranges are buttons, editing a range gives you two native time inputs, and Enter or Escape closes the editor. The calendar view is a pointer-driven surface, so offer the form view (the default toggle does) wherever keyboard-only users need to edit a schedule.

Importing

If you’re using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use any of the following snippets to cherry pick this component.

To import this component from the CDN using a script tag:

<script type="module" src="https://cdn.jsdelivr.net/npm/@kubex/zinc@1.1.92/dist/components/schedule-builder/schedule-builder.js"></script>

To import this component from the CDN using a JavaScript import:

import 'https://cdn.jsdelivr.net/npm/@kubex/zinc@1.1.92/dist/components/schedule-builder/schedule-builder.js';

To import this component using a bundler:

import '@kubex/zinc/dist/components/schedule-builder/schedule-builder.js';

Slots

Name Description
label The schedule’s label. Alternatively, use the label attribute.
help-text Text that describes how to use the schedule. Alternatively, use the help-text attribute.

Learn more about using slots.

Properties

Name Description Reflects Type Default
name The name of the form control, submitted as a name/value pair with form data. string -
value The schedule as a JSON string. This is what gets posted with the form. string ''
defaultValue The default value, used when resetting the containing form. string ''
label The schedule’s label. If you need to display HTML, use the label slot instead. string ''
helpText
help-text
The schedule’s help text. If you need to display HTML, use the help-text slot instead. string ''
view Which view is showing. ScheduleView 'calendar'
openLabel
open-label
The word used for hours the schedule covers, in the legend and in labels. string 'Available'
closedLabel
closed-label
The word used for hours the schedule doesn’t cover, in the legend and against empty days. string 'Closed'
noToggle
no-toggle
Hides the calendar/form view toggle. boolean false
hideSummary
hide-summary
Hides the summary panel beside the calendar. boolean false
startHour
start-hour
The first hour shown in the calendar. number 6
endHour
end-hour
The last hour shown in the calendar. number 22
interval The granularity of the calendar grid and the time inputs, in minutes. number 30
weekStart
week-start
The weekday the week starts on. ScheduleDay 'mon'
timeFormat
time-format
Displays times as 12 or 24 hour. The serialised value is always 24 hour HH:MM. '12' | '24' '24'
displayTimezone
display-timezone
The IANA timezone the hours are shown in. Accepts auto for the viewer’s own timezone. Defaults to save-timezone, so nothing is converted until you ask for it. Changing this only re-labels the same underlying hours; the value never moves. string ''
saveTimezone
save-timezone
The IANA timezone the value is stored in. Defaults to UTC as soon as the schedule is timezone-aware (a display timezone is set, or the picker is shown), and to no timezone at all otherwise — in which case the times are stored exactly as they are shown. string ''
showTimezone
show-timezone
Shows the timezone picker, letting the user read the schedule in any timezone. boolean false
timezones The timezones offered by the picker, as IANA names or one of the named sets — en (the four US zones, the UK and Australia, under those names), offsets (one zone per UTC offset, the default), common (every offset plus the world’s major centres) or all (the complete IANA list). Names and sets can be mixed, e.g. en Asia/Tokyo. string[] []
referenceDate
reference-date
The date (YYYY-MM-DD) used to resolve timezone offsets. A weekly pattern has no date of its own, so one has to be picked to know whether daylight saving applies; today is used by default. string ''
disabled Disables the schedule. boolean false
readonly Renders the schedule without any editing affordances. boolean false
required Makes the schedule a required field, invalid until at least one period is open. boolean false
form The id of the form to associate with, when the control sits outside of it. string -
schedule The schedule as a plain object, with days in the save timezone. Assigning to it replaces the whole schedule. ScheduleValue -
exceptions The exceptions annotating the schedule. Also readable from, and written into, the value. ScheduleException[] -
validity Gets the validity state object. ValidityState -
validationMessage Gets the validation message. string -
displayedDays The hours as currently shown, in the display timezone. ScheduleDayMap -
updateComplete A read-only promise that resolves when the component has finished updating.

Learn more about attributes and properties.

Events

Name React Event Description Event Detail
zn-change Emitted when the schedule changes. -

Learn more about events.

Methods

Name Description Arguments
checkValidity() Checks validity but does not show a validation message. -
getForm() Gets the associated form, if one exists. -
reportValidity() Checks for validity and shows the browser’s validation message if the control is invalid. -
setCustomValidity() Sets a custom validation message. Pass an empty string to restore validity. message: string
setDay() Replaces the hours for a single day, in the save timezone. day: ScheduleDay, ranges: ScheduleRange[]
getDay() Reads the hours for a single day, in the save timezone. day: ScheduleDay
setDisplayDay() Replaces the hours for a single day, given in the display timezone. day: ScheduleDay, ranges: ScheduleRange[]

Learn more about methods.

Custom Properties

Name Description Default
--slot-height The height of a single time slot in the calendar. Defaults to 18px.
--gutter-width The width of the calendar’s time gutter. Defaults to 64px.
--open-color The fill used for open hours.
--reduced-color The fill used for hours an exception removes.

Learn more about customizing CSS custom properties.

Parts

Name Description
form-control The form control that wraps the builder, label and help text.
base The component’s base wrapper.
toolbar The row above the builder holding the hint, legend and view toggle.
calendar The calendar view wrapper.
list The form (list) view wrapper.
summary The summary panel beside the calendar.

Learn more about customizing CSS parts.

Dependencies

This component automatically imports the following dependencies.

  • <zn-example>
  • <zn-icon>
  • <zn-input>
  • <zn-opt-group>
  • <zn-option>
  • <zn-popup>
  • <zn-select>
  • <zn-slash-item>
  • <zn-slash-menu>
  • <zn-tag>
  • <zn-tooltip>