Modal Commands

A modal command is a slash command that opens a Discord modal, a popup form with text inputs, instead of replying straight away.

Added in 2.7.0.


When to Use One

Slash command arguments are fine for one or two short values, but they are typed inline and everything is visible while you type. A modal gives you a form with up to five inputs, multi-line boxes, and placeholders, which suits feedback, bug reports, applications, and anything with a longer text body.


Creating One Interactively

sh
botbox add

Pick modal as the command type. Instead of arguments you'll be asked for fields, and for each field:

  • Name — the Python attribute name, no spaces or dashes, unique within the command
  • Label — what Discord shows above the input, at most 45 characters
  • Style — short for a single line, paragraph for a multi-line box
  • Required — whether Discord blocks submission when the input is empty
  • Placeholder — optional grey hint text

You can add up to five fields, which is Discord's limit for a single modal.


Creating One Headlessly

Set "Type": "modal" and give a Fields array instead of Args.

sh
botbox add Feedback --commands '[
  {
    "Name": "feedback",
    "Scope": "guild",
    "Type": "modal",
    "Description": "Collects user feedback",
    "Fields": [
      { "Name": "subject", "Label": "Subject", "Style": "short", "Required": true, "Placeholder": "Short summary" },
      { "Name": "details", "Label": "Details", "Style": "paragraph", "Required": false, "Placeholder": "" }
    ],
    "ReturnType": "None"
  }
]'

Field object:

KeyTypeDescription
NamestringPython attribute name. No spaces, no dashes, unique within the command
LabelstringLabel shown in Discord, at most 45 characters
Stylestringshort or paragraph
RequiredbooleanWhether Discord requires a value before submitting
PlaceholderstringOptional hint text, omitted from the generated code when empty

Rules

  • A modal command needs at least one field and at most five
  • A modal command cannot have Args, and a non-modal command cannot have Fields
  • ReturnType is always None, BotBox overwrites whatever you pass
  • Modal commands are app commands, so they are stored in slash_commands in botbox.conf, not in a separate list

What Gets Generated

For the example above, BotBox writes a modal class named after the command plus Modal, then a slash command that opens it.

python
class FeedbackModal(discord.ui.Modal, title="Collects user feedback"):
    subject = discord.ui.TextInput(label="Subject", style=discord.TextStyle.short, required=True, placeholder="Short summary")
    details = discord.ui.TextInput(label="Details", style=discord.TextStyle.paragraph, required=False)

    async def on_submit(self, interaction: discord.Interaction):
        await interaction.response.send_message(f"feedback submitted: subject={self.subject.value} details={self.details.value}", ephemeral=True)

class Feedback(commands.Cog, name="Feedback"):
    def __init__(self, bot) -> None:
        self.bot = bot
        logger.info("feedback cog loaded")

    @app_commands.command(name="feedback", description="Collects user feedback")
    @app_commands.guilds(GUILD)
    async def feedback(self, interaction: discord.Interaction) -> None:
        await interaction.response.send_modal(FeedbackModal())

The modal title comes from the command description, truncated to Discord's 45 character limit.

📝 NOTE
The generated on_submit echoes the submitted values back as an ephemeral message. That is a placeholder so the command works the moment it is generated. Replace the body with whatever you actually want to do with the input, such as writing to a database, posting to a channel, opening a ticket, etc.

Reading the Values

Every field is an attribute on the modal class, and its text is at .value.

python
async def on_submit(self, interaction: discord.Interaction):
        subject = self.subject.value
        details = self.details.value

        logger.info("feedback from %s: %s", interaction.user, subject)

        channel = interaction.client.get_channel(YOUR_CHANNEL_ID)
        await channel.send(f"**{subject}**\n{details}")

        await interaction.response.send_message("Thanks, your feedback was sent.", ephemeral=True)
⚠️ WARNING
An optional field that the user leaves blank gives you an empty string, not None. Check for "" rather than falsy-versus-missing.

Config Sync

botbox config sync detects modal commands by finding a send_modal(...) call in the command body, then reads the TextInput assignments out of the modal class to rebuild the field list. Hand-written modal cogs that follow the generated shape are picked up the same way generated ones are.

sh
botbox config sync

Next Steps