Een discord modal is de schoonste manier om gestructureerde invoer van een gebruiker te verzamelen zonder een kanaal vol te gooien met heen-en-weer-berichten. De ticketformulieren, sollicitatiesystemen, feedbackboxen en "stuur een suggestie"-flows die je op drukke servers ziet, draaien bijna altijd op een modal: de gebruiker klikt op een knop of voert een commando uit, er opent een klein pop-upformulier midden in het scherm, hij vult de velden in en verstuurt. In deze gids loop ik stap voor stap door het bouwen van een modal met discord.js, het valideren van de velden en het verwerken van de verstuurde data.
Wat is een modal en wanneer gebruik je het
Een modal is Discords pop-upformulier-component. Het kan worden geopend als reactie op een interactie zoals een knop, een keuzemenu of een slash-commando. Een modal kan maximaal 5 tekstinvoervelden bevatten, en elk veld is ofwel één regel (Short) of meerdere regels (Paragraph).
- Goede toepassing: een ticket openen, een sollicitatieformulier, een bugmelding, of alles wat vrije tekst vereist zoals een naam of een reden.
- Slechte toepassing: ja/nee-vragen of een vaste set keuzes — een knop of een keuzemenu is daar het juiste gereedschap.
Een belangrijke beperking: je kunt een modal alleen openen als directe reactie op een gebruikersinteractie. Je kunt niet eerst een bericht sturen en daarna een modal openen; de aanroep interaction.showModal() moet het eerste antwoord op die interactie zijn.
Vereisten en installatie
Dit voorbeeld richt zich op discord.js v14. Zorg dat Node.js 18 of nieuwer geïnstalleerd is. In een lege map:
npm init -y
npm install discord.js
Ik ga ervan uit dat je je bot-applicatie al hebt aangemaakt in de Discord Developer Portal, het token hebt opgehaald en de bot met de applications.commands-scope op je server hebt uitgenodigd.
De modal bouwen
Een modal bestaat uit vier bouwstenen: ModalBuilder (het venster zelf), TextInputBuilder (elk veld), TextInputStyle (kort/lang) en een ActionRowBuilder die de velden omvat. Elk tekstveld gaat in zijn eigen action row.
const {
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ActionRowBuilder,
} = require('discord.js');
function buildSupportModal() {
const modal = new ModalBuilder()
.setCustomId('support_modal')
.setTitle('Supportverzoek');
const onderwerp = new TextInputBuilder()
.setCustomId('onderwerp')
.setLabel('Onderwerp')
.setStyle(TextInputStyle.Short)
.setMinLength(3)
.setMaxLength(80)
.setRequired(true);
const beschrijving = new TextInputBuilder()
.setCustomId('beschrijving')
.setLabel('Beschrijf je probleem')
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder('Geef zoveel mogelijk details...')
.setMaxLength(1000)
.setRequired(true);
modal.addComponents(
new ActionRowBuilder().addComponents(onderwerp),
new ActionRowBuilder().addComponents(beschrijving),
);
return modal;
}
De customId-waarden zijn hier cruciaal: zowel de modal als elk veld hebben een unieke customId nodig, want we lezen de verstuurde data straks precies met deze identifiers terug.
De modal openen
Laten we de modal openen vanuit een slash-commando. Wanneer de commando-interactie binnenkomt, roepen we showModal aan. Let op: als je een modal gaat tonen, roep dan niet eerst deferReply of reply aan — de modal moet het allereerste antwoord van de interactie zijn.
client.on('interactionCreate', async (interaction) => {
if (interaction.isChatInputCommand() && interaction.commandName === 'support') {
await interaction.showModal(buildSupportModal());
}
});
Je kunt dezelfde modal ook vanuit een knop openen; het enige verschil is dat je het interactietype controleert met interaction.isButton(). De rest van de logica blijft gelijk.
De inzending verwerken en valideren
Wanneer de gebruiker het formulier invult en verstuurt, komt er een nieuwe interactie binnen. We vangen die op met interaction.isModalSubmit(), onderscheiden welke modal het is via customId, en lezen de veldwaarden uit met fields.getTextInputValue().
client.on('interactionCreate', async (interaction) => {
if (!interaction.isModalSubmit()) return;
if (interaction.customId !== 'support_modal') return;
const onderwerp = interaction.fields.getTextInputValue('onderwerp').trim();
const beschrijving = interaction.fields.getTextInputValue('beschrijving').trim();
// Discord dwingt min/max length af; voeg toch je eigen controles toe
if (onderwerp.length < 3) {
return interaction.reply({
content: 'Het onderwerp lijkt te kort.',
ephemeral: true,
});
}
// Verwerk de data hier: opslaan in een DB, posten in een kanaal, ticket openen...
await interaction.reply({
content: `Je verzoek is ontvangen! Onderwerp: **${onderwerp}**`,
ephemeral: true,
});
});
Discord dwingt je setMinLength/setMaxLength- en setRequired-regels al af aan de clientkant, maar echte validatie — zoals het formaat van een e-mailadres of het bereik van een getal — is aan jou. Bij ongeldige invoer is antwoorden met ephemeral: true, zodat alleen de gebruiker de waarschuwing ziet, de schoonste aanpak.
De data zinvol gebruiken: ticketvoorbeeld
In de meeste projecten wordt de modaldata als embed in een logkanaal geplaatst of naar een database geschreven. Bijvoorbeeld het binnenkomende verzoek in een staffkanaal laten vallen:
const { EmbedBuilder } = require('discord.js');
const logKanaal = interaction.guild.channels.cache.get('LOG_KANAAL_ID');
const embed = new EmbedBuilder()
.setTitle('Nieuw supportverzoek')
.addFields(
{ name: 'Onderwerp', value: onderwerp },
{ name: 'Beschrijving', value: beschrijving },
{ name: 'Van', value: `${interaction.user}` },
)
.setTimestamp();
await logKanaal.send({ embeds: [embed] });
Vanaf hier kun je de flow uitbreiden zoals je wilt: knopacties toevoegen zoals "sluiten" of "oppakken", het verzoek bewaren in MySQL/SQLite, of het doorsturen naar een externe API.
Veelgemaakte fouten
- reply/defer aanroepen voordat je de modal toont: dit gooit "Interaction has already been acknowledged". De modal moet altijd het eerste antwoord zijn.
- customId-botsingen: verschillende modals dezelfde id geven breekt je submit-routing. Geef elke modal en elk veld een unieke id.
- De 3-secondenregel: je moet ook snel reageren op een modal-inzending. Voor lang werk roep je eerst
interaction.deferReply()aan en gebruik je daarnaeditReply. - De limiet van 5 velden overschrijden: Discord staat maximaal 5 velden in een modal toe; heb je er meer nodig, dan moet je de flow opsplitsen.
Veelgestelde vragen
Hoeveel velden kan een modal hebben?
Maximaal 5 tekstinvoervelden. Elk veld moet in zijn eigen ActionRowBuilder zitten en kan alleen een tekstveld zijn; je kunt geen knoppen of keuzemenu's in een modal gebruiken.
Kan ik een dropdown (keuzemenu) in een modal gebruiken?
Nee. Modals ondersteunen voorlopig alleen tekstvelden. Heb je een keuze nodig, toon dan eerst een keuzemenu en open de modal na de selectie, of verzamel de keuze in een aparte stap buiten de modal.
Waarom kan ik de verstuurde data niet uitlezen?
Meestal is de id die je aan getTextInputValue() meegeeft niet identiek aan de setCustomId()-waarde van het veld. Zorg dat beide exact overeenkomen.
Wil je een professionele Discord-bot voor je server? Ik bouw ticketsystemen, sollicitatieformulieren en modal-gebaseerde flows op een schone, onderhoudbare manier. Neem contact op om over je project te praten.