Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/disgoorg/disgo/llms.txt

Use this file to discover all available pages before exploring further.

Modals are pop-up forms that allow you to collect detailed user input. They’re perfect for complex forms, settings, or any scenario where you need multiple inputs from a user.

Creating a modal

Modals can only be sent as a response to an interaction (slash command or button click).

Basic modal structure

event.Modal(discord.ModalCreate{
    CustomID: "feedback_modal",
    Title:    "Submit Feedback",
    Components: []discord.LayoutComponent{
        discord.NewActionRow(
            discord.NewShortTextInput("name", "Your Name"),
        ),
        discord.NewActionRow(
            discord.NewParagraphTextInput("feedback", "Your Feedback"),
        ),
    },
})

Input types

Modals support two types of text inputs:
discord.NewShortTextInput("username", "Username").
    WithRequired(true).
    WithPlaceholder("Enter your username").
    WithMinLength(3).
    WithMaxLength(20)

Complete modal example

1

Create a slash command that opens a modal

var commands = []discord.ApplicationCommandCreate{
    discord.SlashCommandCreate{
        Name:        "feedback",
        Description: "Submit feedback",
    },
}

func commandListener(event *events.ApplicationCommandInteractionCreate) {
    data := event.SlashCommandInteractionData()
    if data.CommandName() == "feedback" {
        err := event.Modal(discord.ModalCreate{
            CustomID: "feedback_modal",
            Title:    "Submit Feedback",
            Components: []discord.LayoutComponent{
                discord.NewActionRow(
                    discord.NewShortTextInput("name", "Your Name").
                        WithPlaceholder("John Doe").
                        WithRequired(true),
                ),
                discord.NewActionRow(
                    discord.NewShortTextInput("email", "Email").
                        WithPlaceholder("john@example.com").
                        WithRequired(false),
                ),
                discord.NewActionRow(
                    discord.NewParagraphTextInput("feedback", "Feedback").
                        WithPlaceholder("Tell us what you think...").
                        WithMinLength(10).
                        WithMaxLength(500).
                        WithRequired(true),
                ),
            },
        })
        if err != nil {
            event.Client().Logger.Error("error creating modal", slog.Any("err", err))
        }
    }
}
2

Handle modal submission

func modalListener(event *events.ModalSubmitInteractionCreate) {
    if event.Data.CustomID == "feedback_modal" {
        var content strings.Builder
        
        for component := range event.Data.AllComponents() {
            if textInput, ok := component.(discord.TextInputComponent); ok {
                content.WriteString(textInput.CustomID)
                content.WriteString(": ")
                content.WriteString(textInput.Value)
                content.WriteString("\n")
            }
        }

        err := event.CreateMessage(discord.MessageCreate{
            Content: "Thanks for your feedback!\n\n" + content.String(),
            Flags:   discord.MessageFlagEphemeral,
        })
        if err != nil {
            event.Client().Logger.Error("error responding to modal", slog.Any("err", err))
        }
    }
}
3

Register event listeners

client, err := disgo.New(token,
    bot.WithDefaultGateway(),
    bot.WithEventListenerFunc(commandListener),
    bot.WithEventListenerFunc(modalListener),
)

Full working example

package main

import (
    "context"
    "log/slog"
    "os"
    "os/signal"
    "syscall"

    "github.com/disgoorg/disgo"
    "github.com/disgoorg/disgo/bot"
    "github.com/disgoorg/disgo/discord"
    "github.com/disgoorg/disgo/events"
    "github.com/disgoorg/disgo/gateway"
    "github.com/disgoorg/disgo/handler"
)

var (
    token = os.Getenv("disgo_token")

    commands = []discord.ApplicationCommandCreate{
        discord.SlashCommandCreate{
            Name:        "modal",
            Description: "brings up a modal",
        },
    }
)

func main() {
    client, err := disgo.New(token,
        bot.WithGatewayConfigOpts(gateway.WithIntents(gateway.IntentsNone)),
        bot.WithEventListenerFunc(commandListener),
        bot.WithEventListenerFunc(modalListener),
    )
    if err != nil {
        slog.Error("error while building disgo instance", slog.Any("err", err))
        return
    }

    defer client.Close(context.TODO())

    if err = handler.SyncCommands(client, commands, nil); err != nil {
        slog.Error("error while registering commands", slog.Any("err", err))
    }

    if err = client.OpenGateway(context.TODO()); err != nil {
        slog.Error("error while connecting to gateway", slog.Any("err", err))
    }

    slog.Info("bot is now running. Press CTRL-C to exit.")
    s := make(chan os.Signal, 1)
    signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
    <-s
}

