Skip to main content

Overview

Modals are pop-up forms that allow users to input text and other data. CommandKit’s Modal component provides a declarative way to build forms with automatic submission handling.

Basic Usage

import { Modal, ShortInput, ParagraphInput, OnModalKitSubmit } from 'commandkit';
import { MessageFlags } from 'discord.js';

const handleSubmit: OnModalKitSubmit = async (interaction, context) => {
  const name = interaction.fields.getTextInputValue('name');
  const description = interaction.fields.getTextInputValue('description');
  
  await interaction.reply({
    content: `Name: ${name}\nDescription: ${description}`,
    flags: MessageFlags.Ephemeral,
  });
  
  context.dispose();
};

const modal = (
  <Modal title="User Information" onSubmit={handleSubmit}>
    <ShortInput customId="name" placeholder="John Doe" />
    <ParagraphInput customId="description" placeholder="Tell us about yourself..." />
  </Modal>
);

await interaction.showModal(modal);
title
string
required
The title displayed at the top of the modal
customId
string
Custom identifier for the modal. Auto-generated if onSubmit is provided.
children
ModalKit['components'][number] | ModalKit['components'][number][]
The text input components or action rows to display in the modal
onSubmit
OnModalKitSubmit
Handler called when the modal is submitted
onEnd
OnModalKitEnd
Handler called when the interaction collector ends
onError
EventInterceptorErrorHandler
Handler called when an error occurs in the interaction collector
options
CommandKitModalBuilderInteractionCollectorDispatchContextData
Configuration for the interaction collector

Text Input Types

CommandKit provides three text input components:
Single-line text input for short responses:
<ShortInput 
  customId="username"
  placeholder="Enter your username"
  minLength={3}
  maxLength={32}
  required
/>

Text Input Props

customId
string
required
Unique identifier for this input field
placeholder
string
Placeholder text shown when the input is empty
minLength
number
Minimum number of characters required
maxLength
number
Maximum number of characters allowed
value
string
Pre-filled value for the input
required
boolean
default:true
Whether the input is required before submission
label
string
deprecated
The label for the input. Use the <Label> component instead.

Submit Handler

The onSubmit handler receives the modal submission interaction:
import { OnModalKitSubmit } from 'commandkit';

const handleSubmit: OnModalKitSubmit = async (interaction, context) => {
  // Get text input values
  const name = interaction.fields.getTextInputValue('name');
  const email = interaction.fields.getTextInputValue('email');
  
  // Respond to the user
  await interaction.reply({
    content: `Thanks ${name}! We'll contact you at ${email}`,
    flags: MessageFlags.Ephemeral,
  });
  
  // Clean up the interaction collector
  context.dispose();
};

Accessing Field Values

// Text inputs
const textValue = interaction.fields.getTextInputValue('field-id');

// Get raw field
const field = interaction.fields.getField('field-id');

Real-World Examples

User Registration Form

import {
  Modal,
  ShortInput,
  ParagraphInput,
  Label,
  OnModalKitSubmit,
  ChatInputCommand,
  CommandData,
} from 'commandkit';
import { MessageFlags } from 'discord.js';

export const command: CommandData = {
  name: 'register',
  description: 'Register for an event',
};

const handleSubmit: OnModalKitSubmit = async (interaction, context) => {
  const name = interaction.fields.getTextInputValue('name');
  const email = interaction.fields.getTextInputValue('email');
  const reason = interaction.fields.getTextInputValue('reason');
  
  // Process registration...
  
  await interaction.reply({
    content: `Registration submitted!\n\nName: ${name}\nEmail: ${email}`,
    flags: MessageFlags.Ephemeral,
  });
  
  context.dispose();
};

export const chatInput: ChatInputCommand = async ({ interaction }) => {
  const modal = (
    <Modal title="Event Registration" onSubmit={handleSubmit}>
      <Label label="Full Name" description="Enter your full name">
        <ShortInput 
          customId="name" 
          placeholder="John Doe"
          minLength={2}
          maxLength={50}
          required
        />
      </Label>
      
      <Label label="Email Address" description="We'll send confirmation here">
        <ShortInput 
          customId="email" 
          placeholder="[email protected]"
          required
        />
      </Label>
      
      <Label label="Why do you want to attend?" description="Tell us your motivation">
        <ParagraphInput 
          customId="reason" 
          placeholder="I'm interested in..."
          minLength={20}
          maxLength={500}
        />
      </Label>
    </Modal>
  );
  
  await interaction.showModal(modal);
};

