Buttons, Selects, and Modals (2025)

Design interactive bots with components and robust handler patterns

← Back to Blog

Buttons

const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const row = new ActionRowBuilder().addComponents(
  new ButtonBuilder().setCustomId('approve').setLabel('Approve').setStyle(ButtonStyle.Success),
  new ButtonBuilder().setCustomId('reject').setLabel('Reject').setStyle(ButtonStyle.Danger)
);
await interaction.reply({ content: 'Choose:', components: [row] });

Select Menus

const { StringSelectMenuBuilder, ActionRowBuilder } = require('discord.js');
const select = new StringSelectMenuBuilder()
  .setCustomId('picker')
  .setPlaceholder('Pick an option')
  .addOptions(
    { label: 'A', value: 'a' },
    { label: 'B', value: 'b' }
  );
await interaction.reply({ components: [new ActionRowBuilder().addComponents(select)] });

Modals

const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } = require('discord.js');
const modal = new ModalBuilder().setCustomId('feedback').setTitle('Feedback');
const input = new TextInputBuilder().setCustomId('message').setLabel('Your message').setStyle(TextInputStyle.Paragraph);
modal.addComponents(new ActionRowBuilder().addComponents(input));
await interaction.showModal(modal);

Best Practices

  • Namespace customId values (e.g., order:approve:123)
  • Expire components after use; handle stale interactions
  • Use ephemeral replies for sensitive actions

Timing Rules and Deferrals

You must acknowledge an interaction within 3 seconds by replying or deferring; after that, the token expires. Once acknowledged, you have up to 15 minutes to send or edit the response and follow‑ups. For operations that may exceed 3 seconds (API calls, DB work), defer immediately, then edit the reply when ready.

// discord.js (v14+)
client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  try {
    await interaction.deferReply({ ephemeral: true }); // within 3s
    const data = await slowOperation();
    await interaction.editReply({ content: `Done: ${data}` }); // within 15 min
  } catch (err) {
    if (interaction.deferred || interaction.replied) {
      await interaction.followUp({ content: 'Error occurred.', ephemeral: true });
    } else {
      await interaction.reply({ content: 'Error occurred.', ephemeral: true });
    }
  }
});