Un discord modal est la façon la plus propre de collecter une saisie structurée auprès d'un utilisateur sans inonder un salon de messages aller-retour. Les formulaires de tickets, les systèmes de candidature, les boîtes de retour et les flux « envoyer une suggestion » que tu vois sur les serveurs actifs reposent presque toujours sur un modal : l'utilisateur clique sur un bouton ou lance une commande, un petit formulaire en pop-up s'ouvre au centre de l'écran, il remplit les champs et valide. Dans ce guide, je détaille étape par étape la création d'un modal avec discord.js, la validation des champs et le traitement des données envoyées.
Qu'est-ce qu'un modal et quand l'utiliser
Un modal est le composant formulaire pop-up de Discord. Il peut être ouvert en réponse à une interaction comme un bouton, un menu déroulant ou une commande slash. Un modal peut contenir au maximum 5 champs de saisie texte, et chaque champ est soit sur une seule ligne (Short), soit sur plusieurs lignes (Paragraph).
- Bon usage : ouvrir un ticket, un formulaire de candidature, un rapport de bug, ou tout ce qui demande du texte libre comme un nom ou une raison.
- Mauvais usage : les questions oui/non ou un ensemble fixe de choix — un bouton ou un menu déroulant est alors l'outil adapté.
Une contrainte importante : tu ne peux ouvrir un modal qu'en réponse directe à une interaction utilisateur. Tu ne peux pas envoyer d'abord un message puis ouvrir un modal ; l'appel interaction.showModal() doit être la première réponse à cette interaction.
Prérequis et installation
Cet exemple cible discord.js v14. Assure-toi d'avoir Node.js 18 ou plus récent. Dans un dossier vierge :
npm init -y
npm install discord.js
Je suppose que tu as déjà créé ton application bot dans le Discord Developer Portal, récupéré son token et invité le bot sur ton serveur avec le scope applications.commands.
Construire le modal
Un modal se compose de quatre briques : ModalBuilder (la fenêtre elle-même), TextInputBuilder (chaque champ), TextInputStyle (court/long) et un ActionRowBuilder qui entoure les champs. Chaque champ texte va dans sa propre action row.
const {
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ActionRowBuilder,
} = require('discord.js');
function buildSupportModal() {
const modal = new ModalBuilder()
.setCustomId('support_modal')
.setTitle('Demande de support');
const sujet = new TextInputBuilder()
.setCustomId('sujet')
.setLabel('Sujet')
.setStyle(TextInputStyle.Short)
.setMinLength(3)
.setMaxLength(80)
.setRequired(true);
const description = new TextInputBuilder()
.setCustomId('description')
.setLabel('Décris ton problème')
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder('Donne autant de détails que possible...')
.setMaxLength(1000)
.setRequired(true);
modal.addComponents(
new ActionRowBuilder().addComponents(sujet),
new ActionRowBuilder().addComponents(description),
);
return modal;
}
Les valeurs customId sont ici essentielles : le modal et chaque champ doivent avoir un customId unique, car nous relirons les données envoyées exactement avec ces identifiants.
Ouvrir le modal
Ouvrons le modal depuis une commande slash. Quand l'interaction de la commande arrive, on appelle showModal. Attention : si tu vas afficher un modal, n'appelle pas deferReply ni reply avant — le modal doit être la toute première réponse de l'interaction.
client.on('interactionCreate', async (interaction) => {
if (interaction.isChatInputCommand() && interaction.commandName === 'support') {
await interaction.showModal(buildSupportModal());
}
});
Tu peux ouvrir ce même modal depuis un bouton ; la seule différence est que tu vérifies le type d'interaction avec interaction.isButton(). Le reste de la logique reste identique.
Traiter et valider la soumission
Quand l'utilisateur remplit le formulaire et valide, une nouvelle interaction arrive. On la capte avec interaction.isModalSubmit(), on distingue le modal via customId et on lit les valeurs des champs avec fields.getTextInputValue().
client.on('interactionCreate', async (interaction) => {
if (!interaction.isModalSubmit()) return;
if (interaction.customId !== 'support_modal') return;
const sujet = interaction.fields.getTextInputValue('sujet').trim();
const description = interaction.fields.getTextInputValue('description').trim();
// Discord impose min/max length ; ajoute quand même tes propres contrôles
if (sujet.length < 3) {
return interaction.reply({
content: 'Le sujet semble trop court.',
ephemeral: true,
});
}
// Traite les données ici : enregistrer en BDD, poster dans un salon, ouvrir un ticket...
await interaction.reply({
content: `Ta demande a été reçue ! Sujet : **${sujet}**`,
ephemeral: true,
});
});
Discord applique déjà tes règles setMinLength/setMaxLength et setRequired côté client, mais la vraie validation — par exemple le format d'un e-mail ou la plage d'un nombre — t'appartient. Pour une saisie invalide, répondre avec ephemeral: true pour que seul l'utilisateur voie l'avertissement est l'approche la plus propre.
Exploiter les données : exemple de ticket
Dans la plupart des projets, les données du modal sont postées dans un salon de logs sous forme d'embed ou écrites en base de données. Par exemple, déposer la demande entrante dans un salon réservé au staff :
const { EmbedBuilder } = require('discord.js');
const salonLogs = interaction.guild.channels.cache.get('ID_SALON_LOGS');
const embed = new EmbedBuilder()
.setTitle('Nouvelle demande de support')
.addFields(
{ name: 'Sujet', value: sujet },
{ name: 'Description', value: description },
{ name: 'De', value: `${interaction.user}` },
)
.setTimestamp();
await salonLogs.send({ embeds: [embed] });
À partir de là, tu peux étendre le flux comme tu veux : ajouter des actions de bouton comme « fermer » ou « prendre en charge », persister la demande dans MySQL/SQLite, ou la transmettre à une API externe.
Erreurs fréquentes
- Appeler reply/defer avant d'afficher le modal : cela lève « Interaction has already been acknowledged ». Le modal doit toujours être la première réponse.
- Collisions de customId : donner le même id à des modals différents casse le routage de tes soumissions. Donne un id unique à chaque modal et chaque champ.
- La règle des 3 secondes : tu dois aussi répondre vite à une soumission de modal. Pour un traitement long, appelle d'abord
interaction.deferReply(), puis utiliseeditReply. - Dépasser la limite de 5 champs : Discord autorise au maximum 5 champs dans un modal ; s'il t'en faut plus, tu dois découper le flux.
Questions fréquentes
Combien de champs un modal peut-il avoir ?
Au maximum 5 champs de saisie texte. Chaque champ doit se trouver dans son propre ActionRowBuilder et ne peut être qu'un champ texte ; tu ne peux pas utiliser de boutons ou de menus déroulants dans un modal.
Puis-je utiliser un menu déroulant dans un modal ?
Non. Les modals ne prennent en charge que les champs texte pour le moment. S'il te faut un choix, affiche d'abord un menu déroulant et ouvre le modal après la sélection, ou récupère le choix dans une étape distincte en dehors du modal.
Pourquoi je n'arrive pas à lire les données envoyées ?
En général, l'id passé à getTextInputValue() n'est pas identique à la valeur setCustomId() du champ. Vérifie que les deux correspondent exactement.
Tu veux un bot Discord professionnel pour ton serveur ? Je construis des systèmes de tickets, des formulaires de candidature et des flux basés sur les modals de façon propre et maintenable. Contacte-moi pour parler de ton projet.