Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Scheduler

Scheduler is a calendar with day, week, work-week, month, and year views and full event create, edit, and delete, this library's first feature. It's built on the same date utilities date picker uses, and on two independent sub-elements documented on their own pages: the event chip shown on the calendar grid, and the event editor used to create and edit them.

Dependencies

eun-icon · for the nav chevrons eun-button · throughout the header and editor footers eun-toggle · for the desktop view switcher eun-dropdown-button · for the view switcher below the mobile breakpoint eun-drawer · for the desktop editing surface eun-modal · for the mobile editing fallback, below the mobile breakpoint
Overview API Examples Accessibility

When to use

Reach for scheduler whenever someone needs to see and manage events against a calendar, such as a booking system, a team's shared agenda, or a personal planner. It's a full feature, not a single field: it owns its own navigation (previous, next, and today, across five view granularities) and its own create, edit, and delete flow, rather than being a display-only grid you'd wire interactions around yourself.

It is not form-associated, since there's no single value a calendar of many events could sensibly represent. Instead it's a controlled display component: pass its events in, and listen for its create, update, and delete events to keep your own copy of that array in sync (see "Install and usage" below). Scheduler never mutates the array you pass it.

Clicking an event never jumps straight into editing it. It opens a read-only detail popover first, listing the title, full date and time (or "All day" for an all-day event), location, description, notes, and priority, each only shown if actually set, along with its own Edit and Delete buttons. Only Edit actually opens the create and edit form. This keeps a stray click from accidentally starting an edit, and gives a fast "what's this?" glance that doesn't need the heavier form at all. See "Event details in the popover" under Examples for every field combination shown side by side.

Five views are available, switched via the header's own segmented control:

  • Day: a single-column hour grid for one day.
  • Week (the default): a seven-column hour grid for a full week.
  • Work week: the same hour grid with weekend columns dropped entirely.
  • Month: a full six-week grid, showing as many events as actually fit each day's cell before collapsing the rest behind a "+N more" affordance.
  • Year: twelve mini month grids, with a plain dot marking days that have events.

An all-day or multi-day event, one lasting a full day or spanning several calendar days, never appears inside the timed hour grid. It renders instead as a bar in a dedicated all-day row above it, spanning every day column it covers. See "All-day and multi-day events" under Examples.

Install & usage

npm install @eunomia/elements
import "@eunomia/elements/scheduler.js";
<eun-scheduler id="scheduler"></eun-scheduler>

<script>
  const scheduler = document.querySelector("#scheduler");
  let events = [
    {
      id: "1",
      title: "Team stand-up",
      color: "blue",
      start: new Date(2026, 2, 16, 9, 0),
      end: new Date(2026, 2, 16, 9, 30),
    },
  ];
  scheduler.events = events;

  scheduler.addEventListener("euneventcreate", (event) => {
    events = [...events, event.event]; // event.event already has a generated id
    scheduler.events = events;
  });
  scheduler.addEventListener("euneventupdate", (event) => {
    events = events.map((e) => (e.id === event.event.id ? event.event : e));
    scheduler.events = events;
  });
  scheduler.addEventListener("euneventdelete", (event) => {
    events = events.filter((e) => e.id !== event.eventId);
    scheduler.events = events;
  });
</script>

Importing scheduler.js also registers eun-scheduler-event and eun-scheduler-event-editor, since it renders both internally, alongside the dependencies listed above, which you'll want to import explicitly if you register components individually rather than through a barrel that already does it.

Scheduler delegates the calendar grid itself (view rendering, swipe-to-navigate, the mobile day-selector strip) entirely to Calendar, which in turn renders the event chip for every event shown. Around that, scheduler renders the event editor for its create and edit form, plus the components listed above for its own chrome.

Guidance

  • Listen for eunperiodchange to lazy-load events for the newly visible range instead of loading every event up front: it fires on every nav/swipe/view-switch change with the exact rangeStart/rangeEnd now visible
  • Keep events as the single source of truth on your side, reassigned (not mutated) from the three CRUD listeners: eun-scheduler is deliberately uncontrolled beyond that, the same "controlled array" pattern eun-scheduler-event-editor documents on its own
  • Set readonly for a view-only agenda (a public event calendar, a read-only shift schedule) rather than hiding the create/edit affordances with CSS: the detail popover still opens on click, so consultation stays available, it just hides its own Edit/Delete buttons
  • Expecting per-slot keyboard focus in the day/week/work-week grid: empty-slot creation there is pointer-only by design (see the Accessibility tab), so keyboard users create events through the header's "New event" button
  • Recurring events: each occurrence needs its own entry in events, since there's no recurrence rule concept
  • Relying on the month view's "+N more" to show a full day's agenda inline: it switches to day view for that date instead of opening a second floating list, by design (one editing surface, one navigation model)

