Skip to content
3 Death Saves

Widgets

A module can add buttons and indicators to the HUD, MiniCal, and BigCal, or replace one of Calendaria’s own indicators with markup of its own.


PointLocation
hud.buttons.leftLeft side of the HUD bar
hud.buttons.rightRight side of the HUD bar
hud.indicatorsIndicator row of the HUD bar, MiniCal, and BigCal
hud.trayHUD time controls tray
minical.sidebarMiniCal sidebar
bigcal.actionsBigCal action bar

Read these from CALENDARIA.api.widgetPoints.

A point controls only where a widget renders. Any of the three widget types renders at any point, and registration does not reject a type and point combination.

Despite its name, hud.indicators is the only point that renders in more than one application. A widget registered there appears in the HUD indicator row, the MiniCal indicator row, and the BigCal header indicator row, and its render function receives 'hud', 'minical', or 'bigcal' to match. The MiniCal indicator row is drawn whenever a widget targets this point, even when no built-in indicator is enabled.

The HUD time controls tray is only built for users permitted to change the date and time. Widgets at hud.tray are absent for everyone else, so do not put anything a player must reach there. See Permissions.


Element IDDescription
weather-indicatorWeather condition display
season-indicatorCurrent season display
era-indicatorEra/year display
cycle-indicatorCycle (zodiac, etc.) display

Read these from CALENDARIA.api.replaceableElements. A replacement takes over that indicator wherever it appears, in the HUD, the MiniCal, and the BigCal alike.


Widgets registered during calendaria.ready appear on the first render of every Calendaria application.

Hooks.once('calendaria.ready', () => {
CALENDARIA.api.registerWidget('my-module', {
id: 'my-button',
type: 'button',
insertAt: 'hud.buttons.right',
icon: 'fas fa-star',
label: 'My Button',
tooltip: 'Click to do something',
onClick: () => {
console.log('Button clicked!');
}
});
});

registerWidget returns true on success and false when it refuses the widget, and logs a console warning naming the reason. It refuses a missing moduleId or config.id, a config carrying neither insertAt nor replaces, an insertAt or replaces that is not one of the recognized values, a moduleId.id that is already registered, and a built-in element another widget has already claimed. Replacements are first come: a second widget cannot take an element away from the first. There is no call that unregisters a widget once it has been accepted.

An indicator renders from its icon, label, color, and tooltip properties:

CALENDARIA.api.registerWidget('my-module', {
id: 'mana-indicator',
type: 'indicator',
insertAt: 'hud.indicators',
icon: 'fas fa-gem',
label: 'Mana: 50',
color: '#3aa0ff',
tooltip: 'Current mana',
onAttach: (element) => {
// Called when the widget is added to the DOM
}
});

A type: 'custom' widget supplies a render function. The function receives the location string ('hud', 'minical', or 'bigcal') and returns an HTML string, which Calendaria wraps in a <div class="calendaria-widget-custom">:

CALENDARIA.api.registerWidget('my-module', {
id: 'mana-display',
type: 'custom',
insertAt: 'hud.indicators',
render: (location) => {
return `
<span class="indicator-icon"><i class="fas fa-gem"></i></span>
<span class="indicator-label">Mana: 50</span>
`;
},
onAttach: (element) => {
// Called when the widget is added to the DOM
}
});

A replacement that needs a render function must use type: 'custom'. The render function receives the location string and returns an HTML string:

CALENDARIA.api.registerWidget('my-module', {
id: 'custom-weather',
type: 'custom',
replaces: 'weather-indicator',
render: (location) => {
const weather = CALENDARIA.api.getCurrentWeather();
return `<span>Custom: ${weather?.label || 'None'}</span>`;
}
});

Set disabled: true on a replaces widget to hide the built-in element and render nothing in its place. It works for any widget type, and it is ignored on a widget registered with insertAt.


PropertyTypeDescription
idstringUnique widget identifier within your module
typestring'button', 'indicator', or 'custom'

