Een Discord-bot knop toevoegen verandert je berichten van blokken tekst in een echte interface: de gebruiker klikt, de bot reageert direct. Met discord.js maken knoppen, dropdown-menu's (string select) en de ActionRow-componenten die ze dragen interactieve flows mogelijk, zoals rolkiezers, paginering, bevestigingsvensters of supportticketpanelen. In deze gids bouw je de componenten vanaf nul, luister je na het verzenden correct naar klikken, en leer je de meest voorkomende fouten te vermijden.
Het componentenmodel: ActionRow, knop en select
In Discord worden interactieve elementen nooit rechtstreeks aan een bericht gekoppeld — ze staan in rijen (rows). Eén bericht kan maximaal 5 ActionRows bevatten. Elke rij draagt ofwel tot vijf knoppen, ofwel één keuzemenu; je kunt die twee niet in dezelfde rij mengen. In discord.js v14 bouw je deze structuren met builder-klassen:
ButtonBuilder— een klikbare knop met een stijl, label encustomId.StringSelectMenuBuilder— een dropdown met vaste opties.ActionRowBuilder— de container die componenten groepeert.
Knopstijlen komen uit de ButtonStyle-enum: Primary (blauw), Secondary (grijs), Success (groen), Danger (rood) en Link. Link-knoppen openen een URL en krijgen geen customId; de andere genereren interacties.
Een Discord-bot knop maken
Het onderstaande voorbeeld voegt twee knoppen toe aan een slash-commando. Het veld customId is het cruciale onderdeel: het is de unieke identifier waarmee je klikgebeurtenissen uit elkaar houdt, en hij mag tot 100 tekens lang zijn.
const {
ButtonBuilder,
ButtonStyle,
ActionRowBuilder,
} = require('discord.js');
const confirm = new ButtonBuilder()
.setCustomId('order_confirm')
.setLabel('Bevestigen')
.setStyle(ButtonStyle.Success);
const cancel = new ButtonBuilder()
.setCustomId('order_cancel')
.setLabel('Annuleren')
.setStyle(ButtonStyle.Danger);
const row = new ActionRowBuilder().addComponents(confirm, cancel);
await interaction.reply({
content: 'Wil je de bestelling bevestigen?',
components: [row],
});
Het bericht toont nu twee knoppen. Maar de knoppen zijn nog "dood": om iets te laten gebeuren bij een klik, moet je een aparte listener instellen.
Een string select-menu toevoegen
Een dropdown is overzichtelijker dan knoppen wanneer je de gebruiker meerdere opties wilt aanbieden. Elke optie heeft een zichtbaar label en een value die naar je terugkomt. Optioneel kun je meervoudige selectie inschakelen met setMinValues en setMaxValues.
const { StringSelectMenuBuilder } = require('discord.js');
const menu = new StringSelectMenuBuilder()
.setCustomId('role_select')
.setPlaceholder('Kies een rol')
.addOptions(
{ label: 'Developer', value: 'dev', emoji: '💻' },
{ label: 'Designer', value: 'design', emoji: '🎨' },
{ label: 'Community', value: 'community', emoji: '🤝' },
);
const menuRow = new ActionRowBuilder().addComponents(menu);
await interaction.reply({ components: [menuRow] });
Een keuzemenu vult op zichzelf een hele rij; je kunt er geen knop op dezelfde rij bij zetten. Wil je meerdere menu's of knopgroepen, maak dan aparte ActionRowBuilder-objecten en voeg ze allemaal toe aan de components-array.
Luisteren naar interacties: interactionCreate en collectors
Er zijn twee manieren om klikken op te vangen. Voor permanente, altijd-actieve knoppen gebruik je de hoofd-interactionCreate-gebeurtenis van de bot en vertak je op basis van de customId:
client.on('interactionCreate', async (interaction) => {
if (interaction.isButton()) {
if (interaction.customId === 'order_confirm') {
await interaction.reply({ content: 'Bestelling bevestigd ✅', ephemeral: true });
}
}
if (interaction.isStringSelectMenu() && interaction.customId === 'role_select') {
const choice = interaction.values[0];
await interaction.reply({ content: `Je keuze: ${choice}`, ephemeral: true });
}
});
Voor tijdelijke flows die aan een specifiek bericht zijn gekoppeld (paginering of een eenmalige bevestiging) is createMessageComponentCollector handiger. Een collector sluit na een ingestelde tijd en luistert alleen naar de componenten van dat bericht:
const reply = await interaction.fetchReply();
const collector = reply.createMessageComponentCollector({
time: 60_000,
});
collector.on('collect', async (i) => {
if (i.user.id !== interaction.user.id) {
return i.reply({ content: 'Dit menu is niet voor jou.', ephemeral: true });
}
await i.update({ content: `Keuze: ${i.customId}`, components: [] });
});
collector.on('end', () => {
reply.edit({ components: [] }).catch(() => {});
});
Reageer altijd op elke interactie
Discord verwacht binnen 3 seconden een reactie op een interactie; anders ziet de gebruiker de fout "Deze interactie is mislukt". Kun je niet zo snel antwoorden (een databasequery, een externe API-aanroep, enz.), roep dan eerst deferUpdate() of deferReply() aan en ga verder met editReply(). Onderscheid de reactiemethoden zo:
reply()— stuurt een gloednieuwe reactie.update()— bewerkt het originele bericht waarop de knop stond (ideaal om menu's te sluiten).deferUpdate()/deferReply()— een "aan het denken"-status die tijd wint voor lange bewerkingen.
De optie ephemeral: true toont de reactie alleen aan degene die klikte, waardoor het kanaal schoon blijft voor rolpanelen en foutmeldingen.
Veelgestelde vragen
Waarom is customId zo belangrijk?
Omdat de customId de enige manier is waarop je weet welke knop of welk menu is aangeklikt. Houd hem uniek en leesbaar; indien nodig kun je er data (een gebruikers- of bericht-id) in opnemen, zoals ticket_close_12345, en die bij de klik uitlezen.
Hoeveel knoppen kan een bericht hebben?
Tot 5 ActionRows per bericht en tot 5 knoppen per knoppenrij, dus je komt tot in totaal 25 knoppen. Een keuzemenu daarentegen vult een rij in zijn eentje.
Werken oude knoppen niet meer na een herstart van de bot?
Collectors zijn gekoppeld aan de draaiende bot, dus ze gaan verloren bij een herstart. Wil je permanente knoppen, gebruik dan een globale interactionCreate-listener met customId-gebaseerde routing in plaats van een collector; zo worden klikken nog steeds verwerkt telkens als de bot start.
Een interactieve Discord-bot nodig? Ik kan een nette, onderhoudbare opzet bouwen voor rolpanelen, ticketsystemen of aangepaste select-menu-flows. Neem contact op en laten we het over je project hebben.