Modals collect text input from users. Define handlers in src/components/modals/ and open them from a command or button with interaction.showModal().

The modal customId is set automatically from the file path — src/components/modals/feedback.ts becomes feedback, admin/edit.ts becomes admin.edit. Do not call .setCustomId() in the handler file.

Creating a modal handler

import { Modal } from "@djs-core/runtime";
import { MessageFlags, ActionRowBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
export default new Modal()
.setTitle("Give us feedback")
.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(
new TextInputBuilder()
.setCustomId("name-input")
.setLabel("Your name")
.setStyle(TextInputStyle.Short),
),
new ActionRowBuilder<TextInputBuilder>().addComponents(
new TextInputBuilder()
.setCustomId("feedback-input")
.setLabel("Your feedback")
.setStyle(TextInputStyle.Paragraph),
),
)
.run(async (interaction) => {
const name = interaction.fields.getTextInputValue("name-input");
const feedback = interaction.fields.getTextInputValue("feedback-input");
await interaction.reply({
content: `Thanks ${name}! Feedback: ${feedback}`,
flags: [MessageFlags.Ephemeral],
});
});

Opening a modal

Import the modal and pass it to showModal():

import feedbackModal from "../../components/modals/feedback";
import { Command } from "@djs-core/runtime";
export default new Command()
.setDescription("Open the feedback form")
.run(async (interaction) => {
await interaction.showModal(feedbackModal);
});

Passing data with .setData()

Use .withData<T>() on the modal and .setData() when opening it — same pattern as buttons and select menus:

import { Modal } from "@djs-core/runtime";
import { ActionRowBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
export default new Modal()
.withData<{ userId: string }>()
.setTitle("Edit nickname")
.addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents(
new TextInputBuilder()
.setCustomId("nickname-input")
.setLabel("New nickname")
.setStyle(TextInputStyle.Short),
),
)
.run(async (interaction, data) => {
const nickname = interaction.fields.getTextInputValue("nickname-input");
await interaction.reply(`Updated user ${data.userId} to ${nickname}`);
});
import editUserModal from "../../components/modals/edit-user";
await interaction.showModal(
editUserModal.setData({ userId: targetUser.id }),
);

Modal data is stored server-side with an optional TTL — same as buttons. Expired data replies with an ephemeral error automatically.

Key points

  • Handler path → src/components/modals/
  • interaction.fields.getTextInputValue(id) → read submitted text inputs
  • interaction.showModal(modal) → open from a command or button
  • .withData<T>() + .setData() → pass typed context into the submit handler