Skip to main content

Select Menus

A select menu is added to a menu in embeds mode via .setSelectMenu(). Each menu supports one select menu, which renders in its own action row alongside any buttons.

Select menu types​

Discord provides five select menu types, each letting the user choose from a different domain. Import the appropriate builder from discord.js:

TypeBuildervalues in onSelect
StringStringSelectMenuBuilderCustom string values you define
UserUserSelectMenuBuilderDiscord user IDs
RoleRoleSelectMenuBuilderDiscord role IDs
ChannelChannelSelectMenuBuilderDiscord channel IDs
MentionableMentionableSelectMenuBuilderUser or role IDs

Basic usage​

import { StringSelectMenuBuilder } from 'discord.js';

.setSelectMenu(() => ({
builder: new StringSelectMenuBuilder()
.setPlaceholder('Choose a category...')
.addOptions([
{ label: 'Fire', value: 'fire', emoji: '🔥' },
{ label: 'Water', value: 'water', emoji: '💧' },
{ label: 'Grass', value: 'grass', emoji: '🌿' },
]),
onSelect: async (ctx, values) => {
ctx.state.set('type', values[0]);
// Menu re-renders automatically after onSelect completes
},
}))

The onSelect callback​

onSelect: (ctx: MenuContext, values: string[]) => Awaitable<void>

values is always an array of strings — even if the select menu only allows one selection. Access the selection with values[0]:

onSelect: async (ctx, values) => {
const selectedId = values[0];
const selectedUser = await ctx.client.users.fetch(selectedId);
ctx.state.set('targetUser', selectedUser.username);
}

Select menu properties​

PropertyTypeRequiredDescription
builderAnySelectMenuBuilderYesAny of the five Discord.js select builders
onSelect(ctx, values) => Awaitable<void>NoCallback fired on selection
idstringNoCustom component ID — auto-assigned if omitted

Discord select types in practice​

User select — lets the user pick a Discord member:

import { UserSelectMenuBuilder } from 'discord.js';

.setSelectMenu(() => ({
builder: new UserSelectMenuBuilder()
.setPlaceholder('Choose a player...'),
onSelect: async (ctx, values) => {
const userId = values[0]; // Discord user ID snowflake
ctx.state.set('targetId', userId);
},
}))

Role select — lets the user pick a server role:

import { RoleSelectMenuBuilder } from 'discord.js';

.setSelectMenu(() => ({
builder: new RoleSelectMenuBuilder()
.setPlaceholder('Assign a role...'),
onSelect: async (ctx, values) => {
const roleId = values[0];
const guild = ctx.interaction.guild;
if (!guild) return;
const member = await guild.members.fetch(ctx.interaction.user.id);
await member.roles.add(roleId);
},
}))

Combining with buttons​

A menu can have both a select menu and buttons. They render in separate action rows:

.setEmbeds(() => [ /* ... */ ])
.setButtons(() => [
{ label: 'Confirm', style: ButtonStyle.Success, action: async (ctx) => { /* ... */ } },
{ label: 'Cancel', style: ButtonStyle.Danger, action: closeMenu() },
])
.setSelectMenu(() => ({
builder: new StringSelectMenuBuilder().addOptions([ /* ... */ ]),
onSelect: async (ctx, values) => { /* ... */ },
}))
note

Select menus cannot be combined with button pagination. If you need paginated options, use a string select with its own options list, or structure the flow so the user picks from the select and the paginated list is a separate menu.