feat(guide): updated modal page for label (#11169)

* feat(guide): updated modal page for label

* feat(guide): updated modal page with modal component sections

* feat(guide): added modal to select menu page

* fix(guide): moved select menu and add text display callout

* feat(guide): reworked to show modal together instead of separately

* feat(guide): added file upload  to modal

* fix: code block focus in modals

* fix: grammar

* fix: grammar and wording

* feat: structure and readability

General improvements to structure and reading flow
* Reword sentence structure
* Use semantic code highlights
* Consistent heading casing
* Add additional explanations and elaborations
* Adapt legacy examples in text
* Omit repeated code for brevity and less scrolling

* fix: updated example Image

* chore: typos, grammar, rephrasing

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Ryan Munro <monbrey@gmail.com>

* chore: review rephrasing

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Ryan Munro <monbrey@gmail.com>

* chore: rephrasing for correctness

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

* chore: rephrasing for correctness

* chore: rephrasing for correctness

* chore: formatting

* chore: rephrasing for correctness

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

* chore: update text

---------

Co-authored-by: almostSouji <timoqueezle@gmail.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Ryan Munro <monbrey@gmail.com>
This commit is contained in:
William Bowen
2025-11-28 06:33:26 -05:00
committed by GitHub
parent 02fc101069
commit b2cbdf1567
5 changed files with 329 additions and 85 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -2,30 +2,27 @@
title: Modals
---
With modals you can create pop-up forms that allow users to provide you with formatted inputs through submissions. We'll cover how to create, show, and receive modal forms using discord.js!
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!
<Callout>
This page is a follow-up to the [interactions (slash commands) page](../slash-commands/advanced-creation). Please
carefully read that section first, so that you can understand the methods used in this section.
This page is a follow-up to the [interactions (slash commands) page](../slash-commands/advanced-creation). Reading
that page first will help you understand the concepts introduced in this page.
</Callout>
## Building and responding with modals
Unlike message components, modals aren't strictly components themselves. They're a callback structure used to respond to interactions.
With the `ModalBuilder` class, discord.js offers a convenient way to build modals step by step using setters and callbacks.
<Callout>
You can have a maximum of five `ActionRowBuilder`s per modal builder, and one `TextInputBuilder` within an
`ActionRowBuilder`. Currently, you can only use `TextInputBuilder`s in modal action rows builders.
You can have a maximum of five top-level components per modal, each of which can be a label or a text display
component.
</Callout>
To create a modal you construct a new `ModalBuilder`. You can then use the setters to add the custom id and title.
```js
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');
@@ -35,105 +32,321 @@ client.on(Events.InteractionCreate, async (interaction) => {
```
<Callout>
The custom id is a developer-defined string of up to 100 characters. Use this field to ensure you can uniquely define
all incoming interactions from your modals!
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.
</Callout>
The next step is to add the input fields in which users responding can enter free-text. Adding inputs is similar to adding components to messages.
The next step is adding components to the modal, which may either request input or present information.
At the end, we then call `ChatInputCommandInteraction#showModal` to display the modal to the user.
### Label
<Callout type="warn">
If you're using typescript you'll need to specify the type of components your action row holds. This can be done by specifying the generic parameter in `ActionRowBuilder`:
```diff
- new ActionRowBuilder()
+ new ActionRowBuilder<ModalActionRowComponentBuilder>()
```
</Callout>
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:
```js
const { ActionRowBuilder, Events, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js');
const { LabelBuilder, ModalBuilder } = require('discord.js');
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
// [!code focus:11]
// Create the modal
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
// Add components to modal
// [!code ++:5]
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 an additional optional subtext that aids the label.
.setDescription('Activities you like to participate in');
// Create the text input components
const favoriteColorInput = new TextInputBuilder()
.setCustomId('favoriteColorInput')
// The label is the prompt the user sees for this input
.setLabel("What's your favorite color?")
// Short means only a single line of text
.setStyle(TextInputStyle.Short);
const hobbiesInput = new TextInputBuilder()
.setCustomId('hobbiesInput')
.setLabel("What's some of your favorite hobbies?")
// Paragraph means multiple lines of text.
.setStyle(TextInputStyle.Paragraph);
// An action row only holds one text input,
// so you need one action row per text input.
const firstActionRow = new ActionRowBuilder().addComponents(favoriteColorInput);
const secondActionRow = new ActionRowBuilder().addComponents(hobbiesInput);
// Add inputs to the modal
modal.addComponents(firstActionRow, secondActionRow);
// Show the modal to the user
await interaction.showModal(modal); // [!code word:showModal]
// [!code ++:2]
// Add label to the modal
modal.addLabelComponents(hobbiesLabel);
}
});
```
Restart your bot and invoke the `/ping` command again. You should see a popup form resembling the image below:
![Modal Example](./images/modal-example.png)
<Callout type="warn">
Showing a modal must be the first response to an interaction. You cannot `defer()` or `deferUpdate()` then show a
modal later.
<Callout>
The `label` field has a max length of 45 characters. The `description` field has a max length of 100 characters.
</Callout>
### Input styles
### Text input
Currently there are two different input styles available:
Text input components prompt users for single or multi line free-form text.
- `Short`, a single-line text entry;
- `Paragraph`, a multi-line text entry similar to the HTML `<textarea>`;
```js
const { LabelBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js');
### Input properties
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'ping') {
// [!code focus:10]
// Create the modal
const modal = new ModalBuilder().setCustomId('myModal').setTitle('My Modal');
In addition to the `customId`, `label` and `style`, a text input can be customised in a number of ways to apply validation, prompt the user, or set default values via the `TextInputBuilder` methods:
// [!code ++:6]
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.');
// [!code focus:10]
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 an additional optional subtext that aids the label.
.setDescription('Activities you like to participate in')
// [!code ++:2]
// 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 entry
- `Paragraph`, 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:
```js
const input = new TextInputBuilder()
// set the maximum number of characters to allow
// 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
// Set the minimum number of characters required for submission
.setMinLength(10)
// set a placeholder string to prompt the user
.setPlaceholder('Enter some text!')
// set a default value to pre-fill the input
// Set a default value to prefill the text input
.setValue('Default')
// require a value in this input field
// Require a value in this text input field (defaults to true)
.setRequired(true);
```
<Callout>
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.).
</Callout>
### 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](../interactive-components/select-menus) 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:
```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');
// ...
// [!code focus:24]
// [!code ++:23]
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'),
);
// ...
// [!code focus:4]
// [!code ++:4]
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);
// [!code focus:3]
// Add labels to modal
modal.addLabelComponents(hobbiesLabel); // [!code --]
modal.addLabelComponents(hobbiesLabel, favoriteStarterLabel); // [!code ++]
}
});
```
### 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.
```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');
// ...
// [!code focus:3]
// [!code ++:3]
const text = new TextDisplayBuilder().setContent(
'Text that could not fit in to a label or description\n-# Markdown can also be used',
);
// [!code focus:5]
// Add components to modal
modal
// [!code --]
.addLabelComponents(hobbiesLabel, favoriteStarterLabel);
// [!code ++:2]
.addLabelComponents(hobbiesLabel, favoriteStarterLabel)
.addTextDisplayComponents(text);
}
});
```
### File upload
File upload components allow you to prompt the user to upload a file from their system.
<Callout type="warn">
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!
</Callout>
```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');
// ...
// [!code focus:2]
// [!code ++]
const pictureOfTheWeekUpload = new FileUploadBuilder().setCustomId('picture');
// ...
// [!code focus:12]
// [!code ++:5]
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)
// [!code --]
.addTextDisplayComponents(text);
// [!code ++:2]
.addTextDisplayComponents(text)
.addLabelComponents(pictureOfTheWeekLabel);
}
});
```
#### File upload properties
A file upload component can be customized to apply validation via the following `FileUploadBuilder` methods:
```js
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);
```
<Callout>
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**.
</Callout>
### 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.
<Callout type="warn">
Showing a modal must be the first response to an interaction. You **cannot** defer modals.
</Callout>
```js
// ...
// [!code focus:5]
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');
// ...
// [!code focus:9]
// Add components to modal
modal
.addLabelComponents(hobbiesLabel, favoriteStarterLabel)
.addTextDisplayComponents(text)
.addLabelComponents(pictureOfTheWeekLabel);
// [!code ++:2]
// 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:
![Modal Example](./images/modal-example.png)
## Receiving modal submissions
### Interaction collectors
Modal submissions can be collected within the scope of the interaction that showed it by utilising an `InteractionCollector`, or the `ChatInputCommandInteraction#awaitModalSubmit` promisified method. These both provide instances of the `ModalSubmitInteraction` class as collected items.
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 receiving message components via collectors, please refer to the [collectors guide](../popular-topics/collectors#interaction-collectors).
For a detailed guide on handling interactions with collectors, please refer to the [collectors guide](../popular-topics/collectors#interaction-collectors).
### The interactionCreate event
@@ -141,6 +354,8 @@ To receive a `ModalSubmitInteraction` event, attach an `Client#interactionCreate
```js
client.on(Events.InteractionCreate, (interaction) => {
// ...
// [!code word:isModalSubmit] [!code highlight:2]
if (!interaction.isModalSubmit()) return;
console.log(interaction);
});
@@ -157,7 +372,7 @@ The `ModalSubmitInteraction` class provides the same methods as the `ChatInputCo
- `deleteReply()`
- `followUp()`
If the modal was shown from a `ButtonInteraction` or `StringSelectMenuInteraction`, it will also provide these methods, which behave equally:
If the modal was prompted through a button or select menu interaction, these methods may be used to update the underlying message:
- `update()`
- `deferUpdate()`
@@ -165,28 +380,40 @@ If the modal was shown from a `ButtonInteraction` or `StringSelectMenuInteractio
```js
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isModalSubmit()) return;
console.log(interaction);
if (interaction.customId === 'myModal') {
// [!code highlight] [!code word:reply]
await interaction.reply({ content: 'Your submission was received successfully!' });
}
});
```
<Callout>
If you're using typescript, you can use the `ModalSubmitInteraction#isFromMessage` typeguard, to make sure the
received interaction was from a `MessageComponentInteraction`.
If you're using TypeScript, you can use the `ModalSubmitInteraction#isFromMessage()` type guard to make sure the
received interaction originated from a `MessageComponentInteraction`.
</Callout>
## Extracting data from modal submissions
You'll most likely need to read the data sent by the user in the modal. You can do this by accessing the `ModalSubmitInteraction#fields`. From there you can call `ModalSubmitFields#getTextInputValue` with the custom id of the text input to get the value.
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:
```js
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 favoriteColor = interaction.fields.getTextInputValue('favoriteColorInput');
const hobbies = interaction.fields.getTextInputValue('hobbiesInput');
console.log({ favoriteColor, hobbies });
// [!code focus:6]
// 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 });
}
});
```
<Callout>
Empty text input submissions return an empty string `""`. Select menus without a selection return an empty array `[]`.
</Callout>

View File

@@ -2,13 +2,17 @@
title: Select Menus
---
Select menus are one of the `MessageComponent` classes, which can be sent via messages or interaction responses.
Select menus are interactive components which can be sent via messages, interaction responses, or in modals.
<Callout>
This page is a follow-up to the [slash commands](../slash-commands/advanced-creation) section and [action
rows](../interactive-components/action-rows) page. Please carefully read those pages first so that you can understand
the methods used here.
</Callout>
<Callout>
This page is for using select menus in messages. For using [select menus in
modals](../interactions/modals#select-menu) visit the modal page
</Callout>
## Building string select menus
@@ -22,13 +26,17 @@ const { StringSelectMenuBuilder, StringSelectMenuOptionBuilder, SlashCommandBuil
module.exports = {
// data: new SlashCommandBuilder()...
async execute(interaction) {
const select = new StringSelectMenuBuilder()
const favoriteStarterSelect = new StringSelectMenuBuilder()
.setCustomId('starter')
.setPlaceholder('Make a selection!')
.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 in select menu interaction
.setValue('bulbasaur'),
new StringSelectMenuOptionBuilder()
.setLabel('Charmander')
@@ -59,17 +67,21 @@ const {
StringSelectMenuOptionBuilder,
SlashCommandBuilder,
} = require('discord.js');
// [!code focus:30]
// [!code focus:33]
module.exports = {
// data: new SlashCommandBuilder()...
async execute(interaction) {
const select = new StringSelectMenuBuilder()
const favoriteStarterSelect = new StringSelectMenuBuilder()
.setCustomId('starter')
.setPlaceholder('Make a selection!')
.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 in select menu interaction
.setValue('bulbasaur'),
new StringSelectMenuOptionBuilder()
.setLabel('Charmander')
@@ -81,9 +93,11 @@ module.exports = {
.setValue('squirtle'),
);
// [!code ++:6]
const row = new ActionRowBuilder().addComponents(select);
// [!code ++:8]
// Adding a string select menu to an action row
const row = new ActionRowBuilder().addComponents(favoriteStarterSelect);
// Reply with the action row
await interaction.reply({
content: 'Choose your starter!',
components: [row],
@@ -99,7 +113,7 @@ module.exports = {
components](../popular-topics/display-components) system.
</Callout>
### String select menu options
## String select menu options
String select menu options are custom-defined by the user, as shown in the example above. At a minimum, each option must have it's `label` and `value` defined. The label is shown to the user, while the value is included in the interaction sent to the bot.

View File

@@ -32,6 +32,9 @@ Text Display components let you add markdown-formatted text to your message and
Sending user and role mentions in text display components **will notify users and roles**! You can and should control
mentions with the `allowedMentions` message option.
</Callout>
<Callout>
Text display components can be used in modals. See the [modal guide](../interactions/modals#text-display) for usage.
</Callout>
The example below shows how you can send a Text Display component in a channel.