---
title: "Modals e formulários no bot Discord: candidatura para staff com discord.js"
description: "Crie um formulário de candidatura com ModalBuilder e TextInputBuilder no discord.js v14, leia as respostas e mande para a staff aprovar ou recusar com botões."
url: "https://streethosting.com.br/guias/bots-discord/bot-discord-modals-formularios"
category: "bots-discord"
slug: "bot-discord-modals-formularios"
datePublished: "2026-11-13"
dateModified: "2026-11-13"
author: "Equipe StreetHosting"
difficulty: "intermediario"
language: "pt-BR"
keywords:
  - "bot discord modal formulário discord.js"
  - "discord.js ModalBuilder"
  - "discord.js TextInputBuilder"
  - "showModal discord.js"
  - "formulário de candidatura discord"
  - "fields.getTextInputValue"
---

# Bot Discord com modal: formulário de candidatura para staff

Aprenda a abrir um modal com showModal, montar campos com TextInputBuilder, ler as respostas com fields.getTextInputValue e enviar tudo para um canal da staff com botões de aprovar e recusar.

> **Resposta rápida**
>
> Monte o formulário com `ModalBuilder` e campos `TextInputBuilder`, cada um em uma linha, e abra com `interaction.showModal()` como resposta a um comando ou botão. Quando o usuário enviar, trate `isModalSubmit()`, leia os valores com `fields.getTextInputValue` e mande o resultado para um canal da staff com botões de aprovar e recusar. O limite é de 5 campos por modal.

## O que é um modal e quais os limites

Um modal é uma janela de formulário que o Discord abre por cima da tela, com campos de texto para a pessoa preencher. É a forma certa de coletar texto longo, como uma candidatura, uma sugestão ou um relatório de bug. Botões e menus sozinhos não conseguem isso, porque só oferecem opções prontas.

Os limites que importam para quem vai montar o formulário:

- Até 5 campos de texto por modal, cada um em uma linha própria.
- Título do modal com até 45 caracteres.
- Rótulo de cada campo com até 45 caracteres.
- Cada campo aceita até 4000 caracteres, e você pode definir mínimo e máximo.
- O modal só abre como resposta a um comando ou a um componente, nunca por conta própria.

