Not every command belongs in the / list. Some make more sense as a right-click: right-click a member for User Info, right-click a message for Report. These are context menu commands — they take no typed options, just the thing the user clicked on. This recipe builds one of each and reads the target.
| At a glance | |
|---|---|
| You need | a working bot from the discord.js guide, and comfort with Slash commands in depth |
| Plan | free or premium — free runs while your session timer lasts |
| Time | about fifteen minutes |
Context menus register the same way slash commands do — through application.commands.set([...]) in ClientReady — and arrive through the same InteractionCreate event. Only the builder and the way you read input change.
Two kinds
| Kind | Where it appears | What your code gets |
|---|---|---|
| User | right-click a member → Apps | the user that was clicked (interaction.targetUser) |
| Message | right-click a message → Apps | the message that was clicked (interaction.targetMessage) |
🎯 Good to know: A context menu command takes no options — the "input" is whatever the user right-clicked. That's the whole point: it acts on something already on screen, so there's nothing to type.
Build both
Import the builder and the type enum:
const {
Client, Events, GatewayIntentBits,
ContextMenuCommandBuilder, ApplicationCommandType,
} = require('discord.js');
A context menu is a name plus a type. Build one of each:
const userInfo = new ContextMenuCommandBuilder()
.setName('User Info')
.setType(ApplicationCommandType.User);
const reportMsg = new ContextMenuCommandBuilder()
.setName('Report Message')
.setType(ApplicationCommandType.Message);
Register them in ClientReady, in the same array as your slash commands — they all live in one command list:
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set([userInfo, reportMsg]);
console.log(`Listening as ${c.user.tag}`);
});
💡 Tip: The name is what users see in the right-click menu, so write it in plain Title Case —
User Info, notuser_info. Unlike slash command names, spaces and capitals are allowed.
Handle the clicks
Both arrive through InteractionCreate. Guard each with its own check, then read the target:
client.on(Events.InteractionCreate, async (interaction) => {
if (interaction.isUserContextMenuCommand() && interaction.commandName === 'User Info') {
const user = interaction.targetUser;
await interaction.reply({
content: `**${user.tag}**\nID: ${user.id}\nJoined Discord: <t:${Math.floor(user.createdTimestamp / 1000)}:R>`,
ephemeral: true,
});
return;
}
if (interaction.isMessageContextMenuCommand() && interaction.commandName === 'Report Message') {
const message = interaction.targetMessage;
await interaction.reply({
content: `Reported a message from ${message.author.tag}.`,
ephemeral: true,
});
}
});
The two things that make this work:
isUserContextMenuCommand()/isMessageContextMenuCommand()tell the two kinds apart (and apart from slash commands and buttons).interaction.targetUseris the clicked member;interaction.targetMessageis the clicked message — with its.author,.content,.id, everything a message has.
The <t:…:R> in the reply is a Discord timestamp: give it a Unix time in seconds and Discord renders a live "3 years ago" that updates itself. Handy for join dates and "created" fields.
⚠️ Heads up: Reading a message's
.contentinside a Message context menu needs the Message Content privileged intent enabled on the Bot page — even though the command itself doesn't. Reporting that a message exists (author, ID) works without it; showing what it said does not.
Context menu or slash command?
| Use a... | when the action... |
|---|---|
| Context menu | targets one user or one message already on screen — info, report, quote, warn |
| Slash command | needs typed input, or isn't about a specific thing you can point at |
Many bots offer both: /userinfo @someone for typing a name, plus a User Info right-click for the person in front of you. They can share the same reply-building code.
Limits worth knowing
- A name is up to 32 characters.
- An application may have up to 5 user context menus and 5 message context menus (separate from your slash command budget).
- No options, no subcommands — if you need input, it's a slash command or a follow-up modal.
Verify it works
Start the bot, then in Discord right-click a member and open Apps → User Info, and right-click any message and open Apps → Report Message. Each replies privately (ephemeral). First-time registration can take a few minutes to appear, same as slash commands.
Troubleshooting
- The command isn't in the right-click menu — first-time global registration lags a few minutes; also confirm the bot was invited with the
applications.commandsscope. - "This interaction failed" — no matching guard, or you didn't reply within about three seconds. Match on
commandNameexactly and reply promptly. targetMessage.contentis empty — the Message Content intent isn't enabled. Turn it on in the Developer Portal, or don't rely on the text.- Both handlers fire / wrong one runs — you're missing a
returnafter handling one. Return after each branch so a single interaction takes one path.