Repository navigation
api menubar
Audience: Developers integrating with Blacksmith and leveraging the exposed API.
The Blacksmith Menubar API allows external modules to register custom tools with the Blacksmith menubar system. This provides a unified interface for adding functionality to the global menubar that appears above the FoundryVTT interface.
// Get the Blacksmith module API
const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
// Check if API is available
if (blacksmith?.registerMenubarTool) {
// API is ready to use
} else {
// Wait for API to load
Hooks.once('ready', () => {
// API should be available now
});
}Best Practice: Always explicitly set all optional parameters for clarity and consistency. This makes your code more maintainable and follows the recommended approach.
// Register a custom tool with all parameters explicitly set
const success = blacksmith.registerMenubarTool('my-custom-tool', {
// Required parameters
icon: "fa-solid fa-dice-d20", // FontAwesome icon class
name: "my-custom-tool", // Tool name (used for data-tool attribute)
onClick: () => { // Click handler function
// Your tool logic here
console.log("My custom tool clicked!");
},
// Optional parameters - recommended to set explicitly
title: "My Custom Tool", // Optional: Tooltip text and label (can be a function for dynamic content, defaults to name if omitted)
tooltip: "My Custom Tool", // Optional: explicit tooltip (defaults to title if omitted)
zone: "left", // Optional: "left", "middle", "right" (default: "left")
group: "general", // Optional: group name (default: "general")
groupOrder: 999, // Optional: group order (default: 999, appears last)
order: 5, // Optional: order within group (default: 999)
moduleId: "my-module", // Optional: module identifier (default: "unknown")
gmOnly: false, // Optional: GM-only visibility (default: false)
leaderOnly: false, // Optional: leader-only visibility (default: false)
visible: true, // Optional: visibility (can be function) (default: true)
toggleable: false, // Optional: toggleable behavior (default: false)
active: false, // Optional: initial active state for toggleable tools (default: false)
iconColor: null, // Optional: icon color - any valid CSS color (default: null)
buttonNormalTint: null, // Optional: normal button background color - any valid CSS color (default: null)
buttonSelectedTint: null // Optional: selected button background color - any valid CSS color (default: null)
});
if (success) {
console.log("Tool registered successfully!");
} else {
console.log("Failed to register tool");
}Note: While many parameters are optional, explicitly setting them ensures your code is clear, maintainable, and follows best practices. The system will work with just the required parameters, but for production code, we recommend setting all optional parameters explicitly.
Notifications appear in a dedicated notification area within the middle zone of the menubar. They are separate from the zone-based tool system and do not require zone specification.
Visual Layout:
[LEFT ZONE TOOLS] [MIDDLE ZONE TOOLS] [NOTIFICATION AREA] [RIGHT ZONE TOOLS]
Add a notification to the menubar.
Parameters:
-
text(string): The notification text to display -
icon(string, optional): FontAwesome icon class (default: "fas fa-info-circle") -
duration(number, optional): Duration in seconds, 0 = until manually removed (default: 5) -
moduleId(string, optional): The module ID adding the notification (default: "blacksmith-core") -
options(Object, optional): Behavior options-
onClick(Function, optional): Makes the notification clickable (pointer cursor + hover affordance). Called with the click event when the user clicks the notification body; the notification is then removed.onDismissdoes not fire afteronClick. -
onDismiss(Function, optional): Called only when the notification goes away without being acted on — see the dismiss semantics table below. -
pulse(boolean, optional): Animate the notification icon with an attention pulse — for "You have 5 unread messages"-style alerts.
-
Returns: string - The notification ID for later removal
Note: Notifications do not use zones. They appear in a dedicated notification area within the middle zone of the menubar, separate from the zone-based tool system.
Note: The notification strip is right-aligned and ordered: temporary notifications (duration > 0) sit to the left of persistent ones (duration = 0), and within each group the newest appears leftmost. Changing a notification's duration via updateNotification moves it between groups.
Note: Notifications are per-client and never cross the socket, which is why onClick/onDismiss can be plain function references.
Dismiss semantics — onDismiss fires only when the notification goes away unacted-on:
| Removal path |
onDismiss fires? |
|---|---|
Auto-timeout (duration elapses) |
Yes |
| User clicks the × close button | Yes |
User clicks the body (onClick ran) |
No — the click already told you |
Consumer calls removeNotification(id)
|
No — you initiated it |
clearNotificationsByModule / clearAllNotifications
|
No — bulk teardown |
Example:
// Add a temporary notification
const notificationId = game.modules.get('coffee-pub-blacksmith').api.addNotification(
"New message received",
"fas fa-envelope",
5,
"my-module"
);
// Add a persistent notification
const persistentId = game.modules.get('coffee-pub-blacksmith').api.addNotification(
"Important system update available",
"fas fa-exclamation-triangle",
0, // 0 = until manually removed
"my-module"
);
// Actionable notification: click opens the messages window, pulses for attention
// onClick runs in Blacksmith's context — keep it self-contained (same rule as tool onClick)
const unreadId = game.modules.get('coffee-pub-blacksmith').api.addNotification(
"5 Unread Messages",
"fas fa-envelope",
30,
"my-module",
{
onClick: () => openMessagesWindow(),
onDismiss: () => console.log("Expired or closed without being read"),
pulse: true
}
);Update an existing notification.
Parameters:
-
notificationId(string): The notification ID to update -
updates(Object): Object containing fields to update-
text(string, optional): New notification text -
icon(string, optional): New FontAwesome icon class -
duration(number, optional): New duration in seconds (0 = persistent) -
onClick(Function|null, optional): New click handler; passnullto strip it (the notification becomes display-only again) -
onDismiss(Function|null, optional): New dismiss handler; passnullto strip it -
pulse(boolean, optional): Toggle the attention pulse animation
-
Returns: boolean - True if notification was updated, false if not found
Example:
// Update notification text and icon
blacksmith.updateNotification(notificationId, {
text: "Processing complete!",
icon: "fas fa-check-circle"
});
// Change notification to auto-remove after 3 seconds
blacksmith.updateNotification(notificationId, {
duration: 3
});
// Make notification persistent again
blacksmith.updateNotification(notificationId, {
duration: 0
});Remove a specific notification from the menubar.
Parameters:
-
notificationId(string): The notification ID to remove
Returns: boolean - True if notification was removed, false if not found
Example:
game.modules.get('coffee-pub-blacksmith').api.removeNotification(notificationId);Remove all notifications from a specific module.
Parameters:
-
moduleId(string): The module ID to clear notifications for
Returns: number - Number of notifications removed
Example:
const removedCount = game.modules.get('coffee-pub-blacksmith').api.clearNotificationsByModule('my-module');
console.log(`Removed ${removedCount} notifications`);Get all currently active notifications.
Returns: Array - Array of notification objects
Example:
const notifications = game.modules.get('coffee-pub-blacksmith').api.getActiveNotifications();
console.log(`Currently ${notifications.length} active notifications`);Clear all notifications from the menubar.
Returns: number - Number of notifications removed
Example:
const removedCount = game.modules.get('coffee-pub-blacksmith').api.clearAllNotifications();
console.log(`Cleared ${removedCount} notifications`);Get all notification IDs for a specific module.
Parameters:
-
moduleId(string): The module ID to get notification IDs for
Returns: Array - Array of notification IDs
Example:
const myNotificationIds = game.modules.get('coffee-pub-blacksmith').api.getNotificationIdsByModule('my-module');
console.log(`My module has ${myNotificationIds.length} active notifications`);Registers a new tool with the Blacksmith menubar system.
Parameters:
-
toolId(string): Unique identifier for the tool -
toolData(Object): Tool configuration object
Returns: boolean - Success status
Tool Data Properties:
-
icon(string, required): FontAwesome icon class (e.g., "fa-solid fa-dice-d20") -
name(string, required): Tool name (used for data-tool attribute) -
title(string|Function, optional): Tooltip text and label displayed on hover. Can be a function that returns a string for dynamic tooltips. Defaults tonameif omitted. Can be an empty string or null for icon-only buttons. -
tooltip(string|Function, optional): Alternative tooltip text. If provided, overridestitlefor tooltip display. Can be a function that returns a string for dynamic tooltips. -
onClick(Function, required): Function to execute when tool is clicked -
zone(string, optional): Zone placement - "left", "middle", "right" (default: "left") -
group(string, optional): Group name for organizing tools within a zone. Tools in the same group appear together. If not specified, defaults to "general". Groups are separated by visual dividers. -
groupOrder(number, optional): Order for the group within the zone. Lower numbers appear first. Blacksmith-defined groups take precedence over other modules. If not specified, defaults to 999 (appears last). Minimum value is 1. -
order(number, optional): Order within the group and module. Lower numbers appear first. Blacksmith tools take precedence over other modules within the same group. Default: 999. -
moduleId(string, optional): Module identifier (default: "unknown") -
gmOnly(boolean, optional): Whether tool is GM-only. Affects visibility only; tool is still added to the specified group. Default: false. -
leaderOnly(boolean, optional): Whether tool is leader-only. Affects visibility only; tool is still added to the specified group. Default: false. -
visible(boolean|Function, optional): Whether tool is visible. Can be a function that returns a boolean for dynamic visibility. Default: true. -
toggleable(boolean, optional): Whether tool can be toggled on/off (default: false) -
active(boolean, optional): Initial active state for toggleable tools (default: false) -
iconColor(string, optional): Icon color. Can be any valid CSS color (e.g.,'#ff0000','rgba(255, 0, 0, 0.8)','red'). If omitted, uses default icon color. Unliketitleandvisible,iconandiconColorare strings only -- the template draws them straight -- so a tool that changes its appearance re-registers rather than supplying a function. See Changing a registered tool below. -
buttonNormalTint(string, optional): Background color for the button in normal state. Can be any valid CSS color (e.g.,'rgba(255, 107, 53, 0.2)','#ff6b35','red'). If omitted, uses default button background color. -
buttonSelectedTint(string, optional): Background color for the button when active/selected (for toggleable tools). Can be any valid CSS color (e.g.,'rgba(255, 107, 53, 0.4)','#ff6b35','red'). If omitted, uses default active button background color. -
contextMenuItems(Array | Function, optional): Right-click context menu. If provided, right-clicking the tool shows a menu instead of the browser default. Can be an array of{ name, icon, description?, onClick, submenu? }, or a function(toolId, tool) => arrayfor dynamic items (e.g. list that depends on current state). Icon can be a Font Awesome class string (e.g.'fa-solid fa-hand') or HTML (e.g.'<i class="fa-solid fa-hand"></i>').submenuis an array of{ name, icon, description?, onClick }to render a flyout. Typical use: a tool that selects between several modes.
There is no general update-in-place. Two fields have a method of their own --
updateMenubarToolActive for active on a toggleable tool, and
updateMenubarToolIconColor for iconColor on any tool. To change anything
else -- icon, title, order, visibility -- unregister the tool and register it
again with the new values.
// A tool whose icon reports what its module is doing.
function refreshTool() {
blacksmith.unregisterMenubarTool('my-module-recorder');
blacksmith.registerMenubarTool('my-module-recorder', {
...base,
icon: recording ? 'fa-solid fa-circle-dot' : 'fa-solid fa-map',
iconColor: recording ? '#c9412d' : null
});
}Both calls re-render, and the render is debounced, so the pair costs one render
rather than two. Register only succeeds if the id is free, so the unregister is
not optional -- a re-register over a live id returns false and leaves the old
tool in place.
Whether the change actually reaches the screen depends on the menubar's
structure fingerprint (§9B.3 of architecture-blacksmith.md): a field the
fingerprint does not cover takes the lightweight refresh path, which updates
leader, movement, timer and vote nodes only, and the button keeps whatever it
was last drawn with.
Covered today: visible (with gmOnly / leaderOnly), zone, group,
order, active, title, icon, iconColor. Not covered, though the
template draws them: name, tooltip, toggleable, buttonNormalTint,
buttonSelectedTint, groupOrder. Re-registering a tool to change only one of
those will not repaint it. Change one of the covered fields alongside, or treat
them as fixed at registration.
If you add a property that the template draws, add it to the fingerprint --
this has been missed twice, once for title and once for icon, and both times
the symptom was a button that reported stale state indefinitely while the
registration itself was correct.
Updates the active state of a toggleable tool.
Parameters:
-
toolId(string): Unique identifier for the tool -
active(boolean): The active state to set
Returns: boolean - Success status
Note: Only works for tools registered with toggleable: true.
Example:
// Update a toggleable tool's active state programmatically
blacksmith.updateMenubarToolActive('my-toggle-tool', true);Recolours a registered tool's icon in place.
Parameters:
-
toolId(string): Unique identifier for the tool -
iconColor(string | null): Any valid CSS colour, ornullto clear it
Returns: boolean — Success status
For a tool that reports a state it does not own. active is the wrong instrument for
that: it is for toggleable tools, and a tool can perfectly well be a plain button that
opens a window while still needing to say whether something is switched on somewhere
else. Blacksmith's own dice tool is the case — it goes orange while Foundry's manual
rolls are enabled, and manual rolls are a core setting anyone can change from anywhere.
Unchanged values cost nothing: the call returns early rather than re-rendering, which matters because this is the sort of thing called from a settings-change hook.
// Follow a setting that anything on any client might change.
Hooks.on('updateSetting', () => {
blacksmith.updateMenubarToolIconColor(
'my-module-recorder',
isRecording() ? 'rgba(231, 91, 1, 0.9)' : null
);
});The alternative — unregister and re-register — still works and is what you need for the
icon itself, the title, or visibility. This exists because colour alone is the common
case and the round trip is easy to get wrong: register only succeeds if the id is free,
so a re-register over a live id returns false and silently leaves the old tool up.
Runs a registered tool's onClick from anywhere, without clicking its icon. Returns false if no such tool is registered. Registration already knows what a tool does; this makes that knowledge callable, so a second surface wanting the same behaviour does not reimplement it or reach into the owning module.
An intent is a capability a tool claims, declared at registration:
blacksmith.registerMenubarTool('my-module-vitals', {
icon: 'fa-solid fa-heart',
name: 'Vitals',
onClick: () => VitalsPanel.open(),
intents: ['party-health']
});Any surface can then ask for the capability rather than for the module:
if (blacksmith.hasIntentHandler('party-health')) {
blacksmith.invokeIntent('party-health', { source: 'my-bar' });
}This is how a Blacksmith surface integrates with a sibling without naming it. Blacksmith's combat bar opens a health panel when the party health bars are clicked, and it has no health panel of its own -- naming another module's tool id in the hub would be exactly the coupling the suite's module boundaries forbid. Whichever module claims the intent answers; if none does, invokeIntent returns false.
Ask hasIntentHandler before offering the interaction, not just before running it. A control that looks clickable and does nothing is the failure the readout styling rules exist to prevent, and an unclaimed intent is that failure in a different costume.
Registration order decides ties. Two modules claiming one intent is a configuration the user chose, not an error to resolve here.
Blacksmith's own Health tool claims party-health, so the combat bar's health bars are clickable without any sibling installed. A sibling claiming the same intent takes over only if it registers first.
A tool may declare the ids it replaces:
blacksmith.registerMenubarTool('my-module-notes', {
icon: 'fa-solid fa-note',
name: 'Notes',
onClick: () => NotesWindow.open(),
supersedes: ['other-module-notes']
});A listed id already registered is dropped; a listed id registering later is refused and its registerMenubarTool call returns false. Both halves exist because module load order is not something either module controls, so the outcome is the same whichever registers first.
This is for the window during which a tool has moved from one module to another and a user has updated one module but not the other -- without it they see two identical icons. It is not a priority system, and an entry should be removed once the release that dropped the old tool has shipped.
unregisterMenubarTool releases whatever claims a tool made, so removing the new owner lets the old id register again.
Removes a tool from the Blacksmith menubar system.
Parameters:
-
toolId(string): Unique identifier for the tool
Returns: boolean - Success status
Gets all registered tools.
Returns: Map - Map of all registered tools (toolId -> toolData)
Gets all tools registered by a specific module.
Parameters:
-
moduleId(string): Module identifier
Returns: Array - Array of tools registered by the module
Checks if a tool is registered.
Parameters:
-
toolId(string): Unique identifier for the tool
Returns: boolean - Whether the tool is registered
Gets all tools organized by their zones, groups, and modules.
Returns: Object - Object with zones containing groups, which contain module arrays with tools
{
left: {
"combat": [
{ moduleId: "blacksmith-core", tools: [/* tool objects */], isBlacksmith: true },
{ moduleId: "other-module", tools: [/* tool objects */], isBlacksmith: false }
],
"general": [
{ moduleId: "blacksmith-core", tools: [/* tool objects */], isBlacksmith: true }
]
},
middle: {
"utility": [
{ moduleId: "blacksmith-core", tools: [/* tool objects */], isBlacksmith: true }
],
"general": [...]
},
right: {
"general": [...]
}
}Structure:
-
Zone (
left,middle,right): Top-level organization -
Group (e.g.,
"combat","utility","general"): Groups within each zone, sorted bygroupOrder -
Module Array: Array of objects, each containing:
-
moduleId(string): The module identifier -
tools(Array): Array of tool objects for this module -
isBlacksmith(boolean): Whether this module is Blacksmith
-
- Tools within each module are sorted by their
ordervalue - Modules within a group are sorted with Blacksmith first, then by minimum tool
ordervalue
Note: Only visible tools are included in the returned structure. Tools filtered by gmOnly, leaderOnly, or visible are excluded.
Request a re-render of the menubar. Use this when your module's settings or state change and the menubar should reflect those changes (e.g. tool visibility, secondary bar state).
Parameters:
-
immediate(boolean, optional): Iftrue, re-render immediately. Iffalse(default), debounces the render by ~50ms to batch rapid changes.
Returns: Promise<void>
Example:
const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
if (blacksmith?.renderMenubar) {
blacksmith.renderMenubar(); // Debounced
// or
blacksmith.renderMenubar(true); // Immediate
}Register a callback that can hide the menubar for specific users — for example, when a module wants a designated user to see a clean, UI-free view.
Parameters:
-
moduleId(string): Your module identifier (e.g.'coffee-pub-herald') -
callback(function): Called with(user)and returns{ hide: true }to hide the menubar for that user, or{ hide: false }(or omithide) to allow the menubar
Example:
const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
blacksmith?.registerMenubarVisibilityOverride('my-module', (user) => {
const shouldHide = /* your logic */;
return { hide: shouldHide };
});Unregister a visibility override (e.g. on module unload).
Parameters:
-
moduleId(string): The same module ID used when registering
Example:
blacksmith?.unregisterMenubarVisibilityOverride('my-module');The menubar system organizes tools into three predefined zones:
-
left- Action tools (movement, interface, voting, skill checks) -
middle- General tools and utilities (supports grouping with visual dividers) -
right- Informational tools (leader display, timer)
-
left: Primary action tools that users interact with frequently -
middle: Secondary tools and utilities (supports grouping system) -
right: Read-only information displays and status indicators
- Tools within each zone can be organized into groups using the
groupparameter - Groups are separated by visual dividers in the menubar
- The grouping system is most commonly used in the
middlezone, but works in all zones - Groups are sorted by
groupOrder, with lower numbers appearing first
The menubar uses a three-tier visibility system:
- GM: Sees all tools (including GM tools and leader tools)
- LEADER: Sees all tools except GM tools, plus leader tools
- PLAYER: Sees all tools except GM tools and leader tools
-
gmOnly: true: Only visible to Game Masters -
leaderOnly: true: Visible to party leaders and GMs - Default: Visible to all users
// Tool with dynamic visibility
blacksmith.registerMenubarTool('my-conditional-tool', {
icon: "fa-solid fa-eye",
name: "my-conditional-tool",
title: "Conditional Tool",
tooltip: "Conditional Tool",
zone: "left",
group: "general",
groupOrder: 999,
order: 20,
moduleId: "my-module",
gmOnly: false,
leaderOnly: false,
visible: () => {
// Only show if certain conditions are met
return game.user.isGM || game.settings.get('my-module', 'enableFeature');
},
toggleable: false,
active: false,
iconColor: null,
buttonNormalTint: null,
buttonSelectedTint: null,
onClick: () => {
// Tool logic
}
});The menubar uses a tiered grouping system to organize tools logically:
Tools are organized in the following hierarchy:
-
Zone (
left,middle,right) -
Group (e.g.,
"combat","utility","party","general") - Module (tools from the same module are clustered together)
-
Order (within the module, by
ordervalue)
- Groups are separated by visual dividers in the menubar
-
Groups are sorted by
groupOrder(lower numbers appear first) -
Within a group, tools are organized by module:
- Blacksmith tools appear first (if any)
- Other modules follow, sorted by the minimum
ordervalue of their tools
-
Within a module, tools are sorted by
ordervalue
Blacksmith defines the following groups with their order priorities:
-
"combat"(order: 1) - Combat-related tools -
"utility"(order: 2) - Utility tools -
"party"(order: 3) - Party management tools -
"general"(order: 999) - Default group, always appears last
- Blacksmith groups take precedence: If Blacksmith defines a group order, it overrides any other module's definition
- "general" group is always last: The default group always has order 999 and appears after all other groups
-
Dynamic groups: Modules can create new groups by specifying a
groupname. If the group doesn't exist, it's created automatically -
Auto-assignment: If a module specifies a
groupOrder>= 999, it's automatically assigned to the first available slot below 999
-
gmOnlyandleaderOnlyaffect visibility only - tools are still added to their specified group - Hidden tools don't affect group ordering or module clustering
-
Lower
groupOrdervalues appear first within each zone -
Recommended
groupOrderranges:-
1-10: Core/primary groups (combat, utility, party) -
11-50: Secondary groups -
51-100: Utility groups -
101-998: Custom groups -
999: Default "general" group (always last)
-
-
Lower
ordervalues appear first within each module - Blacksmith tools take precedence over other modules within the same group
-
Recommended
orderranges:-
1-10: Core/primary tools -
11-50: Secondary tools -
51-100: Utility tools -
101+: Optional/advanced tools
-
class MyModule {
constructor() {
this.notificationIds = new Set(); // Track our notification IDs
this.moduleId = 'my-module';
this.blacksmith = null;
}
async initialize() {
// Wait for Blacksmith API to be ready
this.blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
if (!this.blacksmith) {
console.error('Blacksmith API not available');
return;
}
}
// Add a notification and track the ID
addNotification(text, icon = "fas fa-info", duration = 5) {
if (!this.blacksmith) return null;
const notificationId = this.blacksmith.addNotification(text, icon, duration, this.moduleId);
if (notificationId) {
this.notificationIds.add(notificationId);
}
return notificationId;
}
// Update a specific notification
updateNotification(notificationId, newText, newIcon) {
if (!this.blacksmith) return false;
return this.blacksmith.updateNotification(notificationId, {
text: newText,
icon: newIcon
});
}
// Remove a specific notification
removeNotification(notificationId) {
if (!this.blacksmith) return false;
const success = this.blacksmith.removeNotification(notificationId);
if (success) {
this.notificationIds.delete(notificationId);
}
return success;
}
// Clean up all our notifications when module is disabled
cleanup() {
if (this.blacksmith) {
this.blacksmith.clearNotificationsByModule(this.moduleId);
this.notificationIds.clear();
}
}
// Get all our current notification IDs
getMyNotificationIds() {
if (!this.blacksmith) return [];
return this.blacksmith.getNotificationIdsByModule(this.moduleId);
}
// Example: Show a progress notification that updates
async showProgressNotification() {
const notificationId = this.addNotification(
"Starting process...",
"fas fa-spinner fa-spin",
0 // Persistent
);
// Simulate progress updates
setTimeout(() => {
this.updateNotification(notificationId, {
text: "Processing... 50%",
icon: "fas fa-spinner fa-spin"
});
}, 2000);
setTimeout(() => {
this.updateNotification(notificationId, {
text: "Processing complete!",
icon: "fas fa-check-circle",
duration: 3 // Auto-remove after 3 seconds
});
}, 4000);
}
}
// Foundry has no module-unload event, so there is no teardown hook to register.
// Call cleanup from your own lifecycle when you turn the feature off:
// myModuleInstance.cleanup();const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
// Add a temporary notification
const tempId = blacksmith.addNotification(
"Task completed!",
"fas fa-check",
5,
"my-module"
);
// Add a persistent notification
const persistentId = blacksmith.addNotification(
"System is processing...",
"fas fa-spinner fa-spin",
0, // 0 = persistent
"my-module"
);
// Update the persistent notification when done
blacksmith.updateNotification(persistentId, {
text: "Processing complete!",
icon: "fas fa-check-circle",
duration: 3 // Now auto-remove after 3 seconds
});
// Or remove it manually
blacksmith.removeNotification(persistentId);Best Practice: Always explicitly set all optional parameters for clarity and consistency.
// Register a simple utility tool (left zone)
blacksmith.registerMenubarTool('my-utility', {
icon: "fa-solid fa-calculator",
name: "my-utility",
title: "My Utility Tool",
tooltip: "My Utility Tool", // Optional: explicit tooltip (defaults to title)
zone: "left",
group: "general", // Optional: group name (default: "general")
groupOrder: 999, // Optional: group order (default: 999)
order: 10, // Optional: order within group (default: 999)
moduleId: "my-module", // Optional: module identifier (default: "unknown")
gmOnly: false, // Optional: GM-only visibility (default: false)
leaderOnly: false, // Optional: leader-only visibility (default: false)
visible: true, // Optional: visibility (default: true)
toggleable: false, // Optional: toggleable behavior (default: false)
active: false, // Optional: initial active state (default: false)
iconColor: null, // Optional: icon color (default: null)
buttonNormalTint: null, // Optional: normal button background color (default: null)
buttonSelectedTint: null, // Optional: selected button background color (default: null)
onClick: () => {
// Your utility logic
ui.notifications.info("Utility tool activated!");
}
});// Register a GM-only admin tool
blacksmith.registerMenubarTool('my-admin-tool', {
icon: "fa-solid fa-cog",
name: "my-admin-tool",
title: "Admin Tool",
tooltip: "Admin Tool",
zone: "middle",
group: "general",
groupOrder: 999,
order: 5,
moduleId: "my-module",
gmOnly: true, // Only visible to GMs
leaderOnly: false,
visible: true,
toggleable: false,
active: false,
iconColor: null,
buttonNormalTint: null,
buttonSelectedTint: null,
onClick: () => {
// Admin functionality
console.log("Admin tool used by GM");
}
});// Register a leader-only tool
blacksmith.registerMenubarTool('my-leader-tool', {
icon: "fa-solid fa-crown",
name: "my-leader-tool",
title: "Leader Tool",
tooltip: "Leader Tool",
zone: "left",
group: "general",
groupOrder: 999,
order: 1,
moduleId: "my-module",
gmOnly: false,
leaderOnly: true, // Visible to leaders and GMs
visible: true,
toggleable: false,
active: false,
iconColor: null,
buttonNormalTint: null,
buttonSelectedTint: null,
onClick: () => {
// Leader functionality
ui.notifications.info("Leader tool activated!");
}
});// Register an informational tool for the right zone
blacksmith.registerMenubarTool('my-status-tool', {
icon: "fa-solid fa-info-circle",
name: "my-status-tool",
title: "Status Information",
tooltip: "Status Information",
zone: "right",
group: "general",
groupOrder: 999,
order: 10,
moduleId: "my-module",
gmOnly: false,
leaderOnly: false,
visible: true,
toggleable: false,
active: false,
iconColor: null,
buttonNormalTint: null,
buttonSelectedTint: null,
onClick: () => {
// Display status information
ui.notifications.info("Status: All systems operational");
}
});// Register a toggleable tool that shows active/inactive state
blacksmith.registerMenubarTool('my-toggle-tool', {
icon: "fa-solid fa-toggle-on",
name: "my-toggle-tool",
title: "Toggle Feature",
tooltip: "Toggle Feature",
zone: "left",
group: "general",
groupOrder: 999,
order: 15,
moduleId: "my-module",
gmOnly: false,
leaderOnly: false,
visible: true,
toggleable: true, // Enable toggleable behavior
active: false, // Initial state
iconColor: null,
buttonNormalTint: null,
buttonSelectedTint: null,
onClick: () => {
// The active state is automatically toggled by the system
// Check the current state if needed
const tool = blacksmith.getRegisteredMenubarTools().get('my-toggle-tool');
if (tool.active) {
console.log("Feature is now active");
} else {
console.log("Feature is now inactive");
}
}
});
// Update active state programmatically
blacksmith.updateMenubarToolActive('my-toggle-tool', true);// Register a tool with custom icon and button colors
blacksmith.registerMenubarTool('my-colored-tool', {
icon: "fa-solid fa-palette",
name: "my-colored-tool",
title: "Colored Tool",
tooltip: "Colored Tool",
zone: "middle",
group: "utility",
groupOrder: 2,
order: 10,
moduleId: "my-module",
gmOnly: false,
leaderOnly: false,
visible: true,
toggleable: false,
active: false,
iconColor: "#ff6b35", // Orange icon
buttonNormalTint: "rgba(255, 107, 53, 0.2)", // Light orange background
buttonSelectedTint: "rgba(255, 107, 53, 0.4)", // Darker orange when active
onClick: () => {
ui.notifications.info("Colored tool activated!");
}
});// Register a tool in a custom group (creates the group automatically)
blacksmith.registerMenubarTool('my-custom-tool', {
icon: "fa-solid fa-star",
name: "my-custom-tool",
title: "Custom Group Tool",
tooltip: "Custom Group Tool",
zone: "middle",
group: "my-custom-group", // Creates new group automatically
groupOrder: 50, // Appears after utility (2) but before general (999)
order: 1,
moduleId: "my-module",
gmOnly: false,
leaderOnly: false,
visible: true,
toggleable: false,
active: false,
iconColor: null,
buttonNormalTint: null,
buttonSelectedTint: null,
onClick: () => {
ui.notifications.info("Custom group tool activated!");
}
});// Foundry has no module-unload event, and disabling a module reloads the world, which clears
// registered tools anyway. Unregister only if you turn a feature off at runtime:
const tools = blacksmith.getMenubarToolsByModule('my-module');
tools.forEach(tool => blacksmith.unregisterMenubarTool(tool.toolId));The API includes robust error handling:
-
Invalid tool data: Returns
falseand logs error -
Duplicate tool IDs: Returns
false(tools must be unique) -
Missing required properties: Returns
falseand logs error - API not available: Check for API availability before use
Your onClick is stored and invoked as a plain function call (tool.onClick(event)). Two consequences:
-
Your closures work normally. The function keeps its lexical scope, so module-level imports and
variables captured at definition time are available when it runs. Blacksmith's own menubar tools rely
on this — e.g.
onClick: () => new MovementConfig().render(true), closing over an import. -
thisis not bound. Because the call is unbound,thisinside a non-arrowonClickisundefined. If you need instance context, use an arrow function or bind it yourself:
import { MyManager } from './manager-my.js';
blacksmith.registerMenubarTool('my-module-tool', {
icon: 'fa-solid fa-star',
name: 'my-module-tool',
moduleId: 'my-module-id',
onClick: () => MyManager.doSomething() // arrow: closure intact, no `this` needed
});
// If you must use a method as the handler, bind it:
blacksmith.registerMenubarTool('my-module-other', {
/* ... */
onClick: this.handleClick.bind(this)
});Errors thrown inside onClick are caught and logged by Blacksmith so one bad tool cannot break the
menubar — which also means a thrown error will not surface as an uncaught exception. Handle your own
failures if you need the user to see them.
-
Explicit Parameter Setting: Always explicitly set all optional parameters when registering tools. This ensures clarity, consistency, and makes your code more maintainable:
// Good: Explicit parameters blacksmith.registerMenubarTool('my-tool', { icon: "fa-solid fa-icon", name: "my-tool", title: "My Tool", tooltip: "My Tool", zone: "left", group: "general", groupOrder: 999, order: 10, moduleId: "my-module", gmOnly: false, leaderOnly: false, visible: true, toggleable: false, active: false, iconColor: null, buttonNormalTint: null, buttonSelectedTint: null, onClick: () => {} }); // Avoid: Relying on defaults (less clear) blacksmith.registerMenubarTool('my-tool', { icon: "fa-solid fa-icon", name: "my-tool", title: "My Tool", onClick: () => {} });
-
Self-Contained Functions: Make onClick functions completely self-contained with all dependencies imported
-
Unique Tool IDs: Use descriptive, unique tool identifiers
-
Proper Zone Selection: Choose the most appropriate zone for your tool:
-
left: Primary action tools -
middle: General tools and utilities -
right: Informational/read-only tools
-
-
Group Organization: Use appropriate groups to organize related tools:
- Use existing Blacksmith groups (
"combat","utility","party") when appropriate - Create custom groups for module-specific tool collections
- Use
groupOrderto control group positioning
- Use existing Blacksmith groups (
-
Consistent Ordering: Use consistent
ordervalues within your module:- Lower numbers appear first
- Reserve ranges for different tool types
-
Color Format Flexibility: When using
iconColor,buttonNormalTint, orbuttonSelectedTint, you can use any valid CSS color format:- Hex:
'#ff6b35' - RGB/RGBA:
'rgba(255, 107, 53, 0.2)' - Named colors:
'red' - HSL:
'hsl(15, 100%, 60%)'
- Hex:
-
Module Cleanup: Unregister tools when your module is disabled
-
Error Handling: Always check return values and handle errors gracefully
-
API Availability: Check if the API is available before using it
-
Scope Awareness: Understand that onClick functions execute in Blacksmith's context, not your module's context
- Check if tool is registered:
blacksmith.isMenubarToolRegistered('tool-id') - Verify visibility settings (gmOnly, leaderOnly, visible function)
- Check console for error messages
- Ensure API is loaded:
blacksmith?.registerMenubarTool
- Use correct API path:
blacksmith.registerMenubarTool()notblacksmith.api.registerMenubarTool() - Wait for
readyhook, notblacksmithUpdatedhook - Check if module is active:
game.modules.get('coffee-pub-blacksmith')?.active
- Verify
zoneproperty is set correctly - Check zone spelling (must match "left", "middle", or "right" exactly)
- Default zone is "left" if not specified
-
TypeError: Cannot read properties of undefinedonthis— youronClickis invoked unbound, sothisisundefinedin a non-arrow function. Use an arrow function or.bind(this). SeeonClickandthis. -
Nothing happens on click — errors thrown inside
onClickare caught and logged by Blacksmith rather than surfacing as uncaught exceptions. Check the console for the logged error. -
A different module's tool fires — two tools registered with the same
name. Dispatch keys ontoolId; make sure yours is unique and prefixed with your module id.
- Verify
onClickfunction is provided and valid - Check for JavaScript errors in onClick function
- Ensure tool is not disabled by visibility logic
The menubar supports secondary bars - additional toolbars that appear below the main menubar, similar to tabs. Only one secondary bar can be open at a time. When you open a new secondary bar, the existing one automatically closes.
Secondary bar methods at a glance:
| Method | Purpose |
|---|---|
registerSecondaryBarType(typeId, config) |
Define a bar type (size, persistence, groups). Call this first. |
getSecondaryBarHeight(typeId) |
The height in pixels a bar type renders at, including the house default. |
registerSecondaryBarItem(barTypeId, itemId, itemData) |
Add a button or info item to a bar (zone, group, icon/onClick or label/value). |
registerSecondaryBarTool(barTypeId, toolId) |
Optional. Link a menubar tool to this bar so the menubar syncs the tool's active state when the bar opens/closes. |
unregisterSecondaryBarItem(barTypeId, itemId) |
Remove an item from a bar. |
openSecondaryBar(typeId, options) / closeSecondaryBar() / toggleSecondaryBar(typeId, options)
|
Show, hide, or toggle a secondary bar (e.g. from a menubar tool's onClick). |
updateSecondaryBarItemActive(barTypeId, itemId, active) |
Set which button is active on the bar (e.g. radio-style mode buttons). |
updateSecondaryBarItemInfo(barTypeId, itemId, updates) |
Update the value, label, or tooltip of an info item (for dynamic display without re-registering). |
getSecondaryBarItems(barTypeId) |
Get the list of items for a bar. |
updateSecondaryBar(data) |
Update data for an already-open bar (e.g. custom template content). |
Important: Always use the default tool system unless your use case absolutely requires a custom template. The default tool system is simpler, faster to implement, timing-safe, and sufficient for most toolbars. Only use custom templates for complex UIs that cannot be achieved with simple button-based tools.
Common Use Cases:
- Combat tracker (built-in, uses custom template for complex portraits/health rings)
- Drawing tools (Cartographer, uses default tool system)
- Specialized toolbars for specific activities
- Tab-like behavior: Only one secondary bar can be open at a time
- Automatic switching: Opening a new secondary bar closes the currently open one
-
Persistence modes:
'manual'(user closes) or'auto'(auto-closes after delay) - Sizing: a bar type asks for a size preset; a bar that asks for nothing gets the house default, which is the same height as the primary menubar
A bar's height is a master scale factor, not just a dimension. Every font size, icon size, gap, and
padding inside the bar is derived from it, so raising the height to fit content also enlarges the type. Bars
that each picked their own number therefore do not read as one system, which is why sizing is expressed as a
preset rather than a pixel value. The mechanism is described in
../architecture/architecture-menubar.md.
config.size accepts:
| Preset | Height | Use |
|---|---|---|
'default' |
30px, matching the primary menubar | Everything, unless there is a reason not to |
'large' |
45px | Bars whose content genuinely needs the room |
'xlarge' |
60px | Bars meant to be read at a distance, or holding portraits |
Omitting size gives 'default'. An unrecognised preset name logs a warning and falls back to the default.
There is no pixel option. config.height is ignored and logs a warning saying so. A bespoke number sets
the bar's typography as well as its size and stops tracking the house default, and every module in the suite
took that option when it existed. If none of the presets fit, the answer is a new preset in Blacksmith --
a suite-wide decision -- not a local number.
A custom template (templatePath) does not change this. It controls the bar's markup; the bar still renders
inside the same element, still takes its height from the same variable, and still scales its type from it.
openSecondaryBar(typeId, { height }) is a different thing and is not a way to pick a size: it re-opens a bar
at a height that bar recomputed for itself. The encounter bar uses it because it is two rows whose combined
height changes with combat state. A bar with one fixed appearance has no use for it.
Group banners do not come out of the height. A bar with groupBannerEnabled: true gets its banner space
added on top, so its buttons are the same size as an identically-sized bar without banners. Do not size a bar
up to make room for banners.
getSecondaryBarHeight(typeId) returns the height in pixels a given type renders at, resolving the type's
own CSS variable if it declares one and the house default otherwise.
The default tool system (no custom template) supports:
-
Zones: Each item can specify a zone:
'left','middle', or'right'. Items without a zone default to'middle'. The bar renders three regions: left-aligned, center, and right-aligned. This matches the main menubar's left/middle/right layout so you can build encounter-style bars (info on the sides, actions in the center) without a custom template. -
Item kinds:
-
Button (default): Clickable item with
iconorimageandonClick. Same as before. -
Info: Display-only item with
labeland/orvalue. NoonClick. UseupdateSecondaryBarItemInfo(barTypeId, itemId, { value, label })to update the displayed text so the bar can show dynamic content (e.g. Party CR, Difficulty) without re-registering.
-
Button (default): Clickable item with
Before you can open a secondary bar, you must register its type. The system supports three approaches:
- Default Tool System (recommended - use this unless custom template is absolutely necessary) - Register individual tools/items
- Custom Template (only when default system cannot meet your needs) - Provide a custom Handlebars template
-
Hybrid (
templatePathplushybridItems: true) - Your template draws the bar and also renders registered items
Default and custom are mutually exclusive; hybrid combines them. A bar with only templatePath replaces the entire bar and rejects registerSecondaryBarItem calls. Adding hybridItems: true makes the bar accept items, prepares the zones alongside the custom payload, and puts zones, groupBannerEnabled, and groupBannerColor on the object your template receives - so the template can render items by passing its own context to the shared partial:
{{> "menubar-secondary-default" this}}Reach for hybrid when part of the bar cannot be expressed as items and the rest is better off as items - the combat bar's portrait strip needs custom markup, while its readouts are info, progressbar, and balancebar items.
Choose the default tool system unless you need:
- Complex nested HTML structures
- Custom graphics/SVG elements (like health rings)
- Dynamic data-driven layouts that buttons can't handle
- Highly specialized UI components
Use default tool system for:
- Simple button-based toolbars
- Drawing tools, filters, toggles
- Any toolbar that can be represented as a row of buttons/controls
const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
// Option 1: Register a bar type using the default tool system (no template needed)
const success = await blacksmith.registerSecondaryBarType('cartographer', {
size: 'default', // 'default', 'large', or 'xlarge'
persistence: 'manual', // 'manual' or 'auto'
autoCloseDelay: 10000, // Auto-close delay in ms (if persistence is 'auto')
groups: { // Optional: Group configuration
'line-size': {
mode: 'switch', // 'switch' or 'default'
order: 10
},
'colors': {
mode: 'switch',
order: 20
},
'tools': {
mode: 'default', // Default mode allows mixed behaviors
order: 0
}
}
// No templatePath = uses default tool system
});
// Option 2: Register a bar type with a custom template (for complex UIs)
const success = blacksmith.registerSecondaryBarType('combat', {
size: 'xlarge',
persistence: 'manual',
templatePath: 'modules/my-module/templates/menubar-combat.hbs' // Custom template path
});
if (success) {
console.log("Secondary bar type registered!");
}Parameters:
-
typeId(string, required): Unique identifier for the bar type (e.g., 'cartographer', 'combat') -
config(Object, required): Configuration object-
size(string, optional): Size preset -'default','large', or'xlarge'(default:'default'). See Secondary Bar Sizing above. There is no pixel equivalent;heightis ignored and warns. -
persistence(string, optional):'manual'or'auto'(default:'manual') -
autoCloseDelay(number, optional): Auto-close delay in milliseconds (default: 10000) -
templatePath(string, optional): Path to custom Handlebars template partial. If not provided, uses the default tool system. -
hybridItems(boolean, optional): WithtemplatePath, also accept registered items and prepare their zones (default:false). The template receiveszones,groupBannerEnabled, andgroupBannerColoron its context and renders them by passing that context to themenubar-secondary-defaultpartial. Without this flag a custom-template bar rejectsregisterSecondaryBarItem. Ignored when there is notemplatePath. -
groupBannerEnabled(boolean, optional): Enable group banners above each button group (default:false). A banner captions a cluster of otherwise unlabelled buttons; leave it off for groups of readouts, which carry their own labels. -
groupBannerColor(string, optional): Background color for group banners - any valid CSS color (default:'rgba(62, 62, 163, 0.9)') -
groups(Object, optional): Group configuration object. Maps group IDs to group configs:-
groupId(string): Unique identifier for the group (e.g., 'line-size', 'colors') -
mode(string, optional): Group behavior mode -'default'or'switch'(default:'default')-
'default': Independent buttons (can have toggleable buttons) -
'switch': Only one button active at a time, one always active
-
-
order(number, optional): Display order for the group (lower numbers appear first, default: 999)
-
-
Returns: Promise<boolean> - Success status (async method)
Note: This method is async because it loads custom templates. Use await or .then() when using custom templates.
Group Configuration:
- Groups allow you to organize buttons into logical sections
- Multiple modules can contribute groups to the same bar type (groups are merged)
- Groups with different IDs will have dividers between them
- The
'default'group always exists if no group is specified for an item - Group modes determine how buttons within that group behave (see Secondary Bar Groups section below)
Group Banners:
- When
groupBannerEnabledistrue, a banner appears above each button group - The banner displays the group ID as text (e.g., "modes", "tools", "colors")
- Banner height scales proportionally with the secondary bar height (20% of bar height, clamped 10-20px)
- Banner text is automatically scaled to match (10% of bar height, clamped 8-14px)
- Banner text color is always
rgba(255, 255, 255, 0.9)for consistency - Banners are additive: the banner and its gap are added to the bar's height rather than taken out of it, so buttons on a bannered bar are the same size as buttons on an identically-sized bar without banners. The extra space is included in
--blacksmith-menubar-total-height, so the interface below the menubar moves down to match - Use group banners to provide visual organization and labeling for button groups
// Open your secondary bar
const success = blacksmith.openSecondaryBar('cartographer', {
data: {
// Your custom data to pass to the template
tools: ['pencil', 'eraser', 'line'],
activeTool: 'pencil'
},
persistence: 'manual' // Optional: override registered persistence
});
if (success) {
console.log("Cartographer bar opened!");
}Parameters:
-
typeId(string, required): The registered bar type ID -
options(Object, optional): Options for the bar-
data(Object, optional): Data to pass to the bar template -
height(number, optional): Re-open at a height the bar recomputed for itself, for a bar whose height changes with its own state. Not a way to choose a size - see Secondary Bar Sizing. -
persistence(string, optional): Override the registered persistence mode
-
Returns: boolean - Success status
Note: Opening a secondary bar automatically closes any currently open secondary bar.
// Close the currently open secondary bar
const success = blacksmith.closeSecondaryBar();
if (success) {
console.log("Secondary bar closed!");
}Returns: boolean - Success status
// Toggle the cartographer bar (opens if closed, closes if open)
const success = blacksmith.toggleSecondaryBar('cartographer', {
data: {
tools: ['pencil', 'eraser'],
activeTool: 'pencil'
}
});Parameters:
-
typeId(string, required): The bar type to toggle -
options(Object, optional): Options for the bar (same asopenSecondaryBar)
Returns: boolean - Success status
Behavior:
- If the bar is closed, it opens
- If the bar is open and matches the type, it closes
- If a different bar is open, it closes that bar and opens the requested one
Update the data of an already-open secondary bar without closing/reopening:
// Update the cartographer bar data
const success = blacksmith.updateSecondaryBar({
activeTool: 'eraser',
color: '#ff0000'
});Parameters:
-
data(Object, required): New data to merge with existing data
Returns: boolean - Success status
Note: This only works if a secondary bar is currently open.
Secondary bars support groups - logical collections of buttons that are visually separated by dividers. Groups support two behavior modes:
'default' mode (default):
- Buttons are independent
- Each button can be toggleable (use
toggleable: truein item registration) - Buttons can perform actions independently
- Use case: Tool buttons, action buttons, filters
'switch' mode:
- Radio-button behavior: only one button active at a time
- One button must always be active (first item becomes active by default)
- Clicking a button automatically deactivates others in the group
- Use case: Line size selection, color pickers, mode selection
Groups are configured when registering the bar type:
await blacksmith.registerSecondaryBarType('cartographer', {
groups: {
'tools': {
mode: 'default', // Independent buttons
order: 0 // Display order (lower = first)
},
'line-size': {
mode: 'switch', // Radio-button behavior
order: 10
}
},
groupBannerEnabled: true, // Enable group banners (optional)
groupBannerColor: 'rgba(62, 62, 163, 0.9)' // Banner background color (optional)
});When registering items, specify the group parameter:
// Item in 'tools' group (default mode)
blacksmith.registerSecondaryBarItem('cartographer', 'pencil', {
group: 'tools',
toggleable: true, // Can toggle on/off
// ...
});
// Item in 'line-size' group (switch mode)
blacksmith.registerSecondaryBarItem('cartographer', 'small', {
group: 'line-size',
// toggleable not needed - switch mode handles it automatically
// ...
});Groups are rendered with dividers between them:
[Group 1 Items] | [Group 2 Items] | [Group 3 Items]
Items within the same group appear together without dividers.
Multiple modules can contribute groups to the same bar type. Groups are merged when the bar type is registered:
// Module A registers the bar type with groups
await blacksmith.registerSecondaryBarType('cartographer', {
groups: {
'tools': { mode: 'default', order: 0 }
}
});
// Module B can add more groups (merged, not replaced)
await blacksmith.registerSecondaryBarType('cartographer', {
groups: {
'colors': { mode: 'switch', order: 20 } // Added to existing groups
}
});For simple toolbars, use the default tool registration system. This avoids needing to create custom templates. Items are also accepted by a custom-template bar registered with hybridItems: true; a custom-template bar without that flag rejects them.
// Register items for a secondary bar (default tool system)
blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
icon: 'fa-solid fa-pencil',
label: 'Pencil',
tooltip: 'Draw with pencil tool',
group: 'tools', // Optional: Group ID (default: 'default')
toggleable: false, // Optional: Whether item can be toggled (default: false)
active: false, // Optional: whether item is active/selected
order: 10, // Optional: sort order within group
onClick: (event) => {
// Handle click
console.log('Pencil tool clicked');
}
});
blacksmith.registerSecondaryBarItem('cartographer', 'small-line', {
icon: 'fa-solid fa-minus',
label: 'Small',
group: 'line-size', // Part of the 'line-size' switch group
order: 10,
onClick: (event) => {
// In switch groups, clicking automatically activates this item and deactivates others
console.log('Small line size selected');
}
});
blacksmith.registerSecondaryBarItem('cartographer', 'medium-line', {
icon: 'fa-solid fa-equals',
label: 'Medium',
group: 'line-size',
order: 20,
onClick: (event) => {
console.log('Medium line size selected');
}
});Parameters:
-
barTypeId(string, required): The bar type ID to register the item to -
itemId(string, required): Unique identifier for the item -
itemData(Object, required): Item configuration-
kind(string, optional):'button'(default),'info','statchip','portraitstat','gaugechip','nameplate','progressbar', or'balancebar'. Buttons are clickable; every other kind is display-only and is updated withupdateSecondaryBarItemInfo. A display-only item must carry at least one oflabel,value,valueParts, orimage, or the registration is refused. -
zone(string, optional):'left','middle', or'right'. Default:'middle'. Only applies to the default tool system. -
icon(string, required for buttons, optional for info): FontAwesome icon class (e.g.,'fa-solid fa-pencil','fas fa-eraser'). Info items can use icon for consistent styling with buttons. -
label(string, optional): Text label. For buttons, shown next to the icon. For info items, use with or withoutvalue. -
value(string, optional): For info items only. Display value (e.g. "2", "Medium"). Can be updated later withupdateSecondaryBarItemInfo. -
tooltip(string, optional): Tooltip text on hover. If omitted, useslabelas tooltip. -
group(string, optional): Group ID to place the item in. If not specified, uses'default'group. Groups with different IDs will have dividers between them. -
toggleable(boolean, optional): Whether item can be toggled on/off (buttons only; only applies to'default'mode groups). In'switch'mode groups, items are automatically managed. -
active(boolean, optional): Whether button is active/selected. AddsactiveCSS class whentrue. For'switch'mode groups, the first button is automatically made active if none is active. -
order(number, optional): Sort order for displaying items within the group (lower numbers appear first). Items withoutorderappear after items withorder, sorted alphabetically byitemId. -
iconColor(string, optional): Icon color. Can be any valid CSS color (e.g.,'#ff0000','rgba(255, 0, 0, 0.8)','red'). If omitted, uses default icon color. Applies to both buttons and info items. -
buttonColor(string, optional): Background color for the item. Can be any valid CSS color. If omitted, uses the default from--blacksmith-menubar-secondary-buttoncolor. Applies to both buttons and info items. -
borderColor(string, optional): Border color. Applies to both buttons and info items. -
onClick(Function, required for buttons): Click handler function(event) => {}. Receives the click event as parameter. Omit for info items. - Additional properties: Any other properties are preserved and passed through, but not used by the default template.
-
Info item example (display-only, e.g. encounter-style CR/difficulty):
blacksmith.registerSecondaryBarItem('my-encounter', 'party-cr', {
kind: 'info',
zone: 'left',
icon: 'fas fa-helmet-battle',
label: 'Party CR',
value: '2',
group: 'cr',
order: 0
});
blacksmith.updateSecondaryBarItemInfo('my-encounter', 'party-cr', { value: '3' }); // update when assessment changesAn info item accepts value, label, tooltip, borderColor, buttonColor, and iconColor through
updateSecondaryBarItemInfo. tooltip is worth knowing about: a chip that shows a short figure usually
wants the explanation behind it to change with the figure, so the tooltip can carry the detail the chip has
no room for.
blacksmith.updateSecondaryBarItemInfo('my-bar', 'biggest-hit', {
label: 'Kar-ahn',
value: '26',
tooltip: 'Biggest hit this combat: Kar-ahn hit the Ogre for 26'
});Statchip item (kind: 'statchip'): A value that carries a tone. Same content as info - icon, label, value - plus tone: 'neutral' (default), 'good', 'bad', or 'record'. The tone colours the value and its icon, and record adds a hairline under the value. It never applies a fill or a border box, because a readout that looks like a button offers an affordance it does not have. Update with updateSecondaryBarItemInfo(barTypeId, itemId, { value, label, tone, tooltip }).
Tone describes what a rise in the number means for the reader, not whether the number is large: damage dealt is good and damage taken is bad even though both go up as a fight goes on.
blacksmith.registerSecondaryBarItem('my-bar', 'damage-taken', {
kind: 'statchip',
zone: 'middle',
icon: 'fa-solid fa-shield-halved',
value: '0',
tone: 'bad',
tooltip: 'Party damage taken this combat',
group: 'stats',
order: 1
});
blacksmith.updateSecondaryBarItemInfo('my-bar', 'damage-taken', { value: '38' });shape sets how the chip is drawn. Set it at registration; it is not read from an update.
| Shape | Renders |
|---|---|
'pill' (default) |
A name then a number - the label-then-value chip described above |
'badge' |
The value alone in a small square-cornered box; neither label nor icon is shown |
'split' |
Two values, each in its own field. The leading one goes in the label slot but is typeset as a value - full size, no small-caps - so the two read as equals and the darker field does the separating |
A badge states its own meaning nowhere on the bar, so its tooltip is the only place a reader can find out what the number is. Push the tooltip with the value rather than fixing it at registration, or the tooltip will name the readout while the badge shows a figure it does not mention.
blacksmith.registerSecondaryBarItem('my-bar', 'round-turn', {
kind: 'statchip',
shape: 'split',
emphasis: 'feature',
zone: 'left',
label: '0',
valueParts: ['0', { text: ' of ', muted: true }, '0'],
tooltip: 'Round and turn',
group: 'encounter',
order: 0
});
blacksmith.updateSecondaryBarItemInfo('my-bar', 'round-turn', {
label: '4',
valueParts: ['2', { text: ' of ', muted: true }, '6'],
tooltip: 'Round 4 - turn 2 of 6'
});Composite values (valueParts): any chip kind takes valueParts in place of value when its reading is made of more than one number. Two forms:
| Part | Means |
|---|---|
'4' (plain string) |
A value |
{ text: ' of ', muted: true } |
Scaffolding - a separator or a joining word, drawn quieter and lighter so it organises the numbers instead of competing with them |
A unit belongs to its number and is not scaffolding: '2C' is one reading, and muting the C makes it look like an annotation on a bare 2. A chip may carry a label and valueParts together - split relies on it - and both patch in place.
Push valueParts through updateSecondaryBarItemInfo the same way. The number of parts is structure, so keep it constant across updates - changing the count forces a full bar rebuild rather than an in-place patch.
valueParts: ['6', { text: ' C ', muted: true }, '3', { text: ' F', muted: true }]Portraitstat item (kind: 'portraitstat'): A standing that belongs to a person. Renders image as a round, ringed portrait with value beside it; rank (1, 2, 3, or 0 for unranked) colours the ring gold, silver, or bronze. Falls back to icon - or a generic figure - when there is nobody to show, at the same size, so a standing changing hands never shifts the chips either side of it. Update with updateSecondaryBarItemInfo(barTypeId, itemId, { image, value, rank, tooltip }).
Use it where the answer to the readout is who. A face is recognised instantly where a truncated first name is not, so the name belongs in the tooltip rather than the chip.
blacksmith.registerSecondaryBarItem('my-bar', 'top-mvp', {
kind: 'portraitstat',
zone: 'middle',
rank: 1,
icon: 'fa-solid fa-trophy',
value: '',
tooltip: 'Top MVP on record',
group: 'stats',
order: 2
});
blacksmith.updateSecondaryBarItemInfo('my-bar', 'top-mvp', {
image: 'icons/portraits/kar-ahn.webp',
tooltip: 'Top MVP on record: Kar-ahn'
});Gaugechip item (kind: 'gaugechip'): A percentage as a horizontal meter plus its number. percentProgress (0-100) drives the fill; value is the text beside it, gaugeColor optionally overrides the fill colour. The meter is read before the digits are; the number stays because a meter this size cannot be read precisely. Update with updateSecondaryBarItemInfo(barTypeId, itemId, { percentProgress, value, label, tooltip }).
Push both value and percentProgress - the text is formatted for a reader and the fill needs a number, and deriving one from the other would re-parse a string that was built for display.
blacksmith.registerSecondaryBarItem('my-bar', 'hit-rate', {
kind: 'gaugechip',
zone: 'middle',
value: '0%',
percentProgress: 0,
tooltip: 'Party hit rate this combat',
group: 'stats',
order: 3
});
blacksmith.updateSecondaryBarItemInfo('my-bar', 'hit-rate', { value: '62.5%', percentProgress: 62.5 });Nameplate item (kind: 'nameplate'): A person and what they did, given room to say it. A large round portrait with a rank ring, label as the name on the first line and value as the standing beneath it. Both lines are always rendered - holding a non-breaking space when empty - so the plate keeps its height and its neighbours never shift as the standing changes hands. Update with updateSecondaryBarItemInfo(barTypeId, itemId, { image, label, value, rank, tooltip }).
Use it for the one readout meant to be looked at rather than glanced past, and give it a zone of its own: it is markedly wider than a chip, and in a shared zone it competes for width with readouts that get suppressed.
blacksmith.registerSecondaryBarItem('my-bar', 'mvp', {
kind: 'nameplate',
zone: 'left',
rank: 1,
icon: 'fa-solid fa-trophy',
label: '',
value: '',
tooltip: 'Top MVP on record',
group: 'mvp',
order: 0
});
blacksmith.updateSecondaryBarItemInfo('my-bar', 'mvp', {
image: 'icons/portraits/kar-ahn.webp',
label: 'Kar-ahn',
value: 'Top MVP · 128'
});Progressbar item (kind: 'progressbar'): A horizontal progress bar (0–100%). Required: width, borderColor, barColor, progressColor, percentProgress. Optional: title, icon, leftLabel, leftIcon, rightLabel, rightIcon, height. Height defaults to 40% of secondary bar height if omitted. Update with updateSecondaryBarItemInfo(barTypeId, itemId, { percentProgress, leftLabel, rightLabel, ... }).
blacksmith.registerSecondaryBarItem('my-bar', 'hp-bar', {
kind: 'progressbar',
zone: 'middle',
width: 200,
height: 14,
borderColor: 'rgba(0,0,0,0.5)',
barColor: '#2d5016',
progressColor: '#4a7c23',
percentProgress: 100,
leftLabel: '130',
rightLabel: '130',
group: 'stats',
order: 0
});
blacksmith.updateSecondaryBarItemInfo('my-bar', 'hp-bar', { percentProgress: 65, leftLabel: '85', rightLabel: '130' });Balancebar item (kind: 'balancebar'): A horizontal bar with origin at 0 in the middle; range -100 to +100. A marker (circle) is positioned at the current value: left of center for negative, right for positive. Required: width, borderColor, barColorLeft, barColorRight, markerColor. Optional: percentProgress (default 0), title, icon, leftLabel, rightLabel (inside the bar), leftIcon (outside the bar on the left), rightIcon (outside the bar on the right), height. Optional callbacks: onClick(event) for left-click; contextMenuItems (array or (itemId, item) => array of { name, icon, onClick }) for right-click context menu. Update with updateSecondaryBarItemInfo(barTypeId, itemId, { percentProgress, leftLabel, rightLabel, leftIcon, rightIcon, title, icon, barColorLeft, barColorRight, markerColor, borderColor }).
blacksmith.registerSecondaryBarItem('my-bar', 'approval', {
kind: 'balancebar',
zone: 'middle',
width: 240,
height: 14,
borderColor: 'rgba(0,0,0,0.5)',
barColorLeft: '#4a1c1c',
barColorRight: '#1c4a1c',
markerColor: '#7c2323',
percentProgress: -25,
leftLabel: 'Disapprove',
rightLabel: 'Approve',
group: 'stats',
order: 0,
contextMenuItems: [
{ name: 'Reset', icon: 'fas fa-minus', onClick: async () => { await myModule.setApproval(0); } },
{ name: 'Set to 50', icon: 'fas fa-equals', onClick: async () => { await myModule.setApproval(50); } }
]
});
blacksmith.updateSecondaryBarItemInfo('my-bar', 'approval', { percentProgress: 50, leftLabel: 'Disapprove', rightLabel: 'Approve' });Returns: boolean - Success status
Group Behavior:
- Items are organized into groups specified by the
groupparameter - Groups are separated by visual dividers
- Group behavior is controlled by the group's
mode(set inregisterSecondaryBarType):-
'default'mode: Independent buttons. Each button can be toggleable or perform actions independently. -
'switch'mode: Radio-button behavior. Only one button in the group can be active at a time, and one must always be active.
-
Example:
blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
icon: 'fa-solid fa-pencil', // Required: FontAwesome icon
label: 'Pencil', // Optional: Text label
tooltip: 'Draw with pencil tool', // Optional: Custom tooltip
active: false, // Optional: Active state
order: 10, // Optional: Display order
iconColor: '#3498db', // Optional: Icon color
buttonColor: 'rgba(100, 150, 200, 0.3)', // Optional: Custom button background color
borderColor: 'rgba(100, 150, 200, 0.5)', // Optional: Custom border color
onClick: (event) => { // Required: Click handler
console.log('Pencil tool clicked');
// Update active state
blacksmith.updateSecondaryBar({
activeTool: 'pencil'
});
}
});Timing-Safe: You can register items before the bar type is registered. Items will be queued and applied when the bar type is registered.
// Update an item's active state programmatically
blacksmith.updateSecondaryBarItemActive('cartographer', 'pencil-tool', true);Parameters:
-
barTypeId(string, required): The bar type ID -
itemId(string, required): The item ID to update -
active(boolean, required): The active state to set
Returns: boolean - Success status
Note:
- For
'switch'mode groups, setting an item active will automatically deactivate other items in the same group. - You cannot deactivate all items in a
'switch'mode group (one must always be active).
Example:
// Activate a tool in a switch group
blacksmith.updateSecondaryBarItemActive('cartographer', 'medium-line', true);
// This automatically deactivates 'small-line' and 'large-line' in the same groupUse this to update any display-only item without re-registering it: the value and label of an info, statchip, portraitstat, gaugechip, or nameplate item, the progress and labels of a progressbar, or the balance and labels of a balancebar. Typical for encounter-style bars (Party CR, Monster CR, Difficulty), HP/resource bars, or approval/balance bars.
blacksmith.updateSecondaryBarItemInfo('my-encounter', 'party-cr', { value: '4' });
blacksmith.updateSecondaryBarItemInfo('my-encounter', 'difficulty', { value: 'Deadly', label: '', iconColor: '#a02020', borderColor: null });Parameters:
-
barTypeId(string, required): The bar type ID -
itemId(string, required): The info item ID to update -
updates(Object, required): At least one of:-
Info items:
value,label,borderColor,buttonColor,iconColor -
Progressbar items:
percentProgress(number 0–100),leftLabel,rightLabel,leftIcon,rightIcon,title,icon,barColor,progressColor,borderColor -
Balancebar items:
percentProgress(number -100 to +100),leftLabel,rightLabel,leftIcon,rightIcon,title,icon,barColorLeft,barColorRight,markerColor,borderColor
-
Info items:
Returns: boolean - Success status
If the bar is currently open, it re-renders so the new value/label is visible immediately.
If you create a main toolbar button to toggle your secondary bar, register it so the button's active state automatically syncs when the bar opens/closes:
// 1. Register your secondary bar type
await blacksmith.registerSecondaryBarType('cartographer', {
size: 'default',
persistence: 'manual'
});
// 2. Register your toggle button
blacksmith.registerMenubarTool('cartographer-toggle', {
icon: 'fa-solid fa-map',
name: 'cartographer-toggle',
title: 'Toggle Cartographer Tools',
tooltip: 'Toggle Cartographer Tools',
zone: 'left',
group: 'general',
groupOrder: 999,
order: 20,
moduleId: 'coffee-pub-cartographer',
gmOnly: false,
leaderOnly: false,
visible: true,
toggleable: true, // Important: Make it toggleable for active state syncing
active: false,
iconColor: null,
buttonNormalTint: null,
buttonSelectedTint: null,
onClick: () => {
blacksmith.toggleSecondaryBar('cartographer');
}
});
// 3. Register the mapping so button state syncs automatically
blacksmith.registerSecondaryBarTool('cartographer', 'cartographer-toggle');Parameters:
-
barTypeId(string, required): The secondary bar type ID -
toolId(string, required): The menubar tool ID that toggles this bar
Returns: No return value.
Note:
- The tool must be registered with
toggleable: truefor the active state to work - When a different secondary bar opens, your button will automatically deactivate
- When your secondary bar opens, your button will automatically activate
- When your secondary bar closes, your button will automatically deactivate
Why use this? Without registration, you'd need to manually sync button states in your onClick handler. With registration, Blacksmith handles it automatically when bars open/close/switching.
// Remove an item from a secondary bar
blacksmith.unregisterSecondaryBarItem('cartographer', 'pencil-tool');Parameters:
-
barTypeId(string, required): The bar type ID -
itemId(string, required): The item ID to remove
Returns: boolean - Success status
Note: If the removed item was active in a 'switch' mode group, the first remaining item in that group will automatically become active.
// Get all registered items for a bar type
const items = blacksmith.getSecondaryBarItems('cartographer');
// Returns: Array of item data objectsParameters:
-
barTypeId(string, required): The bar type ID
Returns: Array<Object> - Array of registered item data objects
The default secondary bar items use CSS classes that you can override in your module's stylesheet:
CSS Classes:
-
.blacksmith-menubar-secondary .secondary-bar-item- Base item button -
.blacksmith-menubar-secondary .secondary-bar-item.active- Active/selected item -
.blacksmith-menubar-secondary .secondary-bar-item:hover- Hover state -
.blacksmith-menubar-secondary .secondary-bar-item:active- Clicked/pressed state -
.blacksmith-menubar-secondary .secondary-bar-item i- Icon element -
.blacksmith-menubar-secondary .secondary-bar-item-label- Label text -
.blacksmith-menubar-secondary .secondary-bar-toolbar- Container for all items
CSS Variables Available:
-
--blacksmith-menubar-fontcolor- Primary bar text color -
--blacksmith-menubar-fontsize- Primary bar font size -
--blacksmith-menubar-iconsize- Primary bar icon size -
--blacksmith-menubar-secondary-height- Secondary bar height, and the factor every size inside the bar scales from -
--blacksmith-menubar-secondary-default-height- The house default height a bar gets when it asks for no size -
--blacksmith-menubar-secondary-banner-allowance- Extra height a bar with group banners occupies, added on top of its height -
--blacksmith-menubar-total-height- Primary bar plus secondary bar plus the shadow offset and banner allowance; what the interface below the menubar is offset by -
--blacksmith-menubar-secondary-fontcolor- Secondary bar text color -
--blacksmith-menubar-secondary-fontsize- Secondary bar font size -
--blacksmith-menubar-secondary-iconsize- Secondary bar icon size -
--blacksmith-menubar-secondary-buttoncolor- Default button background color (used whenbuttonColornot specified) -
--blacksmith-menubar-secondary-bordercolor- Default border color (used whenborderColornot specified)
Custom Styling Example:
You can customize styling in two ways:
- Per-item colors (recommended for individual button styling):
blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
icon: 'fa-solid fa-pencil',
label: 'Pencil',
buttonColor: 'rgba(100, 150, 200, 0.3)', // Custom background
borderColor: 'rgba(100, 150, 200, 0.5)', // Custom border
onClick: () => {}
});- CSS overrides (for global styling of all items in a bar type):
/* In your module's CSS file */
.blacksmith-menubar-secondary[data-bar-type="cartographer"] .secondary-bar-item {
background-color: rgba(100, 150, 200, 0.3);
border-radius: 5px;
}
.blacksmith-menubar-secondary[data-bar-type="cartographer"] .secondary-bar-item.active {
background-color: rgba(100, 150, 200, 0.6);
box-shadow: 0 0 8px rgba(100, 150, 200, 0.4);
}Note:
- Per-item colors (via
buttonColorandborderColorparameters) are applied as inline styles and take precedence over CSS rules - CSS hover/active states will still apply on top of the inline styles
- Styles are defined in
styles/menubar.cssin the Blacksmith module. You can override them using more specific selectors in your module's CSS.
For complex UIs (like the combat bar with portraits and health rings), you can provide a custom Handlebars template:
- Create your template file:
templates/partials/menubar-{your-type-id}.hbs
- Register the bar type with
templatePath:
await blacksmith.registerSecondaryBarType('my-complex-bar', {
size: 'xlarge',
persistence: 'manual',
templatePath: 'modules/my-module/templates/partials/menubar-my-complex-bar.hbs'
});- Template Example:
{{!-- templates/partials/menubar-my-complex-bar.hbs --}}
<div class="my-complex-toolbar">
<div class="toolbar-header">
<h3>{{title}}</h3>
</div>
<div class="toolbar-content">
{{#each items}}
<div class="complex-item">
{{name}}: {{value}}
</div>
{{/each}}
</div>
</div>Important:
- Custom templates receive
secondaryBar.dataas their context - Templates are automatically registered as Handlebars partials:
menubar-{typeId} - You do NOT need to modify Blacksmith's main template - it handles custom templates automatically
Hooks.once('ready', async () => {
const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
if (!blacksmith) {
console.error('Blacksmith API not available');
return;
}
// 1. Register the bar type with group configuration
await blacksmith.registerSecondaryBarType('cartographer', {
height: 60,
persistence: 'manual',
autoCloseDelay: 10000,
groups: {
'tools': {
mode: 'default', // Independent buttons
order: 0
},
'line-size': {
mode: 'switch', // Only one active at a time
order: 10
},
'colors': {
mode: 'switch', // Only one active at a time
order: 20
}
}
});
// 2. Register tools in the 'tools' group (default mode - independent)
blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
icon: 'fa-solid fa-pencil',
label: 'Pencil',
group: 'tools',
toggleable: true, // Can be toggled on/off
order: 10,
onClick: () => {
console.log('Pencil tool toggled');
}
});
blacksmith.registerSecondaryBarItem('cartographer', 'eraser-tool', {
icon: 'fa-solid fa-eraser',
label: 'Eraser',
group: 'tools',
toggleable: true,
order: 20,
onClick: () => {
console.log('Eraser tool toggled');
}
});
// 3. Register line size options (switch group - only one active)
blacksmith.registerSecondaryBarItem('cartographer', 'small-line', {
icon: 'fa-solid fa-minus',
label: 'Small',
group: 'line-size',
order: 10,
onClick: () => {
console.log('Small line size selected');
}
});
blacksmith.registerSecondaryBarItem('cartographer', 'medium-line', {
icon: 'fa-solid fa-equals',
label: 'Medium',
group: 'line-size',
order: 20,
onClick: () => {
console.log('Medium line size selected');
}
});
blacksmith.registerSecondaryBarItem('cartographer', 'large-line', {
icon: 'fa-solid fa-grip-lines',
label: 'Large',
group: 'line-size',
order: 30,
onClick: () => {
console.log('Large line size selected');
}
});
// 4. Register color options (switch group)
blacksmith.registerSecondaryBarItem('cartographer', 'color-red', {
icon: 'fa-solid fa-circle',
label: 'Red',
group: 'colors',
iconColor: '#ff0000', // Red icon color
buttonColor: 'rgba(255, 0, 0, 0.5)',
order: 10,
onClick: () => {
console.log('Red color selected');
}
});
blacksmith.registerSecondaryBarItem('cartographer', 'color-blue', {
icon: 'fa-solid fa-circle',
label: 'Blue',
group: 'colors',
iconColor: '#0000ff', // Blue icon color
buttonColor: 'rgba(0, 0, 255, 0.5)',
order: 20,
onClick: () => {
console.log('Blue color selected');
}
});
// 5. Register a menubar tool to toggle the secondary bar
blacksmith.registerMenubarTool('cartographer-toggle', {
icon: 'fa-solid fa-map',
name: 'cartographer-toggle',
title: 'Toggle Cartographer Tools',
zone: 'left',
order: 20,
moduleId: 'coffee-pub-cartographer',
toggleable: true, // Enable toggleable to show active state
onClick: () => {
blacksmith.toggleSecondaryBar('cartographer');
}
});
// 6. Register the tool-to-bar mapping for automatic button state syncing
blacksmith.registerSecondaryBarTool('cartographer', 'cartographer-toggle');
});This creates a toolbar with:
- Tools group (left): Pencil and Eraser (independent toggleable buttons)
- Divider
- Line Size group (middle): Small, Medium, Large (switch mode - only one active)
- Divider
- Colors group (right): Red, Blue (switch mode - only one active)
Hooks.once('ready', async () => {
const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
// Register with custom template
await blacksmith.registerSecondaryBarType('my-complex-bar', {
height: 80,
persistence: 'manual',
templatePath: 'modules/my-module/templates/partials/menubar-my-complex-bar.hbs'
});
// Open the bar with data for the template
blacksmith.openSecondaryBar('my-complex-bar', {
data: {
title: 'Complex Toolbar',
items: [
{ name: 'Item 1', value: 100 },
{ name: 'Item 2', value: 200 }
]
}
});
});// Items can be registered before the bar type - they'll be queued and applied later
Hooks.once('ready', async () => {
const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
// Register items first (they'll be queued)
blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
icon: 'fa-solid fa-pencil',
label: 'Pencil',
onClick: () => console.log('Pencil')
});
// Register bar type later - items will be automatically applied
await blacksmith.registerSecondaryBarType('cartographer', {
height: 60,
persistence: 'manual'
});
// Items are now active!
});| Feature | Default Tool System | Custom Template |
|---|---|---|
| Recommended | Use this by default | Only when absolutely necessary |
| Complexity | Simple toolbars with buttons | Complex UIs (portraits, health rings, etc.) |
| Setup | Register items, no template needed | Create Handlebars template partial |
| Flexibility | Standardized button layout | Full control over HTML/CSS |
| Use Case | Drawing tools, simple controls | Combat bar, complex dashboards |
| Template Path | Not needed (omit templatePath) |
Required (templatePath in config) |
| Example | Cartographer drawing tools | Combat tracker with portraits |
| Maintenance | Low - API handles everything | Higher - maintain template, ensure compatibility |
| Feature | Regular Tools | Secondary Bar |
|---|---|---|
| Location | In main menubar (left/middle/right zones) | Below main menubar |
| Space | Limited (icon + label) | Full-width toolbar |
| Multiple | Many tools can be visible | Only one bar can be open |
| Use Case | Quick actions, status indicators | Complex toolbars, specialized interfaces |
| Template | Not needed | Optional (default tool system) or custom template |
- Default to default tool system: Always use the default tool system unless you absolutely need a custom template
-
Register early: Register your secondary bar type in a
readyhook (async if using custom templates) -
Choose the right approach:
- Default tool system: Use for all simple toolbars (drawing tools, filters, toggles, etc.)
- Custom templates: Only use for complex UIs that cannot be achieved with buttons (portraits, health rings, complex nested layouts)
-
Unique type IDs: Use descriptive, unique type IDs (e.g.,
'cartographer', not'tools') - Timing-safe registration: Items can be registered before bar types - they'll be queued automatically
- Template organization: If using custom templates, keep them simple and focused
- Data structure: For custom templates, pass structured data for flexibility
- Cleanup: Consider closing your bar and unregistering items when your module is disabled
Bar doesn't open:
- Verify the bar type is registered: Check console for registration success
- Check if another bar is open (it should close automatically)
- Verify the template partial exists and is named correctly
Template not rendering:
- For custom templates: Ensure the template path is correct and the file exists
- Verify
templatePathis provided inregisterSecondaryBarTypeconfig - For default tool system: Ensure items are registered with
registerSecondaryBarItem - Check that the bar type is registered before opening
Bar closes unexpectedly:
- Check the
persistencesetting (auto bars close after delay) - Verify no other code is calling
closeSecondaryBar()
- Ai assistance
- Applicationv2 traps
- CSS traps
- dnd5e conditions
- Documentation standard
- Token actor identity
- Campaign
- Canvas
- Chatcards
- Compendiums
- Contextmenu
- Core
- Create journal entry
- Dialog
- Active Effects
- Entity list
- Equiplocations
- Geography
- GM request
- Gmnotes
- Health
- Hookmanager
- Importer
- Inventory
- Menubar
- Notes
- Party
- Pins
- Quantity split
- Requestroll
- Rolls
- Scene config
- Sockets
- Stats
- Tags
- Toast
- Tokens
- Toolbar
- Window
- Worldclock