Live testing

Properties

Scheduler <eun-scheduler>

Attributes

NameTypeDefaultDescription
event-variant'default' | 'transparent' | 'ghost''default'The visual variant applied to day, week, and work week event chips. Month view is unaffected, always picking ghost, or default for multi-day bars, itself
view'day' | 'week' | 'work-week' | 'month' | 'year''week'The active calendar view
week-starts-on0 | 11Which weekday a week starts on
day-start-hournumber0The first visible hour row in day, week, and work week
day-end-hournumber24The last visible hour row, exclusive, in day, week, and work week
slot-durationnumber30The grid's row granularity, in minutes, also the default duration for a newly created event
readonlybooleanfalseDisables creating, editing, and deleting. View only
no-animationbooleanfalseDisables the mobile swipe slide animation and the drawer or modal's own transitions
today-labelstring'Today'The label of the jump-to-today button
new-event-labelstring'New event'The label of the create-a-new-event button
aria-label-previousstring'Previous period'The accessible label of the previous-period button
aria-label-nextstring'Next period'The accessible label of the next-period button
view-day-labelstring'Day'The label of the day view switcher option
view-week-labelstring'Week'The label of the week view switcher option
view-work-week-labelstring'Work week'The label of the work week view switcher option
view-month-labelstring'Month'The label of the month view switcher option
view-year-labelstring'Year'The label of the year view switcher option
apply-labelstring"Apply"
cancel-labelstring"Cancel"
delete-labelstring"Delete"
edit-labelstring"Edit"Label of the read-only event popover's "switch to editing" button.
close-labelstring"Close"Accessible label of the read-only event popover's close (✕) button.
more-events-labelstring'+{count} more'The template for the month view's overflow affordance, with the placeholder replaced by the hidden event count

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
eventsArray<EunomiaSchedulerEvent>[]The events to display. Never mutated internally
displayedDateDatenew Date()The anchor date for the active view. Defaults to today, self-managed on navigation, swipe, or view switch, always overridable from outside
localestringThe locale used throughout. Defaults to the runtime's own
dictionaryThis instance's locale-resolved string dictionary — see the class doc for the resolution order.

Events

NameTypeDescription
eunperiodchangePeriodChangeEventFired whenever the visible period changes, from navigation, a swipe, a view switch, or an external view or displayedDate assignment
euneventcreateEventCreateEventFired when a new event is saved
euneventupdateEventUpdateEventFired when an existing event is saved, or a day, week, or work week chip is dragged to a new time or day
euneventdeleteEventDeleteEventFired when an event is deleted

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--scheduler-border-colorSets the border color throughout the shell and grids
--scheduler-backgroundSets the background color
--scheduler-slot-heightSets the pixel height per slotDuration row in day, week, and work week
--scheduler-gutter-widthSets the width of the hour-label gutter
--scheduler-body-max-heightSets the maximum height, with scroll, of the day, week, and work week grid body
--scheduler-month-cell-min-heightSets the minimum height of month view cells
--scheduler-swipe-durationSets the duration of the mobile swipe slide animation

The event chip's and event editor's own properties and events are documented on their own pages. See Event: API and Event editor: API.

Day view

A single-column hour grid: the finest-grained view, best for a focused look at one day's schedule.

<eun-scheduler view="day"></eun-scheduler>

Week and work week

week (the default) renders all 7 days, while work-week drops Saturday/Sunday entirely (a 5-column grid), matching eun-date-picker's own work-week mode.

<eun-scheduler view="work-week"></eun-scheduler>

Month view

Each week shows as many event rows as actually fit its fixed cell height (measured live, not a hardcoded count), so a taller --scheduler-month-cell-height always shows more before falling back to a "+N more" affordance. Today (below) is deliberately overbooked to show that affordance in action. Clicking either the day number or "+N more" switches to day view for that date.

