A Quarto shortcode extension for embedding TOAST UI Calendar in HTML output.
Note
All TOASTUI features are not supported by this extension. Interactive calendar input through the widget is not supported.
- Embed TOAST UI Calendar in Quarto HTML output
- Data input via YAML metadata, inline shortcode arguments, or text files
- Configuration via YAML metadata or inline shortcode arguments
- Multiple calendar views (month, week, day)
- Customizable event and popup details
- 12/24hr formats
- Timezone support
- Automatic time-grid hour-range fitting
- Responsive layout
- HTML format is supported
Add this extension to your project:
quarto add royfrancis/quarto-toastuiThis will create an _extensions/toastui/ folder in your project with all necessary files.
Define a calendar in YAML metadata. This example creates a calendar with a single event:
toastui:
calendar-1:
defaultView: week
calendars:
- id: cal1
name: Personal
backgroundColor: "#03bd9e"
events:
- id: "1"
calendarId: cal1
title: "Meeting"
start: "2026-04-09T09:00:00"
end: "2026-04-09T10:00:00"Then, add the shortcode to your document where you want the calendar to appear:
{{< toastui calendar-1 >}}For more examples and usage, see the extension docs website.
These are extension-level options handled directly by the shortcode.
| Parameter | Type | Default | Description |
|---|---|---|---|
| calendar key (positional) | string | none | Selects the metadata block at toastui.<key>. |
height |
string or number | 600px |
Calendar container height. Numeric values are coerced to pixels; string values are used as-is. |
timegridHeight |
string | 200% |
Height of the inner .toastui-calendar-timegrid element. Controls the scrollable time grid size in week/day views. |
navigation |
boolean-like | true |
Shows or hides the built-in navigation controls: prev, today, next, and month/week/day buttons. |
timeFormat |
'12h' | '24h' |
'24h' |
Sets one clock format for rendered timed event labels, time-grid labels, the current-time indicator, and the event detail popup. |
eventDetailItems |
string or string[] | [] |
Selects event fields to render inside timed events. Any field present in the event data is accepted; known TOAST UI fields like location, attendees, calendar, state etc. use their native icons, while custom fields render as field: value. |
popupDetailItems |
string or string[] | [] |
Selects event fields to render in the detail popup. Known TOAST UI fields use their native popup rows and icons; custom fields are appended as field: value. |
file |
string | none | Path to a delimited text file containing events. Absolute paths are used directly; relative paths are resolved against the input document directory. |
file-sep |
string | \t |
Delimiter used when parsing file. |
date |
string | unset | Initial calendar date passed to new Date(...). |
events |
array of objects | none | Inline event data from YAML metadata. Ignored if file is also provided. |
calendars |
CalendarInfo[] |
[] |
Calendar definitions used for labels and colors. |
These are passed into the Calendar constructor if present.
| Parameter | Type | Default | Description |
|---|---|---|---|
defaultView |
'month' | 'week' | 'day' |
'week' |
Sets the initial view mode. |
useFormPopup |
boolean |
false |
Enables the built-in event create/edit popup. Upstream date/time picker styles are also required when used. |
useDetailPopup |
boolean |
false |
Enables the built-in event detail popup. |
isReadOnly |
boolean |
upstream: false; extension default: true |
Makes the calendar non-editable. |
usageStatistics |
boolean |
upstream: true; extension default: false |
Controls TOAST UI usage statistics collection. |
eventFilter |
(event) => boolean |
(event) => !!event.isVisible |
Upstream option. Not forwarded by this extension because it isn't in the allowed constructor-option list, and YAML can't express a JavaScript function anyway. Use isVisible on individual events, or post-init custom JS, instead. |
gridSelection |
boolean | { enableClick?: boolean, enableDblClick?: boolean } |
true |
Configures click and double-click date selection behavior. |
timezone |
TimezoneOptions |
{ zones: [] } |
Configures calendar time zone handling. |
theme |
ThemeObject |
DEFAULT_THEME |
Applies TOAST UI theme customizations. |
template |
TemplateObject |
DEFAULT_TEMPLATE |
Provides custom render templates for events and labels. YAML can't express the real JavaScript functions TOAST UI expects here, so string values are instead treated as ${field} interpolation templates, see Templates. |
week |
WeekOptions |
DEFAULT_WEEK_OPTIONS |
Weekly and daily view configuration options. |
month |
MonthOptions |
DEFAULT_MONTH_OPTIONS |
Monthly view configuration options. |
autoHourRange |
boolean-like |
false |
Extension-only option (not forwarded to TOAST UI). Fills in whichever of week.hourStart/week.hourEnd isn't already set explicitly, from the exact min/max timed extent of the events, with no padding. Falls back to leaving the upstream 0/24 full-day default when there are no timed events. See Automatic Hour Range below. |
| Field | Type | Default | Description |
|---|---|---|---|
startDayOfWeek |
number |
0 |
Start day of week (0 Sunday to 6 Saturday). |
dayNames |
string[7] |
[] |
Optional custom labels for week/day views. |
narrowWeekend |
boolean |
false |
Narrows weekend columns in week/day views. |
workweek |
boolean |
false |
Excludes weekends in week/day views. |
showNowIndicator |
boolean |
true |
Shows current-time indicator in week/day view. |
showTimezoneCollapseButton |
boolean |
false |
Shows timezone collapse button when using multiple zones. |
timezonesCollapsed |
boolean |
false |
Starts sub-timezones collapsed. |
hourStart |
number |
0 |
Start hour for time grid. |
hourEnd |
number |
24 |
End hour for time grid. |
eventView |
boolean | ('allday' | 'time')[] |
true |
Controls allday/time event panels. |
taskView |
boolean | ('milestone' | 'task')[] |
true |
Controls milestone/task panels. |
collapseDuplicateEvents |
boolean | object |
false |
Duplicate event collapsing behavior. |
| Field | Type | Default | Description |
|---|---|---|---|
dayNames |
string[7] |
['sun','mon','tue','wed','thu','fri','sat'] |
Day labels in month view. |
startDayOfWeek |
number |
0 |
Start day of week (0 Sunday to 6 Saturday). |
narrowWeekend |
boolean |
false |
Narrows weekend columns in month view. |
visibleWeeksCount |
number |
0 |
Number of visible weeks (0 means six-week behavior). |
isAlways6Weeks |
boolean |
true |
Always render six rows in month view. |
workweek |
boolean |
false |
Excludes weekends in month view. |
visibleEventCount |
number |
6 |
Max visible events per day cell. |
For complete option schemas and semantics, see TOAST UI Calendar docs:
- https://nhn.github.io/tui.calendar/latest/
- https://github.com/nhn/tui.calendar
timezone:
zones:
- timezoneName: Europe/StockholmWrite event start/end as plain wall-clock timestamps (2026-10-05T09:00:00, no UTC offset) and set timezone.zones[0].timezoneName to the venue's IANA zone. The extension resolves each naive timestamp against that zone (DST-aware, via the browser's Intl API) before handing events to TOAST UI, so every viewer sees the same venue-local time regardless of their own device's timezone. A start/end that already carries an explicit offset or Z is left as-is. With no timezone configured, naive timestamps keep the upstream default: parsed as each viewer's own local clock.
Only zones[0] is used as the source zone for this resolution. Additional entries just add extra labeled time columns for comparison (timezoneName required, plus optional displayLabel/tooltip), see showTimezoneCollapseButton/timezonesCollapsed above and TOAST UI's own TimezoneConfig docs for the full shape.
The current-time indicator (week.showNowIndicator) isn't affected by this resolution. TOAST UI renders it from the browser's actual current moment, per configured zone, independent of event data.
Quick reference, what each combination actually displays:
Event start/end |
timezone.zones |
Displayed event time | Current-time indicator |
|---|---|---|---|
Absolute (...T09:00:00Z) |
Defined | Same wall-clock time for every viewer | Shared/fixed, the configured zone's current time, same for every viewer |
Absolute (...T09:00:00Z) |
Not defined | Correctly converted to each viewer's own local time | Per-viewer, each viewer's own real local time |
Naive (...T09:00:00) |
Defined | Same wall-clock digits for every viewer (venue-fixed) | Shared/fixed, the configured zone's current time, same for every viewer |
Naive (...T09:00:00) |
Not defined | Same literal digits shown to every viewer | Per-viewer, each viewer's own real local time |
week:
hourStart: 0 # omit, or set explicitly to opt out for that side
autoHourRange: trueSetting autoHourRange: true fills in whichever of week.hourStart/week.hourEnd you didn't set explicitly, based on the events shown:
- Scans each event's
start/end, ignoring all-day events. - Sets the range to the exact min/max timed extent of the events, with no padding.
- A timed event that spans midnight (its
startandendfall on different dates) is treated as spanning the full day, since a single hour window can't bound it. - If there are no timed events to measure,
hourStart/hourEndare left unset and the upstream0/24full-day default applies. - If you set one of
week.hourStart/week.hourEndexplicitly, that value is kept and only the other side is auto-fit.
This computation runs in the browser, once, when the calendar initializes (not in Lua at Quarto render time). That's intentional: when timezone.zones is set, event start/end values are repositioned to zones[0], the configured venue timezone, not necessarily the viewer's own clock (see Time Zones below), and that repositioning itself happens client-side. The scan measures each event's hour extent in the same zone TOAST UI actually draws it in — zones[0] when timezone is configured (so the computed range is identical for every viewer), otherwise each viewer's own local clock.
Limitation: week.hourStart/week.hourEnd is a single fixed window. If you configure timezone.zones with multiple named zones and a user-toggleable primary zone, the range is computed once at calendar init and does not refit if a viewer switches the primary zone afterward, the same is true of a manually-set hourStart/hourEnd today.
template:
milestone: "Custom: ${title}"Upstream template.* options are JavaScript functions (model) => string, which plain YAML cannot express. Instead, give any template.* entry a string, and the extension turns it into a function that substitutes ${field} placeholders with the matching property from the model object TOAST UI passes to that template (e.g. ${title}, ${start}, or a nested path like ${raw.project}); missing/null values resolve to an empty string, and the inserted values are HTML-escaped. This only applies to string values; template entries that are objects/arrays/numbers/booleans are passed through unchanged (and, per TemplateObject, upstream will ignore or error on those since it expects a function there too).
For which template names exist and what fields their model objects expose, see TOAST UI's own TemplateObject/Template docs.
Event objects (from events: YAML or event files) map to TOAST UI EventObject.
| Field | Type | Required | Description |
|---|---|---|---|
title |
string |
yes | Event title shown on the card. |
start |
string | number | Date |
yes | Event start date/time. |
end |
string | number | Date |
yes | Event end date/time. |
| Field | Type | Default | Description |
|---|---|---|---|
id |
string |
auto/internal if omitted | Event identifier. Recommended for updates/deletes. |
calendarId |
string |
none | Calendar id this event belongs to (should match a calendars[].id entry). |
title |
string |
none | Event title text. |
body |
string |
empty | Additional text content. |
category |
'milestone' | 'task' | 'time' | 'allday' |
inferred/upstream behavior | Event type affecting rendering panel. |
isAllday |
boolean |
false |
Marks event as all-day. |
start |
string | number | Date | TZDate |
none | Start date/time. |
end |
string | number | Date | TZDate |
none | End date/time. |
location |
string |
empty | Location label on event detail/card templates. |
attendees |
string[] |
[] |
Optional attendee list. |
state |
'Busy' | 'Free' | string |
hidden | Free/busy state. Only shown in event or popup details when selected and explicitly set. |
dueDateClass |
string |
empty | Optional class/tag used by task/milestone displays. |
recurrenceRule |
string |
empty | Recurrence rule text. |
isVisible |
boolean |
true |
Visibility flag (also used by default eventFilter). |
isPending |
boolean |
false |
Marks event as pending. |
isFocused |
boolean |
false |
Focus state metadata. |
isReadOnly |
boolean |
inherits calendar/global behavior | Per-event read-only override. |
isPrivate |
boolean |
false |
Marks event as private. |
color |
string |
inherited | Event text color. |
backgroundColor |
string |
inherited | Event card background color. |
dragBackgroundColor |
string |
inherited | Event background while dragging. |
borderColor |
string |
inherited | Event border color. |
- This extension forwards event fields directly to TOAST UI (
createEvents). - Event files are plain delimited text (TSV/CSV style): header names must match field names.
- String values
trueandfalsein files are converted to booleans. - Text color does not auto-contrast in this extension; set
colorexplicitly if needed. - When using
useDetailPopup: true, events should have acalendarIdthat references a definedcalendarsentry. Without this, the popup may render with incorrect colours and fail to dismiss on click. - Optional timed-event details are opt-in through
eventDetailItems; optional popup rows are independently opt-in throughpopupDetailItems. - Both options accept arbitrary event fields, including columns loaded from delimited files. Known TOAST UI fields retain their native icons; custom fields are labeled with their field names.
- A selected detail appears only when the event provides matching data; upstream defaults such as
state: "Busy"remain suppressed for events that do not define them. - For full upstream definitions, see EventObject docs:
The first line is a header row. Each following row becomes an event object.
- Column names become object keys
- true and false are converted to booleans
goingDurationandcomingDurationare converted to numbers- A non-empty
attendeesvalue is wrapped into a single-element array - Other values are left as strings
To pass extension validation, an event file must satisfy all of the following:
- Include a header row as the first line
- Include required columns:
title,start,end - Include at least one data row after the header
- Use the correct delimiter for the file and set
file-sepaccordingly (\tfor TSV,,for CSV)
If any of these requirements are not met, the extension does not crash. It renders a styled error message in the output document instead.
Example TSV file:
id calendarId title category start end isAllday location backgroundColor
1 cal1 Team Meeting time 2026-04-09T09:00:00 2026-04-09T10:00:00 false Room A #03bd9e
---
format: html
toastui:
calendar-1:
defaultView: week
height: 700px
calendars:
- id: cal1
name: Personal
backgroundColor: "#03bd9e"
events:
- id: "1"
calendarId: cal1
title: "Standup"
start: "2026-04-09T09:00:00"
end: "2026-04-09T09:30:00"
---
{{< toastui calendar-1 >}}---
format: html
toastui:
calendar-1:
defaultView: month
file: events.txt
file-sep: "\t"
---
{{< toastui calendar-1 >}}{{< toastui file="events.txt" file-sep="\t" defaultView="month" height="520px" >}}- RevealJS is supported but known to be flaky
- For other formats, the shortcode emits no output
- Short duration events may not be displayed legibly depending on the max day duration
- The calendar may not display legibly on very small screens
- TOAST UI Calendar keeps
themestate in a store shared across every calendar instance on the page, not per instance: setting a custom theme on one calendar can eventually change the appearance of other calendar instances on the same page too (including ones that render earlier in the document), regardless of their ownthemesetting. This extension works around it fortheme.common.backgroundColoronly (reinforced per instance via scoped CSS, so that one field stays isolated); othertheme.*fields remain plain pass-through and are still subject to this upstream behavior. If you rely on per-instance theming beyond the background color, test with every calendar that will appear on the same page.
2026 • Roy Francis