---
title: "Modals and forms in a Discord bot: a staff application with discord.js | StreetHosting"
description: "Build a staff application form with ModalBuilder and TextInputBuilder in discord.js v14 and send the answers to staff to approve or reject with buttons."
url: "https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms"
type: "page"
language: "en-US"
---

Discord Bots · 6 min · Intermediate

Published on Nov 13, 2026 · Updated on Nov 13, 2026

# Discord bot with a modal: staff application form

Learn to open a modal with showModal, build fields with TextInputBuilder, read the answers with fields.getTextInputValue and send everything to a staff channel with approve and reject buttons.

By [Equipe StreetHosting](https://streethosting.com.br/en/autores#equipe-streethosting) · StreetHosting infrastructure and support team

[Automation and webhooks](https://streethosting.com.br/en/guides/topics/automation)

Summarize with:

[](https://chat.openai.com/?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-modals-forms.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "ChatGPT") [](https://claude.ai/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-modals-forms.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Claude") [](https://www.google.com/search?udm=50&aep=11&q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-modals-forms.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Google AI Mode") [](https://x.com/i/grok?text=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-modals-forms.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Grok") [](https://www.perplexity.ai/search/new?q=Summarize%20the%20key%20points%20of%20this%20StreetHosting%20guide%3A%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-modals-forms.%20Highlight%20the%20step-by-step%20instructions%2C%20the%20prerequisites%20and%20the%20most%20common%20mistakes. "Perplexity")

Share:

[](https://x.com/intent/tweet?text=Modals%20and%20forms%20in%20a%20Discord%20bot%3A%20a%20staff%20application%20with%20discord.js&url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-modals-forms "Share on X") [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-modals-forms "Share on Facebook") [](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-modals-forms "Share on LinkedIn") [](https://wa.me/?text=Modals%20and%20forms%20in%20a%20Discord%20bot%3A%20a%20staff%20application%20with%20discord.js%20https%3A%2F%2Fstreethosting.com.br%2Fen%2Fguides%2Fdiscord-bots%2Fdiscord-bot-modals-forms "Share on WhatsApp")

For agents: Copy as Markdown [.md](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms.md)

In this guide 8 sections

* [What a modal is and its limits](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#o-que-e-modal)
* [Build the form](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#montar-modal)
* [Open it with showModal](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#show-modal)
* [Read the submitted answers](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#ler-respostas)
* [Send to staff with approve and reject](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#canal-staff)
* [Handle the staff decision](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#decisao)
* [Validation and bigger forms](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#cuidados)
* [Where to run it at StreetHosting](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#hospedar)

Quick answer

Build the form with `ModalBuilder` and `TextInputBuilder` fields, each in its own row, and open it with `interaction.showModal()` as the reply to a command or button. When the user submits, handle `isModalSubmit()`, read the values with `fields.getTextInputValue` and send the result to a staff channel with approve and reject buttons. The limit is 5 fields per modal.

## What a modal is and its limits[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#o-que-e-modal)

A modal is a form window that Discord opens over the screen, with text fields for the person to fill in. It is the right way to collect long text, like an application, a suggestion or a bug report. Buttons and menus alone cannot do that, because they only offer ready made options.

The limits that matter when you build the form:

* Up to 5 text fields per modal, each in its own row.
* Modal title with up to 45 characters.
* Label of each field with up to 45 characters.
* Each field takes up to 4000 characters, and you can set a minimum and a maximum.
* The modal only opens as a reply to a command or a component, never on its own.

The examples use discord.js v14 and Node 20 or newer. If you are not yet familiar with handling clicks in the `interactionCreate` event, first read the guide on [interactive buttons and menus](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus), because the form uses the same path.

## Build the form[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#montar-modal)

The modal needs a **customId** and a title. Each field is a `TextInputBuilder` with a customId, a label and a style. The `Short` style is a single line, good for age or name. The `Paragraph` style is a big box, for long text. Since each field has to sit inside an `ActionRowBuilder`, it is worth gathering everything in one function.

`const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, } = require('discord.js'); function montarModal() { const idade = new TextInputBuilder() .setCustomId('idade') .setLabel('Sua idade') .setStyle(TextInputStyle.Short) .setMaxLength(3) .setRequired(true); const experiencia = new TextInputBuilder() .setCustomId('experiencia') .setLabel('Experiência anterior com moderação') .setStyle(TextInputStyle.Paragraph) .setPlaceholder('Conte onde e por quanto tempo você moderou') .setMinLength(30) .setMaxLength(1000) .setRequired(true); const motivo = new TextInputBuilder() .setCustomId('motivo') .setLabel('Por que quer entrar na equipe?') .setStyle(TextInputStyle.Paragraph) .setMinLength(30) .setMaxLength(1000) .setRequired(true); return new ModalBuilder() .setCustomId('form_candidatura') .setTitle('Candidatura para staff') .addComponents( new ActionRowBuilder().addComponents(idade), new ActionRowBuilder().addComponents(experiencia), new ActionRowBuilder().addComponents(motivo) ); }`

The `setMaxLength(1000)` on the long fields is no accident. Later the answer goes into an embed field, which takes up to 1024 characters. If the text field limit were 4000, a long answer would break the send to staff.

## Open it with showModal[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#show-modal)

The rule that causes the most errors: the modal only opens as the **first reply** to an interaction. That holds for slash commands, button clicks, menu choices and context menu commands. If you already called `reply` or `deferReply` on the same interaction, or if 3 seconds went by, Discord refuses. That is why `showModal` comes before any database or API query.

Two common ways to open it. The first, by command:

`// comando /candidatar module.exports = { data: new SlashCommandBuilder() .setName('candidatar') .setDescription('Abre o formulário de candidatura para a staff'), async execute(interaction) { await interaction.showModal(montarModal()); }, };`

The second, through a fixed button in a recruitment channel, handled in `interactionCreate`:

`if (interaction.isButton() && interaction.customId === 'abrir_candidatura') { await interaction.showModal(montarModal()); }`

The fixed button is usually better for recruitment, because the person sees the invitation in the channel and clicks without having to know the command.

## Read the submitted answers[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#ler-respostas)

When the user clicks submit, Discord sends an interaction of the modal submit type. You recognize it with `isModalSubmit()`, check the customId and read each field by the customId you gave it.

`if (interaction.isModalSubmit() && interaction.customId === 'form_candidatura') { const idade = interaction.fields.getTextInputValue('idade'); const experiencia = interaction.fields.getTextInputValue('experiencia'); const motivo = interaction.fields.getTextInputValue('motivo'); // validação: o modal não garante que a idade seja um número if (!/^\d{1,3}$/.test(idade.trim())) { return interaction.reply({ content: 'A idade precisa ser um número. Abra o formulário de novo.', flags: MessageFlags.Ephemeral, }); } // continua na próxima seção }`

Submitting the modal also counts as an interaction, so it has the same 3 seconds for the first reply. For anything that takes long, use `deferReply` first.

## Send to staff with approve and reject[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#canal-staff)

Now the result goes to a private staff channel, in an embed, together with two buttons. The clever part is the buttons' customId: it carries the decision and the ID of whoever applied, like `cand:aprovar:ID_DO_USUARIO`. That way the bot does not need to store anything to know whose application it is when staff clicks, even after a restart.

`` const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageFlags, } = require('discord.js'); // buscar o canal e enviar pode passar de 3 segundos: reconheça antes await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const canalStaff = await client.channels.fetch(process.env.CANAL_STAFF_ID); const embed = new EmbedBuilder() .setColor(0xff6a00) .setTitle('Nova candidatura') .setThumbnail(interaction.user.displayAvatarURL()) .addFields( { name: 'Candidato', value: `<@${interaction.user.id}>` }, { name: 'Idade', value: idade }, { name: 'Experiência', value: experiencia }, { name: 'Motivo', value: motivo } ) .setTimestamp(); const botoes = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId(`cand:aprovar:${interaction.user.id}`) .setLabel('Aprovar') .setStyle(ButtonStyle.Success), new ButtonBuilder() .setCustomId(`cand:recusar:${interaction.user.id}`) .setLabel('Recusar') .setStyle(ButtonStyle.Danger) ); await canalStaff.send({ embeds: [embed], components: [botoes] }); await interaction.editReply('Candidatura enviada. A staff vai analisar e você recebe o resultado.'); ``

Make the staff channel visible only to the people who decide. The buttons sit in a public message inside that channel, so real security comes from who can see the channel and from the permission check in the code, which comes next.

## Handle the staff decision[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#decisao)

The click on Approve or Reject lands in the global button handler. The code checks whether the person who clicked has permission, gives the role if approved, notifies the applicant by DM and swaps the embed to show the decision, removing the buttons so nobody decides twice.

`` const { PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js'); if (interaction.isButton() && interaction.customId.startsWith('cand:')) { if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageRoles)) { return interaction.reply({ content: 'Só a staff pode decidir candidaturas.', flags: MessageFlags.Ephemeral, }); } const [, decisao, userId] = interaction.customId.split(':'); const aprovado = decisao === 'aprovar'; // dar o cargo e mandar DM podem demorar mais que 3 segundos await interaction.deferUpdate(); const candidato = await interaction.guild.members.fetch(userId).catch(() => null); if (candidato) { if (aprovado) await candidato.roles.add(process.env.CARGO_STAFF_ID); await candidato .send(aprovado ? 'Sua candidatura foi aprovada!' : 'Sua candidatura foi recusada desta vez.') .catch(() => {}); // DM fechada não pode derrubar o fluxo } const embedFinal = EmbedBuilder.from(interaction.message.embeds[0]) .setColor(aprovado ? 0x57f287 : 0xed4245) .addFields({ name: 'Decisão', value: `${aprovado ? 'Aprovada' : 'Recusada'} por ${interaction.user.username}`, }); await interaction.editReply({ embeds: [embedFinal], components: [] }); } ``

For the bot to hand out the role, it needs the permission to manage roles and its own role must sit above the staff role in the list. If the applicant already left the server, the `fetch` fails and the code carries on without them, only recording the decision. About the DM, the guide on [automatic DMs on Discord](https://streethosting.com.br/en/guides/discord-bots/discord-bot-send-automatic-dm) explains why it can fail and how to handle it.

## Validation and bigger forms[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#cuidados)

The modal validates only minimum length, maximum length and whether the field is required. Everything beyond that is up to you. A few practices that save headaches:

1. Validate the format in code, like the numeric age in the example, and reply with a message only the sender sees.
2. Limit abuse: store who already applied and block repeated submissions in a short time.
3. Use short labels and clear placeholders, since the label has only 45 characters.
4. Check the total embed size. It takes at most 6000 characters counting the title and fields, so 3 answers of 1000 characters still fit with room to spare.

The limit is 5 fields per modal. If your form needs more, split it into steps: on receiving the first modal, reply with a message that has a Continue button, and clicking it opens the second modal. Keep the answers from the first step until the second one arrives, in memory for a short time or in a database.

To store the applications durably, for example to look them up later, see the guide on [connecting a Discord bot to a MongoDB database](https://streethosting.com.br/en/guides/discord-bots/connect-discord-bot-to-database).

## Where to run it at StreetHosting[](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#hospedar)

Forms only work with the bot online, since the modal and the staff reply depend on it. On the [StreetHosting bot hosting](https://streethosting.com.br/en/bots) the bot runs in a Node.js container with a panel, on all the time. The Discord Essential plan, at R$ 9.99/month with 1 GB of RAM and one MariaDB database included, suits an application bot well, and the database helps if you want to keep a history.

Register the bot token, the staff channel ID and the role ID as environment variables in the panel, never in the code. If you want to understand the hosting options better, read [hosting a Discord bot 24/7](https://streethosting.com.br/en/guides/discord-bots/discord-bot-hosting-24-7).

In this guide

* [What a modal is and its limits](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#o-que-e-modal)
* [Build the form](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#montar-modal)
* [Open it with showModal](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#show-modal)
* [Read the submitted answers](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#ler-respostas)
* [Send to staff with approve and reject](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#canal-staff)
* [Handle the staff decision](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#decisao)
* [Validation and bigger forms](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#cuidados)
* [Where to run it at StreetHosting](https://streethosting.com.br/en/guides/discord-bots/discord-bot-modals-forms#hospedar)

## Frequently asked questions

Can I open a modal at any time, without the user doing anything?

No. showModal only works as the first response to an interaction, meaning when the user ran a slash command, clicked a button or picked a menu option. You cannot open a modal from a server event, a regular message or a schedule.

How many fields does a modal take?

Up to 5 text fields. Each text field sits in its own row. The modal title has at most 45 characters and each text field takes up to 4000 characters.

How do I get what the user typed?

In the interactionCreate event, check interaction.isModalSubmit() and the modal customId, and read each field with interaction.fields.getTextInputValue('field\_id'), using the customId you gave the field.

Why does the modal fail to open?

The most common reason is going past 3 seconds before calling showModal, for example by querying a database first. Call showModal right at the start. Another reason is having already replied or deferred the same interaction, because after that the modal no longer opens.

Can I open a second modal from a modal?

Not directly. When the user submits the first modal, you reply with a message that has a button, and clicking that button opens the second modal. That is how you build forms with more than 5 fields.

Next step

See bot plans

24/7 Discord bots with fast activation and support in Portuguese.

[See bot plans](https://streethosting.com.br/en/bots)

## Related guides

[Discord bots Intermediate Buttons and select menus in a Discord bot with discord.js v14 Learn to build ButtonBuilder and StringSelectMenuBuilder, read the customId in interactionCreate, meet the 3 second deadline, choose between a collector and a global handler and disable used buttons. 7 min Read guide](https://streethosting.com.br/en/guides/discord-bots/discord-bot-buttons-select-menus) [Discord bots Beginner How to create a Discord bot with slash commands Slash commands improve the user experience and make your bot easier to use. Here is a clear path to get started. 2 min Read guide](https://streethosting.com.br/en/guides/discord-bots/create-discord-bot-slash-commands) [Discord bots Intermediate Discord ticket bot for support: setup and hosting A badly built ticket system turns into spam in the general channel. A good one reduces churn and frees your staff for the cases that actually pay. This guide covers the flow, the bot and the host. 2 min Read guide](https://streethosting.com.br/en/guides/discord-bots/discord-ticket-bot)

[← Back to the Guide Center](https://streethosting.com.br/en/guides)
