Select Menus
Overview
Select menus send selected values in a message-component interaction. The components package supports string, user, role, mentionable, and channel select builders. String selects carry application-defined option values; the other types return Discord IDs as strings through ctx.Values().
Tutorial: Build And Read A Select
- Build options with
NewSelectOptionBuilderfor a string select. - Set a stable custom ID and optional min/max values.
- Put the menu in an action row.
- Route the ID with
router.Select. - Read and validate every value returned by
ctx.Values()before using it.
Complete Runnable Example
Copy to examples/select-menus/main.go, set DISCORD_TOKEN, and run it. Invoke /select, choose an option, and inspect the updated message.
package main
import (
"fmt"
"log"
"os"
"github.com/discord-go/discord.go/bot"
"github.com/discord-go/discord.go/components"
"github.com/discord-go/discord.go/intents"
"github.com/discord-go/discord.go/interactions"
)
func main() {
token := os.Getenv("DISCORD_TOKEN")
if token == "" {
log.Fatal("DISCORD_TOKEN is required")
}
router := bot.NewRouter()
router.Command("select", "Choose a deployment environment", func(ctx *bot.InteractionContext) {
options := []components.SelectOption{
components.NewSelectOptionBuilder().SetLabel("Development").SetValue("dev").SetDescription("Local testing").Build(),
components.NewSelectOptionBuilder().SetLabel("Staging").SetValue("stage").SetDescription("Pre-release testing").Build(),
components.NewSelectOptionBuilder().SetLabel("Production").SetValue("prod").SetDescription("Released service").Build(),
}
menu := components.NewStringSelectMenuBuilder().
SetCustomID("select:environment").
SetPlaceholder("Choose one environment").
AddOptions(options[0], options[1], options[2]).
SetMinValues(1).
SetMaxValues(1).
Build()
row := components.NewActionRowBuilder().AddComponents(menu).Build()
if err := ctx.ReplyComplex(&interactions.InteractionCallbackData{
Content: "Select an environment.",
Components: []components.Component{row},
}); err != nil {
log.Printf("select response: %v", err)
}
})
router.Select("select:environment", func(ctx *bot.InteractionContext) {
values := ctx.Values()
if len(values) != 1 || (values[0] != "dev" && values[0] != "stage" && values[0] != "prod") {
_ = ctx.ReplyEphemeral("That selection is not valid.")
return
}
if err := ctx.UpdateContent(fmt.Sprintf("Selected environment: %s", values[0])); err != nil {
log.Printf("select update: %v", err)
}
})
b := bot.New(token, bot.WithIntents(intents.Guilds), bot.WithRouter(router))
if err := b.Run(); err != nil {
log.Fatal(err)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Select Types
NewStringSelectMenuBuilderuses application-definedSelectOptionvalues.NewUserSelectMenuBuilderreturns user IDs.NewRoleSelectMenuBuilderreturns role IDs.NewChannelSelectMenuBuilderreturns channel IDs and can restrict channel types withSetChannelTypes.RoleSelect,UserSelect, andChannelSelectdo not have string options; Discord populates their candidates.
All selected values are client input. Check count, format, guild ownership, and the actor's authority before a REST operation. router.SelectPrefix is useful for a family of menus, but the suffix still needs validation.
Common Mistakes
Wrong:
router.Select("roles", func(ctx *bot.InteractionContext) {
// A selected role ID is not permission to modify that role.
_ = ctx.UpdateContent(ctx.Values()[0])
})2
3
4
Correct handlers check the value and authorization before changing state, and they handle an empty selection safely. Never index ctx.Values() without checking its length.
Expected Result
/select renders a single-choice string menu. Valid values update the source message; malformed or unexpected values receive a private error response.