Handling buttons and menus
When someone clicks a button or picks something in a menu, Discord sends your bot an interaction. Your bot must respond within 3 seconds.
How it works
Section titled “How it works”- Your bot sends a message with a button that has
custom_id: "counter". - A user clicks the button.
- Discord sends your bot a
MESSAGE_COMPONENT(3) interaction with thatcustom_id. - Your bot looks at the
custom_id, decides what to do and responds (new message, edit the message or open a modal).
What your bot receives
Section titled “What your bot receives”Inside the interaction, the data field has the component information:
| Field | Type | Description |
|---|---|---|
custom_id |
string | The custom_id you set on the component. |
component_type |
integer | Component type (2 = button, 3 = string select…). |
values |
array of strings | Menus only: the selected values. |
resolved |
object | User/role/channel menus only: full data (users, roles, channels) of the selection. |
{ "type": 3, "id": "1234567890", "token": "aW50ZXJhY3Rpb24...", "data": { "custom_id": "counter", "component_type": 2, "id": 3 }, "message": { "id": "...", "flags": 32768, "components": [] }}Response types
Section titled “Response types”| Type | Value | What it does | When to use it |
|---|---|---|---|
CHANNEL_MESSAGE_WITH_SOURCE |
4 |
Sends a new message. | Reply with information (can be ephemeral). |
DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE |
5 |
Shows “thinking…” and you reply later. | When you need more than 3 s to build the message. |
DEFERRED_UPDATE_MESSAGE |
6 |
Acknowledges the click showing nothing; you edit later. | Slow work that ends up editing the message. |
UPDATE_MESSAGE |
7 |
Edits the message the button is on. | Counters, pagination, toggling state. |
MODAL |
9 |
Opens a form (modal). | Ask the user for text, files or choices. |
Full example: a counter
Section titled “Full example: a counter”Every time someone clicks the button, the message is updated with the new number (response type 7).
{ "flags": 32768, "components": [ { "type": 17, "accent_color": 5793266, "components": [ { "type": 10, "content": "### Counter\nYou clicked the button **3** times." }, { "type": 1, "components": [ { "type": 2, "style": 1, "label": "+1", "custom_id": "counter:3" }, { "type": 2, "style": 4, "label": "Reset", "custom_id": "reset" } ] } ] } ]}Respond to the interaction with UPDATE_MESSAGE (7). The body is the complete new message:
{ "type": 7, "data": { "flags": 32768, "components": [ { "type": 17, "accent_color": 5793266, "components": [ { "type": 10, "content": "### Counter\nYou clicked the button **4** times." }, { "type": 1, "components": [ { "type": 2, "style": 1, "label": "+1", "custom_id": "counter:4" }, { "type": 2, "style": 4, "label": "Reset", "custom_id": "reset" } ] } ] } ] }}import { Events, MessageFlags, ContainerBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
function counterMessage(count) { const container = new ContainerBuilder() .setAccentColor(0x5865f2) .addTextDisplayComponents((text) => text.setContent(`### Counter\nYou clicked the button **${count}** times.`), ) .addActionRowComponents((row) => row.setComponents( new ButtonBuilder().setCustomId(`counter:${count}`).setLabel('+1').setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId('reset').setLabel('Reset').setStyle(ButtonStyle.Danger), ), ); return { flags: MessageFlags.IsComponentsV2, components: [container] };}
client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isButton()) return;
if (interaction.customId.startsWith('counter:')) { const count = Number(interaction.customId.split(':')[1]) + 1; await interaction.update(counterMessage(count)); // type 7 }
if (interaction.customId === 'reset') { await interaction.update(counterMessage(0)); }});import discord
def counter_view(count: int) -> discord.ui.LayoutView: plus = discord.ui.Button(label="+1", style=discord.ButtonStyle.primary, custom_id=f"counter:{count}") reset = discord.ui.Button(label="Reset", style=discord.ButtonStyle.danger, custom_id="reset")
async def on_plus(interaction: discord.Interaction): await interaction.response.edit_message(view=counter_view(count + 1)) # type 7
async def on_reset(interaction: discord.Interaction): await interaction.response.edit_message(view=counter_view(0))
plus.callback = on_plus reset.callback = on_reset
view = discord.ui.LayoutView() view.add_item( discord.ui.Container( discord.ui.TextDisplay(f"### Counter\nYou clicked the button **{count}** times."), discord.ui.ActionRow(plus, reset), accent_colour=0x5865F2, ) ) return view
# To send it:# await channel.send(view=counter_view(0))func counterComponents(count int) []discordgo.MessageComponent { color := 0x5865F2 return []discordgo.MessageComponent{ discordgo.Container{ AccentColor: &color, Components: []discordgo.MessageComponent{ discordgo.TextDisplay{Content: fmt.Sprintf("### Counter\nYou clicked the button **%d** times.", count)}, discordgo.ActionsRow{Components: []discordgo.MessageComponent{ discordgo.Button{Label: "+1", Style: discordgo.PrimaryButton, CustomID: fmt.Sprintf("counter:%d", count)}, discordgo.Button{Label: "Reset", Style: discordgo.DangerButton, CustomID: "reset"}, }}, }, }, }}
s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { if i.Type != discordgo.InteractionMessageComponent { return }
customID := i.MessageComponentData().CustomID count := 0 if strings.HasPrefix(customID, "counter:") { count, _ = strconv.Atoi(strings.TrimPrefix(customID, "counter:")) count++ }
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseUpdateMessage, // type 7 Data: &discordgo.InteractionResponseData{ Flags: discordgo.MessageFlagsIsComponentsV2, Components: counterComponents(count), }, })})Replying with an ephemeral message
Section titled “Replying with an ephemeral message”An ephemeral message is only visible to the person who clicked. Combine the IS_COMPONENTS_V2 (32768) and EPHEMERAL (64) flags = 32832.
{ "type": 4, "data": { "flags": 32832, "components": [ { "type": 10, "content": "✅ You're signed up for the event. See you there!" } ] }}await interaction.reply({ flags: MessageFlags.IsComponentsV2 | MessageFlags.Ephemeral, components: [new TextDisplayBuilder().setContent("✅ You're signed up for the event. See you there!")],});view = discord.ui.LayoutView()view.add_item(discord.ui.TextDisplay("✅ You're signed up for the event. See you there!"))await interaction.response.send_message(view=view, ephemeral=True)s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, // type 4 Data: &discordgo.InteractionResponseData{ Flags: discordgo.MessageFlagsIsComponentsV2 | discordgo.MessageFlagsEphemeral, Components: []discordgo.MessageComponent{ discordgo.TextDisplay{Content: "✅ You're signed up for the event. See you there!"}, }, },})Reading menu selections
Section titled “Reading menu selections”For menus, the selected values arrive in data.values:
{ "custom_id": "pick_color", "component_type": 3, "values": ["red", "blue"]}if (interaction.isStringSelectMenu() && interaction.customId === 'pick_color') { const colors = interaction.values; // ['red', 'blue']}select = discord.ui.Select(custom_id="pick_color", options=[...])
async def on_select(interaction: discord.Interaction): colors = select.values # ['red', 'blue']
select.callback = on_selectdata := i.MessageComponentData()if data.CustomID == "pick_color" { colors := data.Values // []string{"red", "blue"} _ = colors}Editing after responding
Section titled “Editing after responding”If you used DEFERRED_UPDATE_MESSAGE (6) or want to change the response later, edit the original response:
PATCH /webhooks/{application.id}/{interaction.token}/messages/@originalAnd to send extra messages (follow-ups):
POST /webhooks/{application.id}/{interaction.token}Next step
Section titled “Next step”Learn how to ask users for input with modals.