---
title: "Botões e menus de seleção no bot Discord com discord.js v14"
description: "Crie botões e menus de seleção no discord.js v14, trate cada clique no interactionCreate, responda dentro de 3 segundos e desative os botões depois do uso."
url: "https://streethosting.com.br/guias/bots-discord/bot-discord-botoes-menus-interativos"
category: "bots-discord"
slug: "bot-discord-botoes-menus-interativos"
datePublished: "2026-11-13"
dateModified: "2026-11-13"
author: "Equipe StreetHosting"
difficulty: "intermediario"
language: "pt-BR"
keywords:
  - "bot discord botões menus discord.js"
  - "discord.js ButtonBuilder"
  - "discord.js StringSelectMenuBuilder"
  - "interactionCreate customId"
  - "discord.js collector botão"
  - "desabilitar botão discord.js"
---

# Bot Discord com botões e menus: como tratar os cliques

Veja como montar ButtonBuilder e StringSelectMenuBuilder, ler o customId no interactionCreate, cumprir o prazo de 3 segundos, escolher entre collector e handler global e desabilitar botões usados.

> **Resposta rápida**
>
> Monte o botão com `ButtonBuilder`, coloque dentro de um `ActionRowBuilder` e envie em `components`. No evento `interactionCreate`, confira `isButton()` e leia o `customId`. Responda em até 3 segundos, ou use `deferUpdate` e responda depois. O menu segue a mesma ideia com `StringSelectMenuBuilder`.

## Como funcionam os componentes

Botões e menus são componentes de mensagem. Você anexa eles a uma mensagem do bot, o usuário clica, e o Discord manda para o seu bot um evento de interação com o identificador do componente. Esse identificador é o **customId**, um texto que você escolhe e que é a única forma de saber qual botão foi clicado.

Os componentes ficam dentro de linhas, as action rows. As regras de encaixe são rígidas e valem a pena decorar:

- Uma mensagem aceita até 5 linhas.
- Uma linha aceita até 5 botões, ou um único menu de seleção. Não dá para misturar menu com botão na mesma linha.
- Um menu de seleção de texto aceita até 25 opções.
- O customId tem no máximo 100 caracteres e o texto do botão no máximo 80.
- Dentro da mesma mensagem, os customId precisam ser únicos.
- Botões de link, com `ButtonStyle.Link`, têm URL em vez de customId e não geram evento para o bot.