A multi-day event (allDay, or just an end on a later calendar day than start, the same rule the week view's all-day row uses) renders as a single continuous bar spanning every day cell it covers, exactly like a typical calendar app, rather than a separate chip repeated in each day it happens to touch:

scheduler.events = [
  {
    id: "offsite",
    title: "Company offsite",
    color: "purple",
    allDay: true,
    start: new Date(2026, 2, 17), // Tuesday
    end: new Date(2026, 2, 20), // Friday — spans 4 days as one bar
  },
];
<eun-scheduler view="month"></eun-scheduler>

Year view

A 12-month overview, where a plain dot marks days with at least one event, not color-coded, since that would be illegible at this scale. Click a day to jump to day view, or a month's name to jump to month view for it.

<eun-scheduler view="year"></eun-scheduler>

All-day and multi-day events

Set allDay (or just give end a later calendar day than start) and the event renders as a bar in the all-day row above the hour grid instead of inside it, spanning every visible day column it covers and stacked into extra rows when several overlap.

scheduler.events = [
  {
    id: "conf",
    title: "Company offsite",
    color: "purple",
    allDay: true,
    start: new Date(2026, 2, 17), // Tuesday
    end: new Date(2026, 2, 19), // Thursday — spans 3 days
  },
];

Event colors

Six accent colors (the library's decorative token scale), each an event's color, set per event in the data you pass to events, not a property of eun-scheduler itself. See Event: Examples for every color/variant combination shown side by side.

Localizing labels

Every piece of visible text the header, footer, and popover render is its own overridable property: the "Today" and "New event" buttons, the previous/next arrows' accessible labels, each of the five view names, and the Apply/Cancel/Delete/Edit/Close actions across the drawer, modal, and popover. Override as many as your own locale needs; the ones left alone keep their English default.

<eun-scheduler
  today-label="Aujourd'hui"
  new-event-label="Nouvel évènement"
  aria-label-previous="Période précédente"
  aria-label-next="Période suivante"
  view-day-label="Jour"
  view-week-label="Semaine"
  view-work-week-label="Semaine ouvrée"
  view-month-label="Mois"
  view-year-label="Année"
  apply-label="Enregistrer"
  cancel-label="Annuler"
  delete-label="Supprimer"
  edit-label="Modifier"
  close-label="Fermer"
></eun-scheduler>

Click an event above and open its popover, or the "New event" button, to see the translated Edit/Delete/Apply/Cancel/Close labels alongside the header's own.

Read-only

readonly hides the "New event" button and the detail popover's Edit/ Delete buttons, for a public/view-only agenda. Clicking an event still opens the read-only popover, there's just nothing in it to act on.

<eun-scheduler readonly></eun-scheduler>

Moving an event by drag

A timed event chip in day/week/work-week can be dragged (mouse/pen only) to a different time and/or day, not just clicked open: grab it, a floating ghost follows the pointer with a live-updating time label, and dropping it fires the exact same euneventupdate a form save does (see the demo at the top of this page, nothing extra to wire up if you're already listening for it there, reassigning events the same way). readonly disables it, same as the "New event" button and the popover's Edit action. The mechanics themselves (the ghost, the snapping, the cross-day hit-testing) live on eun-calendar. See Calendar: Moving an event by drag for the full details, including why it's mouse/pen-only and how a keyboard user reschedules an event instead.

Event details in the popover

The read-only detail popover only ever shows the fields a given event actually has set: nothing more, nothing padded out with placeholders. Click each event below to see the difference: Team sync has just a title and time, Client call adds a location, Design review sets every optional field at once (location, description, notes, priority, and guests, where the description and notes rows each get their own icon so they're never mistaken for one another, and each guest's email shows a status icon: a check for confirmed, an hourglass for pending, and a cross for declined), and Public holiday/Company offsite are allDay (a single day and a multi-day span, respectively), which prefixes the date line with "All day" instead of a start–end time.

scheduler.events = [
  {
    id: "minimal",
    title: "Team sync",
    color: "blue",
    start: new Date(2026, 2, 16, 9, 0),
    end: new Date(2026, 2, 16, 9, 30),
  },
  {
    id: "location-only",
    title: "Client call",
    color: "red",
    location: "Zoom",
    start: new Date(2026, 2, 16, 11, 0),
    end: new Date(2026, 2, 16, 11, 30),
  },
  {
    id: "full",
    title: "Design review",
    color: "purple",
    location: "Room A",
    description: "Walk through the new onboarding flow.",
    notes: "Bring the latest Figma export.",
    priority: "high",
    guests: [
      { email: "amelie@example.com", status: "confirmed" },
      { email: "noah@example.com", status: "pending" },
    ],
    start: new Date(2026, 2, 16, 14, 0),
    end: new Date(2026, 2, 16, 15, 0),
  },
  {
    id: "all-day",
    title: "Public holiday",
    color: "green",
    allDay: true,
    start: new Date(2026, 2, 17),
    end: new Date(2026, 2, 17),
  },
  {
    id: "multi-day",
    title: "Company offsite",
    color: "yellow",
    allDay: true,
    start: new Date(2026, 2, 18),
    end: new Date(2026, 2, 20), // spans 3 days
  },
];

Mobile layout

Below 768px: the desktop eun-toggle-group view switcher becomes a eun-dropdown-button (5 spelled-out options don't fit a phone width), the period label sits between the prev/next arrows, and editing switches from the desktop eun-drawer side panel to a eun-modal with an explicit Apply/Cancel(/Delete) footer, the same split eun-date-picker uses. The calendar itself also gains touch swipe-to-navigate (prev/next period, with the same slide-in replay animation eun-date-picker's own mobile swipe uses), and horizontal touch panning is reserved for it, so a finger-drag always changes the period rather than fighting a horizontal scrollbar.

The preview below is pinned to a real 375px-wide iframe so it always renders the mobile layout, right here, seeded with a couple of events. Tap "Team sync" to see the read-only detail popover, then its Edit button to open the modal editor. Tap "New event" to create one from scratch, or swipe the calendar left/right on a touch device.

Building your own scheduler

Everything above uses eun-scheduler as a whole. Its own calendar grid (view rendering, swipe-to-navigate, the mobile day-selector strip) is itself a standalone, independently usable layout primitive: eun-calendar. Composing your own scheduler UI means driving eun-calendar yourself (events/view/displayedDate down, euneventclick/eunslotclick/ eunperiodchange back up) around your own chrome, rather than forking the whole feature, the same way eun-scheduler-event-editor plugs into whatever surface you put around it. The example below builds a compact week calendar with its own centered header (built from plain <button>s and a native <select> view switcher, not eun-toggle-group/eun-dropdown-button) and a eun-modal that lets the editor's own built-in Save/Cancel/Delete footer drive everything: hide-actions goes on the modal instead (so its default Confirm/Cancel pair doesn't double up with the editor's own), and the editor itself needs no hide-actions at all, unlike eun-scheduler's own unify-the-footer approach. No drawer, no popover, a simpler composition for a simpler surface. Full code below.

+ New event
<div id="custom-scheduler">
  <header>
    <eun-button id="custom-new" size="s" variant="primary"
      >+ New event</eun-button
    >
    <button type="button" id="prev" aria-label="Previous week"></button>
    <strong id="period-label"></strong>
    <button type="button" id="next" aria-label="Next week"></button>
    <select id="view" aria-label="Calendar view">
      <option value="day">Day</option>
      <option value="week" selected>Week</option>
      <option value="work-week">Work week</option>
      <option value="month">Month</option>
    </select>
  </header>
  <eun-calendar id="calendar" view="week"></eun-calendar>
  <eun-button id="new-event" variant="primary">+ New event</eun-button>
</div>

<eun-modal id="event-modal" heading="Event" hide-actions>
  <eun-scheduler-event-editor id="event-editor"></eun-scheduler-event-editor>
</eun-modal>

<script type="module">
  import "@eunomia/elements/calendar.js";
  import "@eunomia/elements/scheduler-event-editor.js";
  import "@eunomia/elements/modal.js";

  const calendar = document.querySelector("#calendar");
  const labelEl = document.querySelector("#period-label");
  const modal = document.querySelector("#event-modal");
  const editor = document.querySelector("#event-editor");

  let events = [
    // your own event objects — see EunomiaSchedulerEvent's own shape
  ];
  calendar.events = events;

  function openEditor(event, initialStart) {
    editor.event = event ?? null; // null => create mode
    if (!event) {
      editor.initialStart = initialStart;
      editor.initialEnd = new Date(initialStart.getTime() + 60 * 60 * 1000);
    }
    modal.heading = event ? event.title : "New event";
    modal.open = true;
  }

  // eun-calendar only reports intent — it never opens an editor itself.
  calendar.addEventListener("euneventclick", (e) => openEditor(e.event));
  calendar.addEventListener("eunslotclick", (e) => openEditor(null, e.start));
  calendar.addEventListener("eunperiodchange", (e) => {
    const { rangeStart, rangeEnd } = e;
    labelEl.textContent = `${rangeStart.toLocaleDateString(undefined, { day: "numeric", month: "short" })}${rangeEnd.toLocaleDateString(undefined, { day: "numeric", month: "short" })}`;
  });
  // Re-triggers the calendar's own first eunperiodchange in case its
  // initial render already happened before the listener above attached.
  calendar.displayedDate = new Date(calendar.displayedDate.getTime());

  // The editor dispatches these regardless of what wraps it — a drawer, a
  // modal, or (like here) nothing at all beyond your own <div>.
  editor.addEventListener("euneventcreate", (e) => {
    events = [...events, e.event];
    calendar.events = events;
    modal.open = false;
  });
  editor.addEventListener("euneventupdate", (e) => {
    events = events.map((existing) =>
      existing.id === e.event.id ? e.event : existing,
    );
    calendar.events = events;
    modal.open = false;
  });
  editor.addEventListener("euneventdelete", (e) => {
    events = events.filter((existing) => existing.id !== e.eventId);
    calendar.events = events;
    modal.open = false;
  });

  document.querySelector("#prev").addEventListener("click", () => {
    const next = new Date(calendar.displayedDate);
    next.setDate(next.getDate() - 7);
    calendar.displayedDate = next;
  });
  document.querySelector("#next").addEventListener("click", () => {
    const next = new Date(calendar.displayedDate);
    next.setDate(next.getDate() + 7);
    calendar.displayedDate = next;
  });
  document.querySelector("#view").addEventListener("change", (e) => {
    calendar.view = e.target.value;
  });
  document.querySelector("#new-event").addEventListener("click", () => {
    const start = new Date();
    start.setMinutes(0, 0, 0);
    start.setHours(start.getHours() + 1);
    openEditor(null, start);
  });
</script>

<style>
  #custom-scheduler header {
    display: flex;
    align-items: center;
    justify-content: center; /* the "centered header" composition */
    gap: 12px;
  }
</style>

Reordering a calendars list

eun-scheduler/eun-calendar render events, not the "my calendars" sidebar list a real scheduler is usually built alongside: that list is just your own markup, composed the same way the custom header above is. Being plain markup means it's also just a container and its direct children as far as createDragReorder (the framework-agnostic drag-and-drop utility) is concerned, the same way it would attach to any other list. Drag a calendar by its handle to reorder it, or focus the handle and press Space, then the arrow keys, then Space again. Toggle its checkbox to show or hide its events on the week view alongside it.

  • Work
  • Design reviews
  • Personal
  • 1:1s
  • Clients
  • Releases
<ul id="calendars">
  <li data-color="blue">
    <button class="drag-handle" aria-label="Reorder Work">
      <eun-icon name="drag_indicator"></eun-icon>
    </button>
    <span class="dot" style="background: var(--eun-color-blue-500);"></span>
    <span>Work</span>
    <eun-checkbox checked aria-label="Show Work events"></eun-checkbox>
  </li>
  <!-- ...more calendars... -->
</ul>
<eun-calendar id="calendar" view="week"></eun-calendar>
import { createDragReorder } from "@eunomia/elements";

const list = document.querySelector("#calendars");

createDragReorder(list, {
  items: () => Array.from(list.children),
  handle: ".drag-handle",
  onReorder: ({ order }) => {
    // Persist your own sidebar order, if it should survive a reload —
    // `order` is the `<li>`s themselves, already in their new DOM order.
    myCalendarOrder = order.map((li) => li.dataset.color);
  },
});

Reordering a calendars list like this has nothing to do with the calendar grid's own event dragging (moving an event to a different day/time). That's a distinct problem, mapping a pointer position to a calendar cell, and isn't what createDragReorder does. See the drag-and-drop utility's own page for the boundary between the two.

Keyboard interactions

Key Action
Tab / Shift+Tab Moves focus through the header controls, the active view's cells/chips, and (once open) the popover/editor
ArrowLeft/ArrowRight/ArrowUp/ArrowDown (month view) Moves the roving-tabindex focus by day/week
Home / End (month view) Jumps focus to the start/end of the focused week
PageUp / PageDown (month view) Changes the displayed month (Shift for a year instead)
Enter / Space (nav/today) Moves the displayed period / jumps to today
Enter / Space (view switcher) Changes the active view
Enter / Space (an event chip) Opens the read-only detail popover for that event (consultation mode)
Enter / Space (the popover's Edit button) Closes the popover and opens the editor, pre-filled with that event
Enter / Space (the popover's close button) Closes the popover without acting on the event, same as Escape
Enter / Space ("New event") Opens the editor in create mode, seeded at the next hour
Escape (popover open) Closes the popover (native Popover API light-dismiss), focus returns to the chip that opened it
Escape (editor open) Closes the drawer/modal, discarding unsaved changes (the drawer's built-in unsaved-changes guard confirms first if the form is dirty)

Month view day cells follow the same roving-tabindex pattern eun-date-picker's own day grid uses: ArrowLeft/ArrowRight/ArrowUp/ ArrowDown move focus by day/week, Home/End jump to the focused day's week bounds, PageUp/PageDown (Shift for a year instead of a month) change the displayed month, and Enter/Space activate the focused cell (a native <button>, no custom handling needed for those two). Moving focus past the currently displayed month's grid updates displayedDate to bring the new month into view, same as eun-date-picker's own PageUp/PageDown. Year view does not follow this pattern, see below.

Deliberately bounded: empty-slot creation in day/week/work-week

Creating an event by clicking a blank spot on the hour grid is pointer/ touch-only: there's no per-slot keyboard focus target. A full roving-tabindex 2D grid over every 30-minute slot across up to 7 columns would mean dozens of tab stops for one view, a worse keyboard experience than the alternative: the header's "New event" button, always reachable in one tab stop, opens the same editor seeded at a sensible default time. Event chips themselves remain fully keyboard-focusable/operable (native <button>s), since there are far fewer of them than there are slots.

Dragging a chip to move it is bounded the same way, and mouse/pen only even among pointer types (a touch press on a chip scrolls/swipes instead of dragging). A keyboard user reschedules an event through the editor instead: Enter/Space on the chip opens the popover, its Edit button opens the same editor a drag would have produced an equivalent euneventupdate from, just via a fully keyboard-operable form.

Year view is similarly a coarse, primarily mouse/touch-driven overview (as in most calendar apps) rather than a roving-tabindex surface. Its day cells are still real, individually reachable <button>s, just without the arrow-key roving month/day grids get.

Aria attributes and rules

  • The period label is role="status" aria-live="polite", so a screen reader announces the new period on every nav/swipe/view-switch change. It sits between the prev/next buttons visually, but that's a pure CSS ordering choice, and the buttons still have their own explicit aria-labels rather than relying on visual proximity to the label.
  • Below the mobile breakpoint, the view switcher becomes a eun-dropdown-button (its own role="menu"/roving-focus keyboard pattern, see that component's own Accessibility tab) instead of eun-toggle-group's roving-tabindex segmented control. Both expose the same 5 choices, just through a different, already-accessible control.
  • Month/year view day cells expose a full aria-label (weekday, day, month, year) rather than just the bare day number.
  • eun-scheduler-event renders as a native <button> with an aria-label combining the title and time (and end time, when known). See its own Accessibility tab.
  • The detail popover is a native [popover] element (not eun-tooltip, since that component is non-interactive by ARIA convention, the wrong fit for a panel holding focusable Edit/Delete/close buttons), positioned against the clicked chip the same way eun-date-picker positions its own calendar panel. Its own light-dismiss (outside click, Escape) and return-focus behavior come from the native Popover API, not a custom reimplementation.
  • The editor's own dialog semantics (focus trap, Escape handling, labelled heading) come entirely from eun-drawer/eun-modal: see their own Accessibility tabs for the full contract, and nothing about those is reimplemented here.
  • The header (navigation, view switcher, "New event") is position: sticky, staying pinned to the top of whichever ancestor actually scrolls as the calendar body scrolls past it (a no-op if nothing outside the component scrolls). The day/week/work-week grid separately scrolls both horizontally and vertically within itself on narrow viewports, with its own header row and hour gutter each independently sticky, so day labels and hour labels both stay in view.