Feedback Form

import {
  Modal,
  ShortInput,
  ParagraphInput,
  OnModalKitSubmit,
} from 'commandkit';

const handleFeedback: OnModalKitSubmit = async (interaction, context) => {
  const rating = interaction.fields.getTextInputValue('rating');
  const comments = interaction.fields.getTextInputValue('comments');
  
  // Store feedback in database...
  
  await interaction.reply({
    content: 'Thank you for your feedback!',
    flags: MessageFlags.Ephemeral,
  });
  
  context.dispose();
};

const modal = (
  <Modal title="Provide Feedback" onSubmit={handleFeedback}>
    <ShortInput 
      customId="rating"
      label="Rating (1-5)"
      placeholder="5"
      minLength={1}
      maxLength={1}
      required
    />
    
    <ParagraphInput 
      customId="comments"
      label="Comments"
      placeholder="What did you think?"
      maxLength={1000}
    />
  </Modal>
);
import {
  Modal,
  ShortInput,
  StringSelectMenu,
  StringSelectMenuOption,
  Label,
  OnModalKitSubmit,
} from 'commandkit';

const handleSubmit: OnModalKitSubmit = async (interaction, context) => {
  const name = interaction.fields.getTextInputValue('name');
  const category = interaction.fields.getField('category');
  
  await interaction.reply({
    content: `Name: ${name}\nCategory: ${category}`,
    flags: MessageFlags.Ephemeral,
  });
  
  context.dispose();
};

const modal = (
  <Modal title="Create Ticket" onSubmit={handleSubmit}>
    <Label label="Your Name">
      <ShortInput customId="name" placeholder="John Doe" required />
    </Label>
    
    <Label label="Category" description="Select a category">
      <StringSelectMenu customId="category">
        <StringSelectMenuOption 
          label="Technical Support" 
          value="tech"
          emoji="🔧"
        />
        <StringSelectMenuOption 
          label="Billing" 
          value="billing"
          emoji="💰"
        />
        <StringSelectMenuOption 
          label="General Question" 
          value="general"
          emoji="❓"
        />
      </StringSelectMenu>
    </Label>
  </Modal>
);

Using Labels

The <Label> component provides better structure for form fields:
import { Label } from 'commandkit';

<Modal title="Form" onSubmit={handleSubmit}>
  <Label 
    label="Username" 
    description="This will be visible to other users"
  >
    <ShortInput 
      customId="username" 
      placeholder="cooluser123"
      minLength={3}
      maxLength={20}
      required
    />
  </Label>
</Modal>
The label prop on text inputs is deprecated. Use the <Label> component wrapper instead for better compatibility.

Pre-filling Values

You can pre-fill input fields with default values:
<ShortInput 
  customId="username"
  value={user.name}
  placeholder="Enter username"
/>

Collector Options

Customize modal submission handling:
const options = {
  // How long to wait for submission (default: 5 minutes)
  time: 10 * 60 * 1000, // 10 minutes
  
  // Auto-reset timer (default: false for modals)
  autoReset: false,
  
  // Only allow one submission (default: true for modals)
  once: true,
  
  // Filter submissions
  filter: (interaction) => interaction.user.id === userId,
};

<Modal title="Form" onSubmit={handleSubmit} options={options}>
  {/* ... */}
</Modal>

Best Practices

Keep forms short - Users are more likely to complete shorter forms. Limit to 5 input fields.
Use appropriate input types - ShortInput for single-line text, ParagraphInput for longer responses.
Provide clear labels - Use the <Label> component with descriptive text and descriptions.
Validate input - Use minLength and maxLength to enforce valid input lengths.
Dispose collectors - Always call context.dispose() after handling submissions.
  • Maximum 5 text input fields per modal
  • Modal titles are limited to 45 characters
  • Text input labels are limited to 45 characters
  • ShortInput is limited to 4,000 characters
  • ParagraphInput is limited to 4,000 characters
  • Modals automatically timeout after 15 minutes

Error Handling

Handle errors in modal submissions:
const handleError = (error: Error) => {
  console.error('Modal submission error:', error);
};

<Modal 
  title="Form" 
  onSubmit={handleSubmit}
  onError={handleError}
>
  {/* ... */}
</Modal>

See Also

Build docs developers (and LLMs) love