func commandListener(event *events.ApplicationCommandInteractionCreate) {
    data := event.SlashCommandInteractionData()
    if data.CommandName() == "modal" {
        if err := event.Modal(discord.ModalCreate{
            Title:    "Modal Title",
            CustomID: "modal-id",
            Components: []discord.LayoutComponent{
                discord.NewActionRow(
                    discord.NewShortTextInput("short-text-input", "Short text"),
                ),
                discord.NewActionRow(
                    discord.NewParagraphTextInput("paragraph-text-input", "Paragraph text"),
                ),
            },
        }); err != nil {
            event.Client().Logger.Error("error creating modal", slog.Any("err", err))
        }
    }
}

func modalListener(event *events.ModalSubmitInteractionCreate) {
    var content string
    for component := range event.Data.AllComponents() {
        if textInput, ok := component.(discord.TextInputComponent); ok {
            content += textInput.CustomID + ": " + textInput.Value + "\n"
        }
    }

    if err := event.CreateMessage(discord.MessageCreate{
        Content: content,
    }); err != nil {
        event.Client().Logger.Error("error responding to modal", slog.Any("err", err))
    }
}

Modals with select menus

You can also include select menus in modals:
event.Modal(discord.ModalCreate{
    CustomID: "profile_modal",
    Title:    "Update Profile",
    Components: []discord.LayoutComponent{
        discord.NewActionRow(
            discord.NewShortTextInput("name", "Display Name"),
        ),
        discord.NewActionRow(
            discord.NewStringSelectMenu(
                "favorite_color",
                "Favorite Color",
                discord.NewStringSelectMenuOption("Red", "red"),
                discord.NewStringSelectMenuOption("Blue", "blue"),
                discord.NewStringSelectMenuOption("Green", "green"),
            ),
        ),
    },
})
Handle the select menu in the modal submission:
func modalListener(event *events.ModalSubmitInteractionCreate) {
    for component := range event.Data.AllComponents() {
        switch c := component.(type) {
        case discord.TextInputComponent:
            // Handle text input
            fmt.Println(c.CustomID, c.Value)
        case discord.StringSelectMenuComponent:
            // Handle select menu
            fmt.Println(c.CustomID, c.Values)
        }
    }
}

Using builder pattern

DisGo supports a builder pattern for cleaner modal creation:
event.Modal(discord.NewModalCreateBuilder().
    SetTitle("Survey").
    SetCustomID("survey_modal").
    AddLabel("Question 1", discord.NewShortTextInput("q1")).
    AddLabel("Question 2", discord.NewParagraphTextInput("q2")).
    Build(),
)

Using the handler package

The handler package provides cleaner modal routing:
import "github.com/disgoorg/disgo/handler"

r := handler.New()
r.SlashCommand("/feedback", onFeedbackCommand)
r.Modal("/feedback_modal", onFeedbackSubmit)

func onFeedbackCommand(_ discord.SlashCommandInteractionData, e *handler.CommandEvent) error {
    return e.Modal(discord.ModalCreate{
        CustomID: "/feedback_modal",
        Title:    "Feedback Form",
        Components: []discord.LayoutComponent{
            discord.NewActionRow(
                discord.NewParagraphTextInput("feedback", "Your Feedback"),
            ),
        },
    })
}

func onFeedbackSubmit(e *handler.ModalEvent) error {
    return e.CreateMessage(discord.MessageCreate{
        Content: "Thanks for your feedback!",
        Flags:   discord.MessageFlagEphemeral,
    })
}

Best practices

  • Keep modals simple: Don’t overwhelm users with too many fields (maximum 5)
  • Use appropriate input types: Short text for names, paragraph for descriptions
  • Provide clear labels: Make it obvious what each field is for
  • Set reasonable limits: Use MinLength and MaxLength to validate input
  • Make fields required: Only when absolutely necessary
  • Use placeholders: Show examples of valid input
  • Always respond: Discord requires a response to modal submissions
  • Validate input: Check submitted data before processing

Limitations

  • Modals can only be sent in response to interactions
  • Maximum of 5 action rows per modal
  • Each text input must be in its own action row
  • Modals timeout after 15 minutes
  • Custom IDs have a maximum length of 100 characters

Next steps