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);
Modal Props
The title displayed at the top of the modal
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
Handler called when the modal is submitted
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:
Text Input Props
Unique identifier for this input field
Placeholder text shown when the input is empty
Minimum number of characters required
Maximum number of characters allowed
Pre-filled value for the input
Whether the input is required before submission
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
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);
};
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.
Modal Limitations
- 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