| name | components-v2-modals |
| description | Guide for creating Discord modals with Labels using Components v2 in Open Guard Use when this capability is needed. |
| metadata | {"author":"erdemgksl"} |
Discord Modals (Components v2)
What this skill does
Quick reference for creating Discord modals using Components v2 with Labels in the Open Guard bot.
When to use
- Create popup forms in Discord
- Collect structured user input (text, selects, files)
- Show modals in response to button clicks or commands
Core Concepts
Modal: Popup form triggered by interactions (button click, command)
Label: Wrapper component with label + optional description for inputs
Creating Modals
Basic Structure
use poise::serenity_prelude as serenity;
let modal = serenity::CreateModal::new(custom_id, title)
.components(vec![
serenity::CreateModalComponent::Label(label),
]);
interaction.create_response(
ctx,
serenity::CreateInteractionResponse::Modal(modal)
).await?;
Label Types
1. Text Input Label
let label = serenity::CreateLabel::input_text(
"Email Address",
serenity::CreateInputText::new(
serenity::InputTextStyle::Short,
"email_field"
)
.placeholder("user@example.com")
.min_length(5)
.max_length(100)
.required(true)
)
.description("We'll never share your email");
2. Select Menu Label
let select = serenity::CreateSelectMenu::new(
"level_select",
serenity::CreateSelectMenuKind::String {
options: vec![
serenity::CreateSelectMenuOption::new("Low", "1"),
serenity::CreateSelectMenuOption::new("High", "2"),
].into()
}
).required(true);
let label = serenity::CreateLabel::select_menu("Priority Level", select)
.description("Choose priority");
3. User Select Label
let select = serenity::CreateSelectMenu::new(
"user_select",
serenity::CreateSelectMenuKind::User {
default_users: Some(vec![user_id].into())
}
)
.min_values(1)
.max_values(3);
let label = serenity::CreateLabel::select_menu("Select Users", select);
4. File Upload Label
let upload = serenity::CreateFileUpload::new("screenshot_upload")
.min_values(1)
.max_values(5)
.required(true);
let label = serenity::CreateLabel::file_upload("Upload Screenshot", upload)
.description("PNG or JPG only");
Complete Example
pub fn build_feedback_modal(id: &str) -> serenity::CreateModal {
serenity::CreateModal::new(
format!("feedback_{}", id),
"Feedback Form"
)
.components(vec![
serenity::CreateModalComponent::Label(
serenity::CreateLabel::input_text(
"Your Feedback",
serenity::CreateInputText::new(
serenity::InputTextStyle::Paragraph,
"feedback_text"
)
.placeholder("Write your feedback...")
.required(true)
)
.description("Tell us what you think")
),
serenity::CreateModalComponent::Label(
serenity::CreateLabel::select_menu(
"Rating",
serenity::CreateSelectMenu::new(
"rating_select",
serenity::CreateSelectMenuKind::String {
options: vec![
serenity::CreateSelectMenuOption::new("Excellent", "5"),
serenity::CreateSelectMenuOption::new("Good", "4"),
serenity::CreateSelectMenuOption::(, ),
].()
}
)
.()
)
),
])
}
Handling Modal Submissions
Extract Data from Submission
async fn handle_modal_submit(
ctx: &serenity::Context,
interaction: &serenity::ModalSubmitInteraction,
) -> Result<(), Error> {
let custom_id = &interaction.data.custom_id;
if custom_id.starts_with("feedback_") {
let rating = extract_string_select_value(
&interaction.data.components,
"rating_select"
);
let feedback = extract_text_input_value(
&interaction.data.components,
"feedback_text"
);
let resolved = &interaction.data.resolved;
let selected_users: Vec<_> = resolved.users.iter().collect();
interaction.create_response(
ctx,
serenity::CreateInteractionResponse::Acknowledge
).await?;
}
Ok(())
}
fn extract_string_select_value(
components: &[serenity::Component],
target_custom_id: &str,
) -> Option<String> {
for component components {
::Component::(label) = component {
::LabelComponent::(menu) = &label.component {
&*menu.custom_id == target_custom_id {
menu.values.().(|s| s.());
}
}
}
}
}
(
components: &[serenity::Component],
target_custom_id: &,
) <> {
components {
::Component::(label) = component {
::LabelComponent::(input) = &label.component {
&*input.custom_id == target_custom_id {
(input.value.());
}
}
}
}
}
Response Pattern
interaction.create_response(
ctx,
serenity::CreateInteractionResponse::Acknowledge
).await?;
let edit = serenity::EditInteractionResponse::new()
.content("Thank you for your feedback!")
.components(vec![]);
interaction.edit_response(ctx, edit).await?;
Best Practices
- Concise labels: Max 45 characters
- Helpful descriptions: Max 100 characters, use sparingly
- Required flags: Only for truly necessary fields
- Unique custom_ids: Use descriptive, unique identifiers
- Modals require interaction: Cannot show proactively
- No disabled components: Disabled fields cause errors in modals
Codebase Examples
src/services/config/whitelist.rs:323-402 - Modal with user/level selects
src/services/config/whitelist.rs:714-755 - Extract modal data
src/services/event_manager/mod.rs:181-236 - Handle modal submissions
See Also
components-v2-buttons - Creating buttons
components-v2-selects - Select menu details
serenity-interactions - Interaction handling patterns
Converted and distributed by TomeVault — claim your Tome and manage your conversions.