Os exemplos usam discord.js v14 e Node 20 ou mais novo. O bot precisa ter slash commands funcionando, porque normalmente é um comando que envia a mensagem com os botões. Se ainda não tem isso, veja o guia de [slash commands no Discord](https://streethosting.com.br/guias/bots-discord/criar-bot-discord-slash-commands). Componentes não precisam de intent extra além de Guilds.

## Criar e enviar botões

O estilo do botão vem de `ButtonStyle`: Primary (azul), Secondary (cinza), Success (verde), Danger (vermelho) e Link (cinza com URL). O comando abaixo envia uma mensagem com dois botões de cargo e um de link para as regras.

```
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],
    });
  },
};
```

Repare no formato do customId: `cargo:ID`. Colocar a ação e um dado separados por dois pontos permite que um único trecho de código trate vários botões parecidos, lendo o dado no fim. Também pode usar emoji no botão com `.setEmoji()`.

## Tratar o clique no interactionCreate

Todo clique chega no evento `interactionCreate`, o mesmo dos slash commands. Você filtra pelo tipo e depois pelo 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 resposta com `MessageFlags.Ephemeral` aparece só para quem clicou, o que evita encher o canal de confirmações. Para o bot mexer em cargos, ele precisa da permissão de gerenciar cargos e o cargo dele tem que ficar acima dos cargos que ele entrega.

> **Atenção**
>
> Nunca confie no customId para decidir o que o usuário pode fazer. Qualquer um pode clicar em um botão público, então confira permissões e dono da ação dentro do código, como `interaction.memberPermissions`.

## Menu de seleção

O menu de texto é o `StringSelectMenuBuilder`. Cada opção tem rótulo, valor e uma descrição opcional. O valor é o que chega ao seu código, então use algo curto e estável, não o texto visível.

```
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] });
```

Para tratar a escolha, dentro do mesmo `interactionCreate`, use `isStringSelectMenu()`. Os valores escolhidos vêm em um array. Com `setMaxValues` maior que 1, o usuário marca várias opções de uma vez.

```
if (interaction.isStringSelectMenu() && interaction.customId === 'menu_cor') {
  const escolha = interaction.values[0];

  await interaction.update({
    content: `Você escolheu: ${escolha}`,
    components: [],
  });
}
```

## O prazo de 3 segundos

Esse é o erro número um de quem começa. Depois do clique, o bot tem 3 segundos para dar a primeira resposta. Passou disso, o Discord mostra *Esta interação falhou* para o usuário, e qualquer resposta tardia dá erro de interação desconhecida. Um acesso a banco de dados ou a uma API externa pode estourar esse tempo sem você perceber.

A saída é reconhecer o clique logo e fazer o trabalho pesado depois. Você tem três jeitos de dar a primeira resposta:

- `reply()`: manda uma mensagem nova, que pode ser só para quem clicou.
- `update()`: edita a mensagem onde o botão está, no mesmo instante.
- `deferReply()` e `deferUpdate()`: só reconhecem o clique e compram tempo. Depois do defer, você tem 15 minutos para concluir com `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` quando a resposta for uma mensagem nova e `deferUpdate` quando for editar a mensagem do botão. Só responda uma vez a cada interação: uma segunda chamada de `reply` dá erro, e para mandar outra mensagem depois você usa `followUp()`.

## Collector ou handler global

Existem duas formas de escutar os cliques, e cada uma serve para um tipo de situação.

O **collector** escuta uma mensagem específica por um tempo definido. É ótimo para conversas curtas, como um pedido de confirmação: o comando envia Confirmar e Cancelar, espera até 30 segundos pela resposta de quem usou o comando e encerra.

```
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: [] });
  }
});
```

O `filter` garante que só quem rodou o comando consegue clicar. Quem clica de fora é ignorado pelo coletor, e o Discord mostra a falha de interação para essa pessoa, o que no caso é o comportamento esperado.

O **handler global** é o código no `interactionCreate` que vimos antes. Ele não depende de nenhuma mensagem em particular, só do customId. Por isso serve para painéis permanentes, como cargos por botão, tickets e verificação, que precisam continuar valendo dias depois e sobreviver a reinício do bot.

> **Dica**
>
> Regra prática: se o botão deve funcionar por segundos ou minutos e só para uma pessoa, use collector. Se o botão fica fixo em um canal para qualquer um clicar, use o handler global e coloque no customId os dados de que ele precisa, como no exemplo `cargo:ID`.

Esse é o padrão usado, por exemplo, em um [sistema de tickets de suporte](https://streethosting.com.br/guias/bots-discord/bot-discord-sistema-tickets-suporte), em que o botão de abrir ticket tem que funcionar para sempre.

## Desabilitar botões depois do uso

Quando uma ação só pode acontecer uma vez, como aceitar um convite ou votar, desative os botões depois do clique. Assim ninguém clica duas vezes. Como o componente que chega na interação é de leitura, você recria cada botão com `ButtonBuilder.from` e liga o `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],
});
```

Se a mensagem tiver mais de uma linha, repita o processo para cada uma. Em mensagens com collector, faça o mesmo no evento `end`, para que botões sem coletor ativo não fiquem clicáveis e falhando.

1. Reconheça o clique com update, que já troca os botões por versões desativadas.
2. Registre a ação em arquivo ou banco e confira esse registro a cada clique, porque dois cliques muito rápidos podem chegar antes de os botões ficarem desativados.
3. Para ações que o usuário pode repetir, como cargos, deixe o botão ativo.

## Onde rodar na StreetHosting

Botões e menus só respondem enquanto o bot está ligado. Se ele cair, quem clicar vê a falha de interação. Na [hospedagem de bots da StreetHosting](https://streethosting.com.br/bots) o bot roda em um container Node.js com painel e fica no ar o tempo todo. O plano Discord Essential, por R$ 9,99/mês, com 1 GB de RAM, é suficiente para um bot de painéis e confirmações.

Como o handler global não depende da memória do processo, o bot pode reiniciar em uma atualização de código e os botões antigos continuam funcionando. Para subir e manter o processo sem queda, veja o guia de [deploy de bot Discord com PM2](https://streethosting.com.br/guias/bots-discord/deploy-bot-discord-nodejs-pm2). Se você quer coletar texto longo do usuário em vez de cliques, o próximo passo são os [modals e formulários](https://streethosting.com.br/guias/bots-discord/bot-discord-modals-formularios).

## Perguntas frequentes

### Por que aparece Esta interação falhou depois de clicar no botão?

Porque o bot não respondeu em até 3 segundos, ou porque nenhum código está escutando aquele customId. Responda com reply, update ou deferUpdate logo no começo do tratamento e confira se o customId do botão bate com o que o seu código compara.

### Quantos botões cabem em uma mensagem?

Cada linha de componentes aceita até 5 botões, e uma mensagem aceita até 5 linhas, então o máximo são 25 botões. Um menu de seleção ocupa uma linha inteira sozinho.

### Os botões continuam funcionando depois que o bot reinicia?

Funcionam se o tratamento estiver em um handler global no interactionCreate, porque o Discord entrega o clique ao bot mesmo em mensagens antigas. Com collector não funcionam, já que ele vive só na memória do processo e some no reinício.

### Qual a diferença entre deferReply e deferUpdate?

O deferReply avisa ao Discord que você vai responder com uma mensagem nova e mostra o aviso de que o bot está pensando. O deferUpdate apenas reconhece o clique e prepara para você editar a mensagem original depois, sem mostrar nada ao usuário.

### Quantas opções cabem em um menu de seleção?

Até 25 opções por menu. Se você precisa de mais que isso, divida em categorias com vários menus em mensagens diferentes, ou mostre as opções em páginas com botões de anterior e próximo.

## Guias relacionados

- [Como criar bot Discord com slash commands](https://streethosting.com.br/guias/bots-discord/criar-bot-discord-slash-commands.md)
- [Modals e formulários no bot Discord: candidatura para staff com discord.js](https://streethosting.com.br/guias/bots-discord/bot-discord-modals-formularios.md)
- [Embeds no Discord com discord.js: como montar mensagens formatadas](https://streethosting.com.br/guias/bots-discord/bot-discord-embeds-mensagens-formatadas.md)
- [Bot Discord com sistema de tickets para suporte](https://streethosting.com.br/guias/bots-discord/bot-discord-sistema-tickets-suporte.md)

## Produtos citados

- https://streethosting.com.br/bots

## Dados estruturados

```json
[
  {
    "@context": "https://schema.org",
    "@type": [
      "Article",
      "TechArticle"
    ],
    "headline": "Bot Discord com botões e menus: como tratar os cliques",
    "name": "Botões e menus de seleção no bot Discord com discord.js v14",
    "abstract": "Veja como montar ButtonBuilder e StringSelectMenuBuilder, ler o customId no interactionCreate, cumprir o prazo de 3 segundos, escolher entre collector e handler global e desabilitar botões usados.",
    "description": "Crie botões e menus de seleção no discord.js v14, trate cada clique no interactionCreate, responda dentro de 3 segundos e desative os botões depois do uso.",
    "datePublished": "2026-11-13",
    "dateModified": "2026-11-13",
    "author": {
      "@type": "Organization",
      "name": "Equipe StreetHosting"
    },
    "publisher": {
      "@type": "Organization",
      "name": "StreetHosting",
      "url": "https://streethosting.com.br"
    },
    "inLanguage": "pt-BR",
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://streethosting.com.br/guias/bots-discord/bot-discord-botoes-menus-interativos"
    }
  },
  {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "Por que aparece Esta interação falhou depois de clicar no botão?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Porque o bot não respondeu em até 3 segundos, ou porque nenhum código está escutando aquele customId. Responda com reply, update ou deferUpdate logo no começo do tratamento e confira se o customId do botão bate com o que o seu código compara."
        }
      },
      {
        "@type": "Question",
        "name": "Quantos botões cabem em uma mensagem?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Cada linha de componentes aceita até 5 botões, e uma mensagem aceita até 5 linhas, então o máximo são 25 botões. Um menu de seleção ocupa uma linha inteira sozinho."
        }
      },
      {
        "@type": "Question",
        "name": "Os botões continuam funcionando depois que o bot reinicia?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Funcionam se o tratamento estiver em um handler global no interactionCreate, porque o Discord entrega o clique ao bot mesmo em mensagens antigas. Com collector não funcionam, já que ele vive só na memória do processo e some no reinício."
        }
      },
      {
        "@type": "Question",
        "name": "Qual a diferença entre deferReply e deferUpdate?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "O deferReply avisa ao Discord que você vai responder com uma mensagem nova e mostra o aviso de que o bot está pensando. O deferUpdate apenas reconhece o clique e prepara para você editar a mensagem original depois, sem mostrar nada ao usuário."
        }
      },
      {
        "@type": "Question",
        "name": "Quantas opções cabem em um menu de seleção?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Até 25 opções por menu. Se você precisa de mais que isso, divida em categorias com vários menus em mensagens diferentes, ou mostre as opções em páginas com botões de anterior e próximo."
        }
      }
    ]
  }
]
```
