Modals
Modals are pop-up forms that allow you to prompt users for additional input. This form-like interaction response blocks the user from interacting with Discord until the modal is submitted or dismissed. In this section, we will cover how to create, show, and receive modals using discord.js!
This page is a follow-up to the interactions (slash commands) page. Reading that page first will help you understand the concepts introduced in this page.
Building and responding with modals
With the ModalBuilder class, discord.js offers a convenient way to build modals step by step using setters and callbacks.
You can have a maximum of five top-level components per modal, each of which can be a label or a text display component.
const { Events, ModalBuilder } = require('discord.js');
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
// TODO: Add components to modal...
}
});The customId is a developer-defined string of up to 100 characters and uniquely identifies this modal instance. You
can use it to differentiate incoming interactions.
The next step is adding components to the modal. Modal components represent input fields and come in different types depending on which information you want from the user.
Label
Label components wrap around other modal components (text input, select menus, etc.) to add a label and description to it. Since labels are not stand-alone components, we will use this example label to wrap a text input component in the next section:
const { LabelBuilder, ModalBuilder } = require('discord.js');
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
// Create the modal
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
const hobbiesLabel = new LabelBuilder()
// The label is a large header text that identifies the interactive component for the user.
.setLabel('What are some of your favorite hobbies?')
// The description is optional small text beneath the label that provides additional information about the interactive component.
.setDescription('Activities you like to participate in');
// Add label to the modal
modal.addLabelComponents(hobbiesLabel);
}
});The label field has a max length of 45 characters. The description field has a max length of 100 characters.
Text input
Text input components prompt users for single or multi line free-form text.
const { LabelBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js');
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
// Create the modal
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
const hobbiesInput = new TextInputBuilder()
.setCustomId('hobbiesInput')
// Short means a single line of text.
.setStyle(TextInputStyle.Short)
// Placeholder text displayed inside the text input box
.setPlaceholder('card games, films, books, etc.');
const hobbiesLabel = new LabelBuilder()
// The label is a large header that identifies the interactive component for the user.
.setLabel("What's some of your favorite hobbies?")
// The description is optional small text beneath the label that provides additional information about the interactive component.
.setDescription('Activities you like to participate in')
// Set text input as the component of the label
.setTextInputComponent(hobbiesInput);
// Add the label to the modal
modal.addLabelComponents(hobbiesLabel);
}
});Input styles
Discord offers two different input styles:
Short, a single-line text entryParagraph, a multi-line text entry
Input properties
A text input field can be customized in a number of ways to apply validation or set default values via the following TextInputBuilder methods:
const input = new TextInputBuilder()
// Set the component id (this is not the custom id)
.setId(1)
// Set the maximum number of characters allowed
.setMaxLength(1_000)
// Set the minimum number of characters required for submission
.setMinLength(10)
// Set a default value to prefill the text input
.setValue('Default')
// Require a value in this text input field (defaults to true)
.setRequired(true);The id field is used to differentiate components within interactions (which text input, selection, etc.). In
contrast, the customId covered earlier identifies the interaction (which modal, command, etc.).
Select menu
Select menus allow you to limit user input to a preselected list of values. Discord also offers select menus linked directly to native Discord entities like users, roles, and channels. Since they behave very similarly to how they do in messages, please refer to the corresponding guide page for more information on configuring select menus.
Here again, you wrap the select menu with a label component to add context to the selection and add the label to the modal:
// ...
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
// Create the modal
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
// ...
const favoriteStarterSelect = new StringSelectMenuBuilder()
.setCustomId('starter')
.setPlaceholder('Make a selection!')
// Modal only property on select menus to prevent submission, defaults to true
.setRequired(true)
.addOptions(
// String select menu options
new StringSelectMenuOptionBuilder()
// Label displayed to user
.setLabel('Bulbasaur')
// Description of option
.setDescription('The dual-type Grass/Poison Seed Pokémon.')
// Value returned to you in modal submission
.setValue('bulbasaur'),
new StringSelectMenuOptionBuilder()
.setLabel('Charmander')
.setDescription('The Fire-type Lizard Pokémon.')
.setValue('charmander'),
new StringSelectMenuOptionBuilder()
.setLabel('Squirtle')
.setDescription('The Water-type Tiny Turtle Pokémon.')
.setValue('squirtle'),
);
// ...
const favoriteStarterLabel = new LabelBuilder()
.setLabel("What's your favorite Gen 1 Pokémon starter?")
// Set string select menu as component of the label
.setStringSelectMenuComponent(favoriteStarterSelect);
// Add labels to modal
modal.addLabelComponents(hobbiesLabel);
modal.addLabelComponents(hobbiesLabel, favoriteStarterLabel);
}
});Text display
Text display components offer you a way to give additional context to the user that doesn't fit into labels or isn't directly connected to any specific input field.
// ...
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
// Create the modal
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
// ...
const text = new TextDisplayBuilder().setContent(
'Text that could not fit in to a label or description\n-# Markdown can also be used',
);
// Add components to modal
modal
.addLabelComponents(hobbiesLabel, favoriteStarterLabel);
.addLabelComponents(hobbiesLabel, favoriteStarterLabel)
.addTextDisplayComponents(text);
}
});File upload
File upload components allow you to prompt the user to upload a file from their system.
Discord does not send the file data itself in the resulting interaction. You will have to download it from Discords CDN to process and validate it. Do not execute arbitrary code people upload via your app!
// ...
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
// Create the modal
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
// ...
const pictureOfTheWeekUpload = new FileUploadBuilder().setCustomId('picture');
// ...
const pictureOfTheWeekLabel = new LabelBuilder()
.setLabel('Picture of the Week')
.setDescription('The best pictures you have taken this week')
// Set file upload as component of the label
.setFileUploadComponent(pictureOfTheWeekUpload);
// Add components to modal
modal
.addLabelComponents(hobbiesLabel, favoriteStarterLabel)
.addTextDisplayComponents(text);
.addTextDisplayComponents(text)
.addLabelComponents(pictureOfTheWeekLabel);
}
});File upload properties
A file upload component can be customized to apply validation via the following FileUploadBuilder methods:
const pictureOfTheWeekUpload = new FileUploadBuilder()
// Set the optional identifier for component
.setId(1)
// Minimum number of items that must be uploaded (defaults to 1); min 0, max 10
.setMinValues(1)
// Maximum number of items that can be uploaded (defaults to 1); max 10
.setMaxValues(1)
// Require a value in this file upload component (defaults to true)
.setRequired(true);The id field is used to differentiate components within interactions (which text input, selection, etc.).
In contrast, the customId covered earlier identifies the interaction (which modal, command, etc.).
You cannot limit and validate the file size or the file extension.
Responding with a modal
With the modal built, call ChatInputCommandInteraction#showModal() to send the interaction response to Discord and display the modal to the user.
Showing a modal must be the first response to an interaction. You cannot defer modals.
// ...
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
// Create the modal
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
// ...
// Add components to modal
modal
.addLabelComponents(hobbiesLabel, favoriteStarterLabel)
.addTextDisplayComponents(text)
.addLabelComponents(pictureOfTheWeekLabel);
// Show modal to the user
await interaction.showModal(modal);
}
});Restart your bot and invoke the /ping command again. You should see the modal as shown below:

