Component Interactions¶
Component interactions allow you to create interactive Discord messages with buttons, select menus, and other UI elements that users can interact with.
Overview¶
@axrxvm/betterdiscordjs provides built-in support for Discord's message components through the Context (Ctx) object, making it easy to create interactive experiences.
Button Components¶
Creating Buttons¶
// Simple button
const button = ctx.button('Click Me', { style: 'primary' });
// Button with custom ID
const button = ctx.button('Delete', {
customId: 'delete_msg',
style: 'danger'
});
// Button with handler
const button = ctx.button('Confirm', { style: 'success' }, async (interaction) => {
await interaction.reply('Confirmed!');
});
Button Styles¶
primary
(blue)secondary
(gray)success
(green)danger
(red)link
(for URLs)
Button Examples¶
bot.command('poll', async (ctx) => {
const question = ctx.args.join(' ');
const yesButton = ctx.button('đ Yes', {
customId: 'poll_yes',
style: 'success'
});
const noButton = ctx.button('đ No', {
customId: 'poll_no',
style: 'danger'
});
const row = ctx.buttonRow([
{ customId: 'poll_yes', label: 'đ Yes', style: 1 },
{ customId: 'poll_no', label: 'đ No', style: 4 }
]);
const embed = ctx.embed()
.title('đ Poll')
.desc(question)
.color('blue');
const msg = await ctx.reply({
embeds: [embed.embed],
components: [row]
});
// Handle button interactions
await ctx.awaitButton(msg, {
poll_yes: async (interaction) => {
await interaction.reply({ content: 'You voted Yes!', ephemeral: true });
},
poll_no: async (interaction) => {
await interaction.reply({ content: 'You voted No!', ephemeral: true });
}
}, { time: 300000 }); // 5 minutes
});
Confirmation Dialogs¶
bot.command('delete', async (ctx) => {
const confirmButton = ctx.button('â
Confirm', {
customId: 'confirm_delete',
style: 'danger'
});
const cancelButton = ctx.button('â Cancel', {
customId: 'cancel_delete',
style: 'secondary'
});
const row = ctx.buttonRow([
{ customId: 'confirm_delete', label: 'â
Confirm', style: 4 },
{ customId: 'cancel_delete', label: 'â Cancel', style: 2 }
]);
const msg = await ctx.reply({
content: 'â ī¸ Are you sure you want to delete this?',
components: [row]
});
await ctx.awaitButton(msg, {
confirm_delete: async (interaction) => {
await interaction.update({
content: 'â
Deleted successfully!',
components: []
});
// Perform deletion logic here
},
cancel_delete: async (interaction) => {
await interaction.update({
content: 'â Cancelled.',
components: []
});
}
});
});
Select Menu Components¶
Creating Select Menus¶
const options = ['Option 1', 'Option 2', 'Option 3'];
const menu = ctx.menu(options, async (interaction) => {
const selected = interaction.values[0];
await interaction.reply(`You selected: ${selected}`);
});
Advanced Select Menu¶
bot.command('role', async (ctx) => {
const { StringSelectMenuBuilder, ActionRowBuilder } = require('discord.js');
const selectMenu = new StringSelectMenuBuilder()
.setCustomId('role_select')
.setPlaceholder('Choose your role')
.addOptions([
{
label: 'Developer',
description: 'For developers and programmers',
value: 'developer',
emoji: 'đģ'
},
{
label: 'Designer',
description: 'For UI/UX designers',
value: 'designer',
emoji: 'đ¨'
},
{
label: 'Manager',
description: 'For project managers',
value: 'manager',
emoji: 'đ'
}
]);
const row = new ActionRowBuilder().addComponents(selectMenu);
const msg = await ctx.reply({
content: 'Select your role:',
components: [row]
});
const collector = msg.createMessageComponentCollector({
componentType: 3, // SELECT_MENU
time: 60000
});
collector.on('collect', async (interaction) => {
const roleValue = interaction.values[0];
const roleMap = {
developer: 'DEVELOPER_ROLE_ID',
designer: 'DESIGNER_ROLE_ID',
manager: 'MANAGER_ROLE_ID'
};
const roleId = roleMap[roleValue];
const role = ctx.guild.roles.cache.get(roleId);
if (role) {
await interaction.member.roles.add(role);
await interaction.reply({
content: `â
Added ${role.name} role!`,
ephemeral: true
});
}
});
});
Multi-Step Interactions¶
Wizard-Style Interface¶
bot.command('setup', async (ctx) => {
let step = 1;
let config = {};
const nextButton = ctx.button('Next âĄī¸', {
customId: 'next',
style: 'primary'
});
const prevButton = ctx.button('âŦ
ī¸ Previous', {
customId: 'prev',
style: 'secondary'
});
const finishButton = ctx.button('â
Finish', {
customId: 'finish',
style: 'success'
});
const updateMessage = async (interaction) => {
let content, components;
switch (step) {
case 1:
content = 'Step 1: Choose your prefix';
components = [ctx.buttonRow([
{ customId: 'prefix_!', label: '!', style: 2 },
{ customId: 'prefix_?', label: '?', style: 2 },
{ customId: 'prefix_/', label: '/', style: 2 },
{ customId: 'next', label: 'Next âĄī¸', style: 1 }
])];
break;
case 2:
content = `Step 2: Choose welcome channel\nPrefix: ${config.prefix}`;
// Add channel selection logic
components = [ctx.buttonRow([
{ customId: 'prev', label: 'âŦ
ī¸ Previous', style: 2 },
{ customId: 'next', label: 'Next âĄī¸', style: 1 }
])];
break;
case 3:
content = `Step 3: Review settings\nPrefix: ${config.prefix}\nWelcome Channel: ${config.channel}`;
components = [ctx.buttonRow([
{ customId: 'prev', label: 'âŦ
ī¸ Previous', style: 2 },
{ customId: 'finish', label: 'â
Finish', style: 3 }
])];
break;
}
await interaction.update({ content, components });
};
const msg = await ctx.reply({
content: 'Step 1: Choose your prefix',
components: [ctx.buttonRow([
{ customId: 'prefix_!', label: '!', style: 2 },
{ customId: 'prefix_?', label: '?', style: 2 },
{ customId: 'prefix_/', label: '/', style: 2 }
])]
});
await ctx.awaitButton(msg, {
'prefix_!': async (i) => { config.prefix = '!'; step = 2; await updateMessage(i); },
'prefix_?': async (i) => { config.prefix = '?'; step = 2; await updateMessage(i); },
'prefix_/': async (i) => { config.prefix = '/'; step = 2; await updateMessage(i); },
next: async (i) => { step++; await updateMessage(i); },
prev: async (i) => { step--; await updateMessage(i); },
finish: async (i) => {
await i.update({
content: 'â
Setup completed!',
components: []
});
// Save configuration
}
}, { time: 300000 });
});
Pagination with Components¶
Advanced Paginator¶
bot.command('help', async (ctx) => {
const commands = Array.from(bot.commands.values());
const itemsPerPage = 5;
const pages = [];
// Create pages
for (let i = 0; i < commands.length; i += itemsPerPage) {
const pageCommands = commands.slice(i, i + itemsPerPage);
const embed = ctx.embed()
.title('đ Command Help')
.desc(pageCommands.map(cmd =>
`**${cmd.name}** - ${cmd.description}`
).join('\n'))
.footer(`Page ${Math.floor(i / itemsPerPage) + 1}/${Math.ceil(commands.length / itemsPerPage)}`)
.color('blue');
pages.push(embed.embed);
}
let currentPage = 0;
const components = [ctx.buttonRow([
{ customId: 'first', label: 'âŽī¸', style: 2 },
{ customId: 'prev', label: 'âŦ
ī¸', style: 2 },
{ customId: 'next', label: 'âĄī¸', style: 2 },
{ customId: 'last', label: 'âī¸', style: 2 },
{ customId: 'stop', label: 'âšī¸', style: 4 }
])];
const msg = await ctx.reply({
embeds: [pages[currentPage]],
components
});
await ctx.awaitButton(msg, {
first: async (i) => {
currentPage = 0;
await i.update({ embeds: [pages[currentPage]], components });
},
prev: async (i) => {
currentPage = Math.max(0, currentPage - 1);
await i.update({ embeds: [pages[currentPage]], components });
},
next: async (i) => {
currentPage = Math.min(pages.length - 1, currentPage + 1);
await i.update({ embeds: [pages[currentPage]], components });
},
last: async (i) => {
currentPage = pages.length - 1;
await i.update({ embeds: [pages[currentPage]], components });
},
stop: async (i) => {
await i.update({ embeds: [pages[currentPage]], components: [] });
}
}, { time: 300000 });
});
Dynamic Components¶
Context-Aware Buttons¶
bot.command('music', async (ctx) => {
const isPlaying = getMusicStatus(); // Your music logic
const playButton = ctx.button(
isPlaying ? 'â¸ī¸ Pause' : 'âļī¸ Play',
{
customId: 'toggle_play',
style: isPlaying ? 'secondary' : 'success'
}
);
const components = [ctx.buttonRow([
{ customId: 'toggle_play', label: isPlaying ? 'â¸ī¸ Pause' : 'âļī¸ Play', style: isPlaying ? 2 : 3 },
{ customId: 'skip', label: 'âī¸ Skip', style: 2 },
{ customId: 'stop', label: 'âšī¸ Stop', style: 4 }
])];
const embed = ctx.embed()
.title('đĩ Music Player')
.desc(isPlaying ? 'Currently playing...' : 'Music paused')
.color(isPlaying ? 'green' : 'yellow');
const msg = await ctx.reply({
embeds: [embed.embed],
components
});
await ctx.awaitButton(msg, {
toggle_play: async (i) => {
// Toggle music and update message
const newStatus = toggleMusic();
const newEmbed = ctx.embed()
.title('đĩ Music Player')
.desc(newStatus ? 'Currently playing...' : 'Music paused')
.color(newStatus ? 'green' : 'yellow');
const newComponents = [ctx.buttonRow([
{ customId: 'toggle_play', label: newStatus ? 'â¸ī¸ Pause' : 'âļī¸ Play', style: newStatus ? 2 : 3 },
{ customId: 'skip', label: 'âī¸ Skip', style: 2 },
{ customId: 'stop', label: 'âšī¸ Stop', style: 4 }
])];
await i.update({ embeds: [newEmbed.embed], components: newComponents });
},
skip: async (i) => {
skipTrack();
await i.reply({ content: 'âī¸ Skipped track!', ephemeral: true });
},
stop: async (i) => {
stopMusic();
await i.update({
content: 'âšī¸ Music stopped.',
embeds: [],
components: []
});
}
});
});
Best Practices¶
-
Always handle timeouts
await ctx.awaitButton(msg, handlers, { time: 60000 });
-
Provide visual feedback
await interaction.deferUpdate(); // Show loading state // Perform action await interaction.editReply({ content: 'Done!' });
-
Clean up components when done
await interaction.update({ content: 'Completed!', components: [] // Remove buttons });
-
Use ephemeral responses for user-specific actions
await interaction.reply({ content: 'Action completed!', ephemeral: true });
-
Validate user permissions
if (!interaction.member.permissions.has('MANAGE_MESSAGES')) { return interaction.reply({ content: 'â No permission!', ephemeral: true }); }
-
Handle component expiration gracefully
collector.on('end', async () => { await msg.edit({ content: 'âŗ This interaction has expired.', components: [] }); });
Error Handling¶
bot.command('interactive', async (ctx) => {
try {
const msg = await ctx.reply({
content: 'Click a button!',
components: [/* buttons */]
});
await ctx.awaitButton(msg, {
action: async (interaction) => {
try {
await performAction();
await interaction.reply('Success!');
} catch (error) {
console.error('Action failed:', error);
await interaction.reply({
content: 'â Action failed!',
ephemeral: true
});
}
}
});
} catch (error) {
console.error('Component interaction failed:', error);
await ctx.error('Failed to create interactive message.');
}
});
Next Steps¶
Create more interactive experiences:
- đ¨ Embed Builder - Combine components with rich embeds
- đ Modals & Forms - Collect detailed user input
- đ Pagination - Handle large datasets interactively
- đ§ Middleware & Hooks - Add component interaction processing