---
title: "Buttons and select menus in a Discord bot with discord.js v14 | StreetHosting"
description: "Create buttons and select menus in discord.js v14, handle each click in interactionCreate, reply within 3 seconds and disable the buttons after they are used."
url: "https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus"
type: "page"
language: "en-US"
---

Discord Bots · 7 min · Intermediate

Published on Nov 13, 2026 · Updated on Nov 13, 2026

# Discord bot buttons and menus: how to handle clicks

Learn to build ButtonBuilder and StringSelectMenuBuilder, read the customId in interactionCreate, meet the 3 second deadline, choose between a collector and a global handler and disable used buttons.

By [Equipe StreetHosting](https://streethosting.com.br/en/autores#equipe-streethosting) · StreetHosting infrastructure and support team

[Automation and webhooks](https://streethosting.com.br/en/guides/topics/automation)

Summarize with:

[](https://chat.openai.com/?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-buttons-select-menus.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "ChatGPT") [](https://claude.ai/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-buttons-select-menus.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Claude") [](https://www.google.com/search?udm=50&aep=11&q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-buttons-select-menus.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Google AI Mode") [](https://x.com/i/grok?text=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-buttons-select-menus.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Grok") [](https://www.perplexity.ai/search/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-buttons-select-menus.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Perplexity")

Share:

[](https://x.com/intent/tweet?text=Buttons%20and%20select%20menus%20in%20a%20Discord%20bot%20with%20discord.js%20v14&url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-buttons-select-menus "Share on X") [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-buttons-select-menus "Share on Facebook") [](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-buttons-select-menus "Share on LinkedIn") [](https://wa.me/?text=Buttons%20and%20select%20menus%20in%20a%20Discord%20bot%20with%20discord.js%20v14%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-buttons-select-menus "Share on WhatsApp")

For agents: Copy as Markdown [.md](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus.md)

In this guide 8 sections

* [How components work](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#como-funciona)
* [Create and send buttons](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#criar-botoes)
* [Handle the click in interactionCreate](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#interaction-create)
* [Select menu](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#menu-selecao)
* [The 3 second deadline](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#prazo-3-segundos)
* [Collector or global handler](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#collector-ou-global)
* [Disable buttons after use](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#desabilitar)
* [Where to run it at StreetHosting](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#hospedar)

Quick answer

Build the button with `ButtonBuilder`, put it inside an `ActionRowBuilder` and send it in `components`. In the `interactionCreate` event, check `isButton()` and read the `customId`. Reply within 3 seconds, or use `deferUpdate` and reply later. The menu follows the same idea with `StringSelectMenuBuilder`.

## How components work[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#como-funciona)

Buttons and menus are message components. You attach them to a bot message, the user clicks, and Discord sends your bot an interaction event with the component's identifier. That identifier is the **customId**, a text you choose and the only way to know which button was clicked.

Components live inside rows, the action rows. The fitting rules are strict and worth memorizing:

* A message takes up to 5 rows.
* A row takes up to 5 buttons, or a single select menu. You cannot mix a menu and a button in the same row.
* A text select menu takes up to 25 options.
* The customId has at most 100 characters and the button text at most 80.
* Within the same message, customIds must be unique.
* Link buttons, with `ButtonStyle.Link`, have a URL instead of a customId and do not trigger an event for the bot.

The examples use discord.js v14 and Node 20 or newer. Your bot needs working slash commands, because usually a command is what sends the message with the buttons. If you do not have that yet, see the guide on [slash commands on Discord](https://streethosting.com.br/en/guides/discord-bots/create-discord-bot-slash-commands). Components need no extra intent beyond Guilds.

## Create and send buttons[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#criar-botoes)

The button style comes from `ButtonStyle`: Primary (blue), Secondary (grey), Success (green), Danger (red) and Link (grey with a URL). The command below sends a message with two role buttons and one link to the rules.

`const { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('painel') .setDescription('Envia o painel de avisos'), async execute(interaction) { const linha = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('cargo:ID_DO_CARGO_NOTICIAS') .setLabel('Receber notícias') .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId('cargo:ID_DO_CARGO_EVENTOS') .setLabel('Receber eventos') .setStyle(ButtonStyle.Secondary), new ButtonBuilder() .setLabel('Regras') .setStyle(ButtonStyle.Link) .setURL('https://exemplo.com/regras') ); await interaction.reply({ content: 'Escolha os avisos que você quer receber:', components: [linha], }); }, };`

Note the customId format: `cargo:ID`. Putting the action and a piece of data separated by a colon lets a single block of code handle several similar buttons, reading the data at the end. You can also add an emoji to the button with `.setEmoji()`.

## Handle the click in interactionCreate[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#interaction-create)

Every click arrives in the `interactionCreate` event, the same one slash commands use. You filter by type and then by customId.

`const { Events, MessageFlags } = require('discord.js'); client.on(Events.InteractionCreate, async (interaction) => { if (interaction.isChatInputCommand()) { // seus slash commands continuam aqui return; } if (interaction.isButton()) { const [acao, dado] = interaction.customId.split(':'); if (acao === 'cargo') { const membro = interaction.member; if (membro.roles.cache.has(dado)) { await membro.roles.remove(dado); await interaction.reply({ content: 'Cargo removido.', flags: MessageFlags.Ephemeral, }); } else { await membro.roles.add(dado); await interaction.reply({ content: 'Cargo adicionado.', flags: MessageFlags.Ephemeral, }); } } } });`

A reply with `MessageFlags.Ephemeral` shows only to the person who clicked, which keeps the channel from filling up with confirmations. For the bot to change roles, it needs the permission to manage roles and its own role has to sit above the roles it hands out.

Never trust the customId to decide what the user may do. Anyone can click a public button, so check permissions and the owner of the action inside the code, for example with `interaction.memberPermissions`.

## Select menu[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#menu-selecao)

The text menu is the `StringSelectMenuBuilder`. Each option has a label, a value and an optional description. The value is what reaches your code, so use something short and stable, not the visible text.

`const { ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, } = require('discord.js'); const menu = new StringSelectMenuBuilder() .setCustomId('menu_cor') .setPlaceholder('Escolha a cor do seu nome') .setMinValues(1) .setMaxValues(1) .addOptions( new StringSelectMenuOptionBuilder() .setLabel('Vermelho') .setDescription('Nome em vermelho') .setValue('vermelho'), new StringSelectMenuOptionBuilder() .setLabel('Azul') .setDescription('Nome em azul') .setValue('azul') ); const linha = new ActionRowBuilder().addComponents(menu); await interaction.reply({ content: 'Personalize seu perfil:', components: [linha] });`

To handle the choice, inside the same `interactionCreate`, use `isStringSelectMenu()`. The chosen values come in an array. With `setMaxValues` above 1, the user can tick several options at once.

`` if (interaction.isStringSelectMenu() && interaction.customId === 'menu_cor') { const escolha = interaction.values[0]; await interaction.update({ content: `Você escolheu: ${escolha}`, components: [], }); } ``

## The 3 second deadline[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#prazo-3-segundos)

This is the number one mistake for beginners. After the click, the bot has 3 seconds to give its first reply. Go past that and Discord shows _This interaction failed_ to the user, and any late reply throws an unknown interaction error. A database query or a call to an external API can blow through that time without you noticing.

The way out is to acknowledge the click right away and do the heavy work afterwards. You have three ways to give the first reply:

* `reply()`: sends a new message, which can be only for the person who clicked.
* `update()`: edits the message the button is on, instantly.
* `deferReply()` and `deferUpdate()`: only acknowledge the click and buy time. After the defer, you have 15 minutes to finish with `editReply()`.

`if (interaction.isButton() && interaction.customId === 'gerar_relatorio') { // reconhece o clique agora, sem mudar nada na tela await interaction.deferUpdate(); const resultado = await gerarRelatorioDemorado(); await interaction.editReply({ content: resultado, components: [], }); }`

Use `deferReply` when the answer is a new message and `deferUpdate` when you will edit the button's message. Reply only once per interaction: a second `reply` call throws, and to send another message later you use `followUp()`.

## Collector or global handler[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#collector-ou-global)

There are two ways to listen for clicks, and each fits a different kind of situation.

The **collector** listens to one specific message for a set time. It is great for short exchanges, like a confirmation prompt: the command sends Confirm and Cancel, waits up to 30 seconds for the answer from whoever ran the command and ends.

`const { ComponentType } = require('discord.js'); const resposta = await interaction.reply({ content: 'Tem certeza?', components: [linhaConfirmar], withResponse: true, }); const mensagem = resposta.resource.message; const coletor = mensagem.createMessageComponentCollector({ componentType: ComponentType.Button, time: 30_000, filter: (i) => i.user.id === interaction.user.id, }); coletor.on('collect', async (i) => { const confirmou = i.customId === 'confirmar'; await i.update({ content: confirmou ? 'Feito.' : 'Cancelado.', components: [], }); coletor.stop('respondido'); }); coletor.on('end', async (_coletados, motivo) => { if (motivo === 'time') { await interaction.editReply({ content: 'Tempo esgotado.', components: [] }); } });`

The `filter` makes sure only whoever ran the command can click. Anyone clicking from outside is ignored by the collector, and Discord shows the interaction failure to that person, which here is the expected behavior.

The **global handler** is the code in `interactionCreate` we saw before. It does not depend on any particular message, only on the customId. That is why it suits permanent panels, like role buttons, tickets and verification, which have to stay valid days later and survive a bot restart.

Rule of thumb: if the button should work for seconds or minutes and only for one person, use a collector. If the button stays pinned in a channel for anyone to click, use the global handler and put the data it needs in the customId, as in the `cargo:ID` example.

That is the pattern used, for example, in a [support ticket system](https://streethosting.com.br/en/guides/discord-bots/discord-ticket-bot), where the open ticket button has to work forever.

## Disable buttons after use[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#desabilitar)

When an action can only happen once, like accepting an invite or voting, disable the buttons after the click. That way nobody clicks twice. Since the component that arrives in the interaction is read only, you recreate each button with `ButtonBuilder.from` and turn on `setDisabled(true)`.

`const { ActionRowBuilder, ButtonBuilder } = require('discord.js'); const linhaDesativada = new ActionRowBuilder().addComponents( interaction.message.components[0].components.map((botao) => ButtonBuilder.from(botao).setDisabled(true) ) ); await interaction.update({ content: 'Resposta registrada.', components: [linhaDesativada], });`

If the message has more than one row, repeat the process for each one. On messages with a collector, do the same in the `end` event, so buttons with no active collector do not stay clickable and failing.

1. Acknowledge the click with update, which already swaps the buttons for disabled versions.
2. Record the action in a file or database and check that record on every click, because two very fast clicks can arrive before the buttons are disabled.
3. For actions the user can repeat, like roles, leave the button enabled.

## Where to run it at StreetHosting[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#hospedar)

Buttons and menus only respond while the bot is on. If it goes down, anyone who clicks sees the interaction failure. On the [StreetHosting bot hosting](https://streethosting.com.br/en/bots) the bot runs in a Node.js container with a panel and stays up all the time. The Discord Essential plan, at R$ 9.99/month with 1 GB of RAM, is enough for a bot with panels and confirmations.

Since the global handler does not depend on process memory, the bot can restart on a code update and the old buttons keep working. To start and keep the process up without downtime, see the guide on [deploying a Discord bot with PM2](https://streethosting.com.br/en/guides/discord-bots/deploy-discord-bot-pm2). If you want to collect long text from the user instead of clicks, the next step is [modals and forms](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms).

In this guide

* [How components work](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#como-funciona)
* [Create and send buttons](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#criar-botoes)
* [Handle the click in interactionCreate](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#interaction-create)
* [Select menu](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#menu-selecao)
* [The 3 second deadline](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#prazo-3-segundos)
* [Collector or global handler](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#collector-ou-global)
* [Disable buttons after use](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#desabilitar)
* [Where to run it at StreetHosting](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus#hospedar)

## Frequently asked questions

Why does This interaction failed show up after clicking the button?

Because the bot did not reply within 3 seconds, or because no code is listening for that customId. Reply with reply, update or deferUpdate right at the start of the handling and check that the button customId matches what your code compares.

How many buttons fit in one message?

Each row of components takes up to 5 buttons, and a message takes up to 5 rows, so the maximum is 25 buttons. A select menu takes a whole row by itself.

Do the buttons keep working after the bot restarts?

They do if the handling lives in a global handler on interactionCreate, because Discord delivers the click to the bot even on old messages. With a collector they do not, since it lives only in the process memory and disappears on restart.

What is the difference between deferReply and deferUpdate?

deferReply tells Discord you will answer with a new message and shows the bot is thinking notice. deferUpdate just acknowledges the click and prepares you to edit the original message later, without showing anything to the user.

How many options fit in a select menu?

Up to 25 options per menu. If you need more than that, split them into categories with several menus in different messages, or show the options in pages with previous and next buttons.

Next step

See bot plans

24/7 Discord bots with fast activation and support in Portuguese.

[See bot plans](https://streethosting.com.br/en/bots)

## Related guides

[Discord bots Beginner How to create a Discord bot with slash commands Slash commands improve the user experience and make your bot easier to use. Here is a clear path to get started. 2 min Read guide](https://streethosting.com.br/en/guides/discord-bots/create-discord-bot-slash-commands) [Discord bots Intermediate Modals and forms in a Discord bot: a staff application with discord.js Learn to open a modal with showModal, build fields with TextInputBuilder, read the answers with fields.getTextInputValue and send everything to a staff channel with approve and reject buttons. 6 min Read guide](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms) [Discord bots Beginner Discord embeds with discord.js: how to build formatted messages The embed is the colored card that gives your bot's messages a professional look. Learn to build one from scratch, the official limits, what markdown works inside it and how to edit an embed that was already sent. 7 min Read guide](https://streethosting.com.br/en/guides/discord-bots/discordjs-embed-formatted-messages)

[← Back to the Guide Center](https://streethosting.com.br/en/guides)