Receiving modal submissions
Interaction collectors
Modal submissions can be collected within the scope of the interaction that sent the modal by utilizing an InteractionCollector, or the ChatInputCommandInteraction#awaitModalSubmit promisified version. These both provide instances of the ModalSubmitInteraction class as collected items.
For a detailed guide on handling interactions with collectors, please refer to the collectors guide.
The interactionCreate event
To receive a ModalSubmitInteraction event, attach an Client#interactionCreate event listener to your client and use the BaseInteraction#isModalSubmit type guard to make sure you only receive modals:
client.on(Events.InteractionCreate, (interaction) => {
// ...
if (!interaction.isModalSubmit()) return;
console.log(interaction);
});Responding to modal submissions
The ModalSubmitInteraction class provides the same methods as the ChatInputCommandInteraction class. These methods behave equally:
reply()editReply()deferReply()fetchReply()deleteReply()followUp()
If the modal was prompted through a button or select menu interaction, these methods may be used to update the underlying message:
update()deferUpdate()
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isModalSubmit()) return;
console.log(interaction);
if (interaction.customId === 'myModal') {
await interaction.reply({ content: 'Your submission was received successfully!' });
}
});If you're using TypeScript, you can use the ModalSubmitInteraction#isFromMessage() type guard to make sure the
received interaction originated from a MessageComponentInteraction.
Extracting data from modal submissions
You can process the submitted input fields through the use of convenience getters on ModalSubmitInteraction#fields. The library provides getters for all modal components with user submitted data:
client.on(Events.InteractionCreate, (interaction) => {
if (!interaction.isModalSubmit()) return;
if (interaction.customId === 'myModal') {
await interaction.reply({ content: 'Your submission was received successfully!' });
// Get the data entered by the user
const hobbies = interaction.fields.getTextInputValue('hobbiesInput');
const starter = interaction.fields.getStringSelectValues('starter');
const picture = interaction.fields.getUploadedFiles('picture');
console.log({ hobbies, starter, picture });
}
});Empty text input submissions return an empty string "". Select menus without a selection return an empty array [].