| name | fvtt-chat-messages |
| description | This skill should be used when creating chat messages, sending roll results to chat, configuring speakers, implementing whispers and roll modes, or hooking into chat message rendering. |
Foundry VTT Chat Messages
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-05
Overview
ChatMessage documents display in the chat log and handle rolls, whispers, and player communication. Understanding message creation and hooks is essential for game system features.
When to Use This Skill
- Creating chat messages programmatically
- Sending dice roll results to chat
- Configuring message speakers
- Implementing whispers and roll modes
- Adding custom rendering to messages
ChatMessage Structure
Core Fields
{
_id: "documentId",
author: "userId", // Who created the message
content: "<p>HTML</p>", // Message content
flavor: "Roll description", // Flavor text for rolls
speaker: { // Who is "speaking"
scene: "sceneId",
actor: "actorId",
token: "tokenId",
alias: "Display Name"
},
whisper: ["userId"], // Private recipients
blind: false, // GM-only visibility
rolls: [], // Dice roll data
sound: "audio/path.ogg", // Sound to play
flags: {} // Custom data
}
Creating Messages
Simple Text Message
await ChatMessage.create({
content: "Hello, world!"
});
Message with Speaker
await ChatMessage.create({
content: "I attack the dragon!",
speaker: ChatMessage.getSpeaker({ actor: myActor })
});
HTML Content
await ChatMessage.create({
content: `
<h2>Critical Hit!</h2>
<p>You deal <strong>24</strong> damage.</p>
`,
speaker: ChatMessage.getSpeaker({ token: myToken })
});
With Custom Flags
await ChatMessage.create({
content: "Attack roll",
flags: {
"my-module": {
rollType: "attack",
targetId: target.id
}
}
});
Roll Messages
Using Roll.toMessage() (Recommended)
const roll = new Roll("1d20 + @mod", { mod: 5 });
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: "Attack Roll"
});
With Roll Mode
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: "Stealth Check"
}, {
rollMode: game.settings.get("core", "rollMode")
});
Multiple Rolls
const attackRoll = new Roll("1d20 + 5");
const damageRoll = new Roll("2d6 + 3");
await attackRoll.evaluate();
await damageRoll.evaluate();
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: "Attack and Damage",
rolls: [attackRoll, damageRoll]
});
Speaker Configuration
getSpeaker()
// From controlled token (default)
const speaker = ChatMessage.getSpeaker();
// From specific actor
const speaker = ChatMessage.getSpeaker({ actor: myActor });
// From specific token
const speaker = ChatMessage.getSpeaker({ token: myToken });
// Custom alias
const speaker = ChatMessage.getSpeaker({ alias: "The Narrator" });
Speaker Structure
{
scene: "sceneId", // Scene where speaker is
actor: "actorId", // Actor document ID
token: "tokenId", // Token document ID
alias: "Display Name" // Fallback name
}
Get Speaker's Actor
const actor = ChatMessage.getSpeakerActor(message.speaker);
Whispers and Roll Modes
Roll Modes
| Mode | Visibility | Command |
|---|---|---|
| Public | Everyone | /publicroll |
| GM | Roller + GMs | /gmroll |
| Blind | GMs only | /blindroll |
| Self | Roller only | /selfroll |
Apply Roll Mode
// Get current setting
const rollMode = game.settings.get("core", "rollMode");
// Apply to roll
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor })
}, {
rollMode: rollMode
});
Whisper to Specific Users
// Single user
await ChatMessage.create({
content: "Secret message",
whisper: [targetUserId]
});
// Multiple users
await ChatMessage.create({
content: "Group secret",
whisper: [user1Id, user2Id]
});
// All GMs
await ChatMessage.create({
content: "GM only",
whisper: game.users.filter(u => u.isGM).map(u => u.id)
});
Blind Messages
// GM sees content, others see "???"
await ChatMessage.create({
content: "Secret roll result: 15",
blind: true,
whisper: game.users.filter(u => u.isGM).map(u => u.id)
});
Chat Hooks
renderChatMessageHTML (V13+)
Hooks.on("renderChatMessageHTML", (message, html, context) => {
// message: ChatMessage document
// html: HTMLElement
// context: Rendering context
// Add custom styling
if (message.flags["my-module"]?.critical) {
html.classList.add("critical-hit");
}
// Add buttons
const button = document.createElement("button");
button.textContent = "Apply Damage";
button.addEventListener("click", () => applyDamage(message));
html.querySelector(".message-content").append(button);
});
preCreateChatMessage
Hooks.on("preCreateChatMessage", (message, data, options, userId) => {
// Modify before creation
message.updateSource({
content: data.content + " (modified)"
});
// Return false to cancel
return true;
});
createChatMessage
Hooks.on("createChatMessage", (message, options, userId) => {
// After creation, for all clients
console.log("New message:", message.content);
});
chatMessage
Hooks.on("chatMessage", (chatLog, messageText, chatData) => {
// When user sends message via input
// Return false to prevent default handling
if (messageText.startsWith("/custom")) {
handleCustomCommand(messageText);
return false;
}
});
Common Patterns
Roll with Button
async function attackRoll(actor, target) {
const roll = new Roll("1d20 + @mod", actor.getRollData());
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: `Attack vs ${target.name}`,
flags: {
"my-system": {
type: "attack",
targetId: target.id,
total: roll.total
}
}
});
}
// Handle button clicks
Hooks.on("renderChatMessageHTML", (message, html) => {
const flags = message.flags["my-system"];
if (flags?.type !== "attack") return;
html.querySelector(".apply-damage")?.addEventListener("click", () => {
const target = game.actors.get(flags.targetId);
// Apply damage logic
});
});
Collapsible Details
await ChatMessage.create({
content: `
<div class="roll-result">
<h3>Attack Roll: 18</h3>
<details>
<summary>Details</summary>
<p>Base: 1d20 = 13</p>
<p>Modifier: +5</p>
</details>
</div>
`
});
Sound with Message
await ChatMessage.create({
content: "The bell tolls...",
sound: "sounds/bell.ogg"
});
Common Pitfalls
1. Forgetting Roll Evaluation
// WRONG - total is undefined
const roll = new Roll("1d20");
await roll.toMessage(); // roll.total undefined!
// CORRECT
const roll = new Roll("1d20");
await roll.evaluate();
await roll.toMessage();
2. Wrong Speaker Token
// WRONG - uses first controlled token
ChatMessage.getSpeaker();
// CORRECT - specify the token
ChatMessage.getSpeaker({ token: specificToken });
3. Whisper vs Roll Mode Conflict
// Roll messages override whisper with rollMode
// Use rollMode for roll messages:
await roll.toMessage({}, {
rollMode: "gmroll" // Not whisper: [...]
});
4. Ignoring Roll Mode Setting
// WRONG - always public
await roll.toMessage();
// CORRECT - respect user setting
await roll.toMessage({}, {
rollMode: game.settings.get("core", "rollMode")
});
5. Message Update Timing
// Updating too fast causes UI issues
// Wait for notification to fade (~3 seconds)
const msg = await ChatMessage.create({ content: "Loading..." });
setTimeout(() => {
msg.update({ content: "Done!" });
}, 3500);
6. Not Checking Visibility
// Check if message is visible to current user
if (!message.visible) return;
// Check if content is visible (not just presence)
if (message.isContentVisible) {
// Safe to read content
}
Implementation Checklist
- Use
ChatMessage.getSpeaker()for proper speaker - Always
await roll.evaluate()before toMessage - Respect
game.settings.get("core", "rollMode") - Use
renderChatMessageHTMLhook for customization - Store custom data in flags, not content
- Handle whisper recipients appropriately
- Test all roll modes (public, GM, blind, self)
- Add sound effects where appropriate
References
Last Updated: 2026-01-05 Status: Production-Ready Maintainer: ImproperSubset