One of these is required:

PropertyTypeDescription
insertAtstringWidget point ID from widgetPoints
replacesstringElement ID from replaceableElements
PropertyTypeDescription
iconstring | functionFontAwesome icon class
labelstring | functionButton text. Rendered only when no icon is supplied, since an icon replaces the label rather than sitting beside it.
tooltipstring | functionHover tooltip text
colorstring | functionIcon color, as a CSS color value
onClickfunctionClick handler, receives the event
PropertyTypeDescription
iconstring | functionFontAwesome icon class
labelstring | functionIndicator text
colorstring | functionColor of the icon and the label, as a CSS color value
tooltipstring | functionHover tooltip text
onClickfunctionClick handler, receives the event. Also adds a clickable class that switches the cursor to a pointer.
onAttachfunctionCalled with the DOM element when the widget is attached
PropertyTypeDescription
renderfunctionReceives the location string and returns an HTML string
onClickfunctionClick handler, receives the event. Bound to the wrapper, so a click anywhere in your markup reaches it.
onAttachfunctionCalled with the DOM element when the widget is attached

On button and indicator widgets, icon, label, color, and tooltip each take a function in place of a literal. Calendaria calls it every time the widget renders and uses what comes back, which is how a widget shows a value that changes. A custom widget builds its own markup instead, so these four are ignored there.

onAttach runs after every render of the application the widget sits in, not only the first, so attach listeners and read state there rather than assuming a single call. A widget at hud.indicators is attached separately in each application it appears in.


// Get all widgets at a specific point
const hudButtons = CALENDARIA.api.getRegisteredWidgets('hud.buttons.right');
// Get widget replacing a specific element
const weatherWidget = CALENDARIA.api.getWidgetByReplacement('weather-indicator');
// Force all widgets to re-render
CALENDARIA.api.refreshWidgets();

Widget hooks are documented in Hooks > Widget Hooks:

  • calendaria.widgetRegistered - Fired when a widget is registered
  • calendaria.widgetsRefresh - Fired when widgets are refreshed

Every widget carries a class built from its module ID, plus a structural class for its type. Buttons get calendaria-widget, indicators get calendaria-widget-indicator and wrap any label text in a .widget-label span, and custom widgets get the calendaria-widget-custom wrapper.

Button widgets also get a base class for the application they render in: btn in the HUD, sidebar-btn in the MiniCal sidebar, and none in the BigCal action bar. sidebar-btn is not the class the built-in MiniCal sidebar buttons use, so style MiniCal sidebar widgets yourself rather than expecting them to match.

/* Style all widgets from your module */
.widget-my-module {
/* Your styles */
}
/* Style a specific widget */
.widget-my-module[data-widget-id='my-module.my-button'] {
/* Your styles */
}

The data-widget-id attribute holds the fully-qualified id (moduleId.id).

The color property is written to the element as a --widget-color custom property, which Calendaria’s own rules read for both the icon and the label. A widget that registers no color picks up whatever --widget-color a stylesheet sets, while a registered color lands inline and wins over that.


my-module.js
Hooks.once('calendaria.ready', ({ api }) => {
// Add a button to show party resources
api.registerWidget('my-module', {
id: 'party-resources',
type: 'button',
insertAt: 'hud.buttons.right',
icon: 'fas fa-coins',
tooltip: 'Party Resources',
onClick: () => openResourceTracker()
});
// Add a custom widget
api.registerWidget('my-module', {
id: 'party-gold',
type: 'custom',
insertAt: 'hud.indicators',
render: (location) => {
const gold = getPartyGold();
return `
<span class="indicator-icon"><i class="fas fa-coins" style="color: gold;"></i></span>
<span class="indicator-label">${gold} gp</span>
`;
}
});
// Update the widget when gold changes
Hooks.on('updatePartyGold', () => {
api.refreshWidgets();
});
});