Discord bots can do more than just respond to text commands. Buttons and select menus let users interact with your bot through clickable UI elements. These components replace older reaction-based systems and provide a cleaner user experience. This article explains how to set up and use buttons and select menus with discord.js v14.
Key Takeaways: Building Interactive Discord Bot Components
- MessageComponentInteraction class: Handles all button and select menu clicks in your bot code.
- ActionRowBuilder: Required container that holds up to five buttons or one select menu per row.
- ButtonBuilder and StringSelectMenuBuilder: Create the actual clickable elements with custom IDs and labels.
What Are Discord Message Components?
Message components are interactive UI elements that appear below a bot’s message in a Discord channel. Buttons are clickable boxes that trigger an action. Select menus are dropdown lists where users pick one or more options. Both require a custom ID string that your bot uses to identify which component was clicked.
Before writing code, you need a working discord.js v14 bot. Your bot must have the Send Messages and Use External Emojis permissions in the server. You also need Node.js 16.9.0 or newer installed on your machine. The examples in this article use the discord.js package version 14.
Prerequisites for Using Components
Install discord.js v14 in your project folder:
npm install discord.js@14
Create a basic bot file with the Gateway Intent GatewayIntentBits.Guilds and GatewayIntentBits.MessageContent. You also need the partial Partials.Message and Partials.Channel if your bot handles DMs.
Creating and Sending a Button
Buttons are built with ButtonBuilder and placed inside an ActionRowBuilder. Each button needs a custom ID, a label, and a style. The style controls the button color: Primary is blue, Secondary is gray, Success is green, Danger is red, and Link opens a URL.
- Import the required builders
Add these imports at the top of your file:const { ButtonBuilder, ButtonStyle, ActionRowBuilder } = require('discord.js'); - Create a button instance
Usenew ButtonBuilder()and chain the methods.setCustomId('my_button'),.setLabel('Click Me'), and.setStyle(ButtonStyle.Primary). - Wrap the button in an action row
Create a newActionRowBuilderand add the button with.addComponents(button). - Send the message with the component
Useinteraction.reply({ components: [row] })orchannel.send({ components: [row] })to display the button.
Example code for a slash command that sends a button:
const { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('button')
.setDescription('Sends a test button'),
async execute(interaction) {
const button = new ButtonBuilder()
.setCustomId('primary_button')
.setLabel('Click Me')
.setStyle(ButtonStyle.Primary);
const row = new ActionRowBuilder().addComponents(button);
await interaction.reply({ content: 'Here is your button:', components: [row] });
}
};
Handling Button Interactions
When a user clicks a button, Discord sends an interaction event. Your bot listens for this event using the client.on('interactionCreate') handler. Check if the interaction is a ButtonInteraction by testing interaction.isButton().
- Create an interaction listener
In your main bot file, addclient.on('interactionCreate', async interaction => { ... }); - Check the interaction type
Inside the listener, writeif (!interaction.isButton()) return;to ignore non-button interactions. - Match the custom ID
Useif (interaction.customId === 'primary_button')to run specific code for that button. - Reply or update the message
Callinteraction.reply('Button clicked!')orinteraction.update({ content: 'Updated message' })to respond.
Example handler for the button above:
client.on('interactionCreate', async interaction => {
if (!interaction.isButton()) return;
if (interaction.customId === 'primary_button') {
await interaction.reply({ content: 'You clicked the button!', ephemeral: true });
}
});
Creating and Sending a Select Menu
Select menus are built with StringSelectMenuBuilder. Each option has a label, a value, and an optional description. The select menu also needs a custom ID and a placeholder text that appears before the user makes a selection.
- Import StringSelectMenuBuilder
Addconst { StringSelectMenuBuilder, StringSelectMenuOptionBuilder } = require('discord.js'); - Create options
Usenew StringSelectMenuOptionBuilder().setLabel('Option 1').setValue('option1')for each option. - Build the select menu
Create aStringSelectMenuBuilderand chain.setCustomId('menu'),.setPlaceholder('Choose an option'), and.addOptions(option1, option2). - Wrap in an action row
Create anActionRowBuilderwith.addComponents(selectMenu). An action row can hold only one select menu. - Send the message
Include the row incomponentswhen replying or sending a message.
Example slash command that sends a select menu:
const { SlashCommandBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ActionRowBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('menu')
.setDescription('Sends a select menu'),
async execute(interaction) {
const option1 = new StringSelectMenuOptionBuilder()
.setLabel('Red')
.setValue('red');
const option2 = new StringSelectMenuOptionBuilder()
.setLabel('Blue')
.setValue('blue');
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('color_menu')
.setPlaceholder('Choose a color')
.addOptions(option1, option2);
const row = new ActionRowBuilder().addComponents(selectMenu);
await interaction.reply({ content: 'Pick your favorite color:', components: [row] });
}
};
Handling Select Menu Interactions
Select menu interactions are processed similarly to buttons. Check interaction.isStringSelectMenu() and read the selected values from interaction.values, which returns an array of strings.
- Add a select menu handler
In yourinteractionCreatelistener, addif (interaction.isStringSelectMenu()) { ... }. - Get the selected value
Useconst selected = interaction.values[0];to get the first chosen option. - Respond based on the selection
Use conditional logic to reply with different messages depending on the value.
Example handler for the color menu:
client.on('interactionCreate', async interaction => {
if (!interaction.isStringSelectMenu()) return;
if (interaction.customId === 'color_menu') {
const selectedColor = interaction.values[0];
await interaction.reply({ content: `You chose ${selectedColor}`, ephemeral: true });
}
});
Common Mistakes and Limitations
Action Row Limit Exceeded
A single message can have at most five action rows. Each row can hold up to five buttons or one select menu. If you add more rows, Discord returns an error. Count your rows before sending.
Custom ID Not Unique
If two buttons or menus in the same message share the same custom ID, the bot cannot tell them apart. Always use unique custom IDs. For dynamic components, append a unique identifier like a user ID or a timestamp.
Interaction Already Acknowledged
You can reply to an interaction only once. If you need to send multiple follow-up messages, use interaction.followUp() after the initial reply. Calling interaction.reply() twice throws an error.
Select Menu Values Must Be Unique
Each option in a select menu must have a unique value string. Duplicate values cause the interaction to fail silently. Always check your option values for duplicates before sending the menu.
Buttons vs Select Menus: When to Use Each
| Item | Buttons | Select Menus |
|---|---|---|
| Number of choices | Up to 5 per row, 25 per message | Up to 25 options per menu |
| User action | Single click | Click then select from dropdown |
| Best for | Binary choices, confirmations, navigation | Multiple options, lists, settings |
| Visual clarity | Immediate, all options visible | Compact, hidden until opened |
Use buttons when you have few options and want users to act quickly. Use select menus when you have many options or need to save space in a message. You can combine both in a single message by using multiple action rows.
You now know how to create buttons and select menus with discord.js v14. Start by adding a simple button to a test command, then expand to multi-step interactions. For advanced use, try collecting multiple inputs by sending a sequence of components. Disable buttons after use by setting .setDisabled(true) on the button builder to prevent double-clicks.