Right-click commands (context menus)

The commands that live in the right-click menu, not the slash list — "User Info" on a member, "Report" on a message. Build both kinds in discord.js and read the target the user clicked.

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, not user_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.targetUser is the clicked member; interaction.targetMessage is 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 .content inside 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.commands scope.
  • "This interaction failed" — no matching guard, or you didn't reply within about three seconds. Match on commandName exactly and reply promptly.
  • targetMessage.content is 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 return after handling one. Return after each branch so a single interaction takes one path.

Next steps

Was this guide helpful?