Os exemplos usam discord.js v14 e Node 20 ou mais novo. Se você ainda não conhece o tratamento de cliques no evento `interactionCreate`, leia antes o guia de [botões e menus interativos](https://streethosting.com.br/guias/bots-discord/bot-discord-botoes-menus-interativos), porque o formulário usa o mesmo caminho.

## Montar o formulário

O modal precisa de um **customId** e de um título. Cada campo é um `TextInputBuilder` com customId, rótulo e estilo. O estilo `Short` é uma linha só, bom para idade ou nome. O estilo `Paragraph` é uma caixa grande, para texto longo. Como cada campo precisa ficar dentro de um `ActionRowBuilder`, vale reunir tudo em uma função.

```
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)
    );
}
```

O `setMaxLength(1000)` nos campos longos não é por acaso. Depois a resposta vai para um campo de embed, que aceita até 1024 caracteres. Se o limite do campo de texto fosse 4000, uma resposta grande quebraria o envio para a staff.

## Abrir com showModal

A regra que mais gera erro: o modal só abre como **primeira resposta** a uma interação. Isso vale para slash command, clique de botão, escolha em menu e comando de contexto. Se você já chamou `reply` ou `deferReply` na mesma interação, ou se passaram 3 segundos, o Discord recusa. Por isso o `showModal` vem antes de qualquer consulta a banco ou API.

Duas formas comuns de abrir. A primeira, por comando:

```
// 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());
  },
};
```

A segunda, por um botão fixo em um canal de recrutamento, tratado no `interactionCreate`:

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

O botão fixo costuma ser melhor para recrutamento, porque a pessoa vê o convite no canal e clica sem precisar conhecer o comando.

## Ler as respostas enviadas

Quando o usuário clica em enviar, o Discord manda uma interação do tipo envio de modal. Você a reconhece com `isModalSubmit()`, confere o customId e lê cada campo pelo customId que você deu a ele.

```
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
}
```

O envio do modal também conta como interação, então ele tem os mesmos 3 segundos para a primeira resposta. Para qualquer coisa que demore, use `deferReply` primeiro.

## Mandar para a staff com aprovar e recusar

Agora o resultado vai para um canal privado da staff, em um embed, junto com dois botões. O detalhe esperto é o customId dos botões: ele leva a decisão e o ID de quem se candidatou, como `cand:aprovar:ID_DO_USUARIO`. Assim o bot não precisa guardar nada para saber de quem é a candidatura quando a staff clicar, mesmo depois de um reinício.

```
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.');
```

> **Dica**
>
> Deixe o canal da staff visível só para quem decide. Os botões ficam em uma mensagem pública dentro desse canal, então a segurança de verdade vem de quem consegue ver o canal e da checagem de permissão no código, que vem a seguir.

## Tratar a decisão da staff

O clique em Aprovar ou Recusar cai no handler global de botões. O código confere se quem clicou tem permissão, dá o cargo se for aprovado, avisa o candidato por DM e troca o embed para mostrar a decisão, removendo os botões para ninguém decidir duas vezes.

```
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: [] });
}
```

Para o bot dar o cargo, ele precisa da permissão de gerenciar cargos e o cargo dele tem que estar acima do cargo de staff na lista. Se o candidato já saiu do servidor, o `fetch` falha e o código segue sem ele, só registrando a decisão. Sobre a DM, o guia de [DM automática no Discord](https://streethosting.com.br/guias/bots-discord/bot-discord-enviar-mensagem-dm-automatica) explica por que ela pode falhar e como tratar.

## Validação e formulários maiores

O modal valida só tamanho mínimo, tamanho máximo e se o campo é obrigatório. Tudo além disso é com você. Algumas práticas que evitam dor de cabeça:

1. Valide o formato no código, como a idade numérica do exemplo, e responda com mensagem que só quem enviou vê.
2. Limite o abuso: guarde quem já se candidatou e bloqueie envio repetido em pouco tempo.
3. Use rótulos curtos e placeholders claros, já que o rótulo tem só 45 caracteres.
4. Confira o tamanho total do embed. Ele aceita no máximo 6000 caracteres somando título e campos, então 3 respostas de 1000 caracteres ainda cabem com folga.

> **Atenção**
>
> O limite é de 5 campos por modal. Se o seu formulário precisa de mais, divida em etapas: ao receber o primeiro modal, responda com uma mensagem que tenha o botão Continuar, e o clique nele abre o segundo modal. Guarde as respostas da primeira etapa até a segunda chegar, em memória por pouco tempo ou em banco de dados.

Para guardar as candidaturas de forma durável, por exemplo para consultar depois, veja o guia de [conectar o bot Discord a um banco MongoDB](https://streethosting.com.br/guias/bots-discord/conectar-bot-discord-banco-mongodb).

## Onde rodar na StreetHosting

Formulários só funcionam com o bot no ar, já que o modal e a resposta da staff dependem dele. Na [hospedagem de bots da StreetHosting](https://streethosting.com.br/bots) o bot roda em um container Node.js com painel, ligado o tempo todo. O plano Discord Essential, por R$ 9,99/mês, com 1 GB de RAM e um banco MariaDB incluso, atende bem um bot de candidaturas, e o banco ajuda se você quiser guardar o histórico.

Cadastre o token do bot, o ID do canal da staff e o ID do cargo como variáveis de ambiente pelo painel, nunca no código. Se quiser entender melhor as opções de hospedagem, leia [hospedagem de bot Discord 24/7](https://streethosting.com.br/guias/bots-discord/hospedagem-bot-discord-24-7).

## Perguntas frequentes

### Posso abrir um modal a qualquer momento, sem o usuário fazer nada?

Não. O showModal só funciona como primeira resposta a uma interação, ou seja, quando o usuário usou um slash command, clicou em um botão ou escolheu uma opção de menu. Não dá para abrir modal por evento do servidor, por mensagem comum nem por agendamento.

### Quantos campos um modal aceita?

Até 5 campos de texto. Cada campo de texto fica em uma linha própria. O título do modal tem no máximo 45 caracteres e cada campo de texto aceita até 4000 caracteres.

### Como pego o que o usuário digitou?

No evento interactionCreate, confira interaction.isModalSubmit() e o customId do modal, e leia cada campo com interaction.fields.getTextInputValue('id_do_campo'), usando o customId que você deu ao campo.

### Por que o bot mostra que o modal falhou ao abrir?

O motivo mais comum é passar de 3 segundos antes de chamar showModal, por exemplo por consultar um banco antes. Chame showModal logo no começo. Outro motivo é já ter respondido ou usado defer na mesma interação, pois depois disso o modal não abre mais.

### Dá para abrir um segundo modal a partir de um modal?

Não direto. Quando o usuário envia o primeiro modal, você responde com uma mensagem que tenha um botão, e o clique nesse botão abre o segundo modal. É assim que se faz formulários com mais de 5 campos.

## Guias relacionados

- [Botões e menus de seleção no bot Discord com discord.js v14](https://streethosting.com.br/guias/bots-discord/bot-discord-botoes-menus-interativos.md)
- [Como criar bot Discord com slash commands](https://streethosting.com.br/guias/bots-discord/criar-bot-discord-slash-commands.md)
- [Bot Discord com sistema de tickets para suporte](https://streethosting.com.br/guias/bots-discord/bot-discord-sistema-tickets-suporte.md)
- [Embeds no Discord com discord.js: como montar mensagens formatadas](https://streethosting.com.br/guias/bots-discord/bot-discord-embeds-mensagens-formatadas.md)
- [Como fazer o bot Discord enviar DM automática para membros](https://streethosting.com.br/guias/bots-discord/bot-discord-enviar-mensagem-dm-automatica.md)

## Produtos citados

- https://streethosting.com.br/bots

## Dados estruturados

```json
[
  {
    "@context": "https://schema.org",
    "@type": [
      "Article",
      "TechArticle"
    ],
    "headline": "Bot Discord com modal: formulário de candidatura para staff",
    "name": "Modals e formulários no bot Discord: candidatura para staff com discord.js",
    "abstract": "Aprenda a abrir um modal com showModal, montar campos com TextInputBuilder, ler as respostas com fields.getTextInputValue e enviar tudo para um canal da staff com botões de aprovar e recusar.",
    "description": "Crie um formulário de candidatura com ModalBuilder e TextInputBuilder no discord.js v14, leia as respostas e mande para a staff aprovar ou recusar com botões.",
    "datePublished": "2026-11-13",
    "dateModified": "2026-11-13",
    "author": {
      "@type": "Organization",
      "name": "Equipe StreetHosting"
    },
    "publisher": {
      "@type": "Organization",
      "name": "StreetHosting",
      "url": "https://streethosting.com.br"
    },
    "inLanguage": "pt-BR",
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://streethosting.com.br/guias/bots-discord/bot-discord-modals-formularios"
    }
  },
  {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "Posso abrir um modal a qualquer momento, sem o usuário fazer nada?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Não. O showModal só funciona como primeira resposta a uma interação, ou seja, quando o usuário usou um slash command, clicou em um botão ou escolheu uma opção de menu. Não dá para abrir modal por evento do servidor, por mensagem comum nem por agendamento."
        }
      },
      {
        "@type": "Question",
        "name": "Quantos campos um modal aceita?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Até 5 campos de texto. Cada campo de texto fica em uma linha própria. O título do modal tem no máximo 45 caracteres e cada campo de texto aceita até 4000 caracteres."
        }
      },
      {
        "@type": "Question",
        "name": "Como pego o que o usuário digitou?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "No evento interactionCreate, confira interaction.isModalSubmit() e o customId do modal, e leia cada campo com interaction.fields.getTextInputValue('id_do_campo'), usando o customId que você deu ao campo."
        }
      },
      {
        "@type": "Question",
        "name": "Por que o bot mostra que o modal falhou ao abrir?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "O motivo mais comum é passar de 3 segundos antes de chamar showModal, por exemplo por consultar um banco antes. Chame showModal logo no começo. Outro motivo é já ter respondido ou usado defer na mesma interação, pois depois disso o modal não abre mais."
        }
      },
      {
        "@type": "Question",
        "name": "Dá para abrir um segundo modal a partir de um modal?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Não direto. Quando o usuário envia o primeiro modal, você responde com uma mensagem que tenha um botão, e o clique nesse botão abre o segundo modal. É assim que se faz formulários com mais de 5 campos."
        }
      }
    ]
  }
]
```
