Month Planner
Month calendar that draws events and trips as bars across the days they cover.
c2-month-planner shows a plan on a month grid. Each entry in events is drawn as a bar from its start to its end
day, so a vacation reads as one strip with a clear beginning and end, continued on the next row when it crosses a
week. Overlapping entries stack in separate lanes. The header steps between months (and back to today); month
picks the one shown. The week starts on the locale’s usual day (Sunday in en-US, Monday in fr or en-GB,
Saturday in ar-EG); week-start (monday, sunday or saturday) overrides it. The planner’s text follows locale (fr,
en-GB, …) and, when it is not set, the browser’s language: month and day names, Today and “this month” come from the
browser’s Intl data, and the navigation labels are built in for English, French, German, Spanish, Italian, Portuguese,
Dutch and Vietnamese, falling back to English otherwise.
To jump further, click the month title: a picker shows the twelve months of a year with a year stepper above them. A
dot and a soft tint mark every month that has events, so a plan months away is two or three clicks. Arrow keys move between months, Page Up and Page Down change the year,
Enter picks and Escape closes. Set month-picker="false" for a plain title.
Import
import '@c2n/components/month-planner'The examples use icons from the separate @c2n/feather-icons package: npm install @c2n/feather-icons.
Usage
Tuesday, September 1
Team holidays
Tuesday, September 1
No events
Tuesday, September 1
No events
Heading and actions
heading puts a title of the planner’s own in front of the month (--c2-month-planner__heading--font-size,
--c2-month-planner__heading--font-weight) and becomes the planner’s accessible name, unless aria-label is set;
without it, the month names the planner. For more than plain text, put your own markup in the heading slot, such as
a field that renames the planner. It replaces the heading text and names the planner unless heading or
aria-label is set.
The actions slot puts controls of your own at the end of the header, before the month navigation, e.g. an “Add
event” button or a settings menu, spaced by --c2-month-planner__actions--gap. On a narrow planner the header wraps
instead of overflowing. The planner leaves what they do to you.
<c2-month-planner heading="Team holidays">
<c2-button class="planner-add" slot="actions"><c2-feather-plus slot="prefix-icon"></c2-feather-plus>Add event</c2-button>
<c2-icon-button class="planner-icon-action" slot="actions" tooltip="Settings" aria-label="Settings"><c2-feather-settings></c2-feather-settings></c2-icon-button>
</c2-month-planner>Events
Each entry is { id?, title, start, end?, color?, textColor? }. Dates are local YYYY-MM-DD strings and end is inclusive;
leave it out for a one-day entry. color is any CSS colour for that bar (its text stays white in both themes; add textColor for a light color),
otherwise --c2-month-planner__event--background applies. Entries without a valid start are skipped.
Set events as a property from code, or as a JSON attribute in markup. Bars are buttons: clicking one (or pressing
Enter on it) fires event-click with the entry in event.detail.event. The header fires month-change with the
month now shown (YYYY-MM), which is where to load the next month’s plan.
import '@c2n/components/month-planner'
const planner = document.querySelector('c2-month-planner')!
planner.events = [{ id: 'trip', title: 'Vacation', start: '2026-09-10', end: '2026-09-18' }]
planner.addEventListener('event-click', (event) => console.log(event.detail.event.id))
planner.addEventListener('month-change', (event) => console.log(event.detail.month))Editing
Set editable to plan on the grid itself. The planner reports what the user did and leaves the change to you: it
never rewrites events, so apply each change (and save it) in the handler, and the grid redraws from the new list.
- A click on a day’s empty area, or Enter or Space on a focused day, fires
day-clickwithdetail.date(YYYY-MM-DD). Arrow keys move between days. Days of the neighbouring months shown in the grid count too. - Pressing on one day and releasing on another highlights the days between
(
--c2-month-planner__day__range--background) and firesrange-selectwithdetail.startanddetail.end, in order and inclusive. Escape cancels. - Dragging an event bar to another day moves it, keeping its length; dragging its start or end edge
(
--c2-month-planner__handle--width) changes that date. The bar follows the pointer while it is dragged, Escape puts it back, and a press that does not move stays anevent-click. On drop,event-changecarries the entry indetail.eventand its new dates indetail.changes({ start, end },endset even for a one-day entry). - On a focused bar, Alt+Arrow Left/Right moves it by a day, Alt+Arrow Up/Down by a week, and Alt+Shift+Arrow
Left/Right changes its end; each fires
event-change, and focus stays on the bar once the neweventsarrive.
A compact planner fires day-click on a tap as well. None of these events bubble: listen on the planner.
const planner = document.querySelector('c2-month-planner')!
planner.editable = true
planner.addEventListener('day-click', (event) => openNewEventForm({ start: event.detail.date }))
planner.addEventListener('range-select', (event) => openNewEventForm(event.detail))
planner.addEventListener('event-change', (event) => {
const { event: entry, changes } = event.detail
planner.events = planner.events.map((item) => (item === entry ? { ...item, ...changes } : item))
})Narrow screens
Below 480px of width the planner turns compact, the way a phone calendar does: each day is its date over one dot per
event (three at most), a tap picks a day, and that day’s events are listed under the month as buttons that fire the
same event-click. Arrow keys move between days, crossing into the next or previous month. It opens on today, or on
the 1st when another month is shown. The breakpoint follows the planner’s own width, not the viewport’s.
Tuesday, September 1
In a layout that sizes it from its content, such as a centred flex column, the planner takes 720px or the space
available, whichever is smaller; give it a